diff options
Diffstat (limited to 'tests')
63 files changed, 2135 insertions, 536 deletions
diff --git a/tests/modeltests/choices/models.py b/tests/modeltests/choices/models.py index 550e655e46..e378260598 100644 --- a/tests/modeltests/choices/models.py +++ b/tests/modeltests/choices/models.py @@ -36,4 +36,15 @@ __test__ = {'API_TESTS':""" u'Male' >>> s.get_gender_display() u'Female' + +# If the value for the field doesn't correspond to a valid choice, +# the value itself is provided as a display value. +>>> a.gender = '' +>>> a.get_gender_display() +u'' + +>>> a.gender = 'U' +>>> a.get_gender_display() +u'U' + """} diff --git a/tests/modeltests/custom_methods/models.py b/tests/modeltests/custom_methods/models.py index b0ca4131a5..d420871373 100644 --- a/tests/modeltests/custom_methods/models.py +++ b/tests/modeltests/custom_methods/models.py @@ -31,7 +31,8 @@ class Article(models.Model): SELECT id, headline, pub_date FROM custom_methods_article WHERE pub_date = %s - AND id != %s""", [str(self.pub_date), self.id]) + AND id != %s""", [connection.ops.value_to_db_date(self.pub_date), + self.id]) # The asterisk in "(*row)" tells Python to expand the list into # positional arguments to Article(). return [self.__class__(*row) for row in cursor.fetchall()] diff --git a/tests/modeltests/delete/models.py b/tests/modeltests/delete/models.py index f5b423e9ff..49aab1fb1b 100644 --- a/tests/modeltests/delete/models.py +++ b/tests/modeltests/delete/models.py @@ -42,7 +42,9 @@ class F(DefaultRepr, models.Model): __test__ = {'API_TESTS': """ -# First, some tests for the datastructure we use +### Tests for models A,B,C,D ### + +## First, test the CollectedObjects data structure directly >>> from django.db.models.query import CollectedObjects @@ -72,6 +74,7 @@ Traceback (most recent call last): CyclicDependency: There is a cyclic dependency of items to be processed. +## Second, test the usage of CollectedObjects by Model.delete() # Due to the way that transactions work in the test harness, # doing m.delete() here can work but fail in a real situation, @@ -84,14 +87,21 @@ CyclicDependency: There is a cyclic dependency of items to be processed. # then try again with a known 'tricky' order. Slightly naughty # access to internals here :-) +# If implementation changes, then the tests may need to be simplified: +# - remove the lines that set the .keyOrder and clear the related +# object caches +# - remove the second set of tests (with a2, b2 etc) + >>> from django.db.models.loading import cache +>>> def clear_rel_obj_caches(models): +... for m in models: +... if hasattr(m._meta, '_related_objects_cache'): +... del m._meta._related_objects_cache + # Nice order >>> cache.app_models['delete'].keyOrder = ['a', 'b', 'c', 'd'] ->>> del A._meta._related_objects_cache ->>> del B._meta._related_objects_cache ->>> del C._meta._related_objects_cache ->>> del D._meta._related_objects_cache +>>> clear_rel_obj_caches([A, B, C, D]) >>> a1 = A() >>> a1.save() @@ -110,10 +120,7 @@ CyclicDependency: There is a cyclic dependency of items to be processed. # Same again with a known bad order >>> cache.app_models['delete'].keyOrder = ['d', 'c', 'b', 'a'] ->>> del A._meta._related_objects_cache ->>> del B._meta._related_objects_cache ->>> del C._meta._related_objects_cache ->>> del D._meta._related_objects_cache +>>> clear_rel_obj_caches([A, B, C, D]) >>> a2 = A() >>> a2.save() @@ -130,7 +137,9 @@ CyclicDependency: There is a cyclic dependency of items to be processed. [<class 'modeltests.delete.models.D'>, <class 'modeltests.delete.models.C'>, <class 'modeltests.delete.models.B'>, <class 'modeltests.delete.models.A'>] >>> a2.delete() -# Tests for nullable related fields +### Tests for models E,F - nullable related fields ### + +## First, test the CollectedObjects data structure directly >>> g = CollectedObjects() >>> g.add("key1", 1, "item1", None) @@ -142,6 +151,8 @@ True >>> g.ordered_keys() ['key1', 'key2'] +## Second, test the usage of CollectedObjects by Model.delete() + >>> e1 = E() >>> e1.save() >>> f1 = F(e=e1) diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py index ff86823d07..d6a7c38e63 100644 --- a/tests/modeltests/generic_relations/models.py +++ b/tests/modeltests/generic_relations/models.py @@ -27,11 +27,32 @@ class TaggedItem(models.Model): def __unicode__(self): return self.tag +class Comparison(models.Model): + """ + A model that tests having multiple GenericForeignKeys + """ + comparative = models.CharField(max_length=50) + + content_type1 = models.ForeignKey(ContentType, related_name="comparative1_set") + object_id1 = models.PositiveIntegerField() + + content_type2 = models.ForeignKey(ContentType, related_name="comparative2_set") + object_id2 = models.PositiveIntegerField() + + first_obj = generic.GenericForeignKey(ct_field="content_type1", fk_field="object_id1") + other_obj = generic.GenericForeignKey(ct_field="content_type2", fk_field="object_id2") + + def __unicode__(self): + return u"%s is %s than %s" % (self.first_obj, self.comparative, self.other_obj) + class Animal(models.Model): common_name = models.CharField(max_length=150) latin_name = models.CharField(max_length=150) tags = generic.GenericRelation(TaggedItem) + comparisons = generic.GenericRelation(Comparison, + object_id_field="object_id1", + content_type_field="content_type1") def __unicode__(self): return self.common_name @@ -136,4 +157,38 @@ __test__ = {'API_TESTS':""" >>> Animal.objects.filter(tags__content_type=ctype) [<Animal: Platypus>] +# Simple tests for multiple GenericForeignKeys +# only uses one model, since the above tests should be sufficient. +>>> tiger, cheetah, bear = Animal(common_name="tiger"), Animal(common_name="cheetah"), Animal(common_name="bear") +>>> for o in [tiger, cheetah, bear]: o.save() + +# Create directly +>>> Comparison(first_obj=cheetah, other_obj=tiger, comparative="faster").save() +>>> Comparison(first_obj=tiger, other_obj=cheetah, comparative="cooler").save() + +# Create using GenericRelation +>>> tiger.comparisons.create(other_obj=bear, comparative="cooler") +<Comparison: tiger is cooler than bear> +>>> tiger.comparisons.create(other_obj=cheetah, comparative="stronger") +<Comparison: tiger is stronger than cheetah> + +>>> cheetah.comparisons.all() +[<Comparison: cheetah is faster than tiger>] + +# Filtering works +>>> tiger.comparisons.filter(comparative="cooler") +[<Comparison: tiger is cooler than cheetah>, <Comparison: tiger is cooler than bear>] + +# Filtering and deleting works +>>> subjective = ["cooler"] +>>> tiger.comparisons.filter(comparative__in=subjective).delete() +>>> Comparison.objects.all() +[<Comparison: cheetah is faster than tiger>, <Comparison: tiger is stronger than cheetah>] + +# If we delete cheetah, Comparisons with cheetah as 'first_obj' will be deleted +# since Animal has an explicit GenericRelation to Comparison through first_obj. +# Comparisons with cheetah as 'other_obj' will not be deleted. +>>> cheetah.delete() +>>> Comparison.objects.all() +[<Comparison: tiger is stronger than None>] """} diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py index 48e574af48..470afff4fe 100644 --- a/tests/modeltests/invalid_models/models.py +++ b/tests/modeltests/invalid_models/models.py @@ -110,6 +110,63 @@ class Car(models.Model): class MissingRelations(models.Model): rel1 = models.ForeignKey("Rel1") rel2 = models.ManyToManyField("Rel2") + +class MissingManualM2MModel(models.Model): + name = models.CharField(max_length=5) + missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") + +class Person(models.Model): + name = models.CharField(max_length=5) + +class Group(models.Model): + name = models.CharField(max_length=5) + primary = models.ManyToManyField(Person, through="Membership", related_name="primary") + secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") + tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") + +class GroupTwo(models.Model): + name = models.CharField(max_length=5) + primary = models.ManyToManyField(Person, through="Membership") + secondary = models.ManyToManyField(Group, through="MembershipMissingFK") + +class Membership(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + not_default_or_null = models.CharField(max_length=5) + +class MembershipMissingFK(models.Model): + person = models.ForeignKey(Person) + +class PersonSelfRefM2M(models.Model): + name = models.CharField(max_length=5) + friends = models.ManyToManyField('self', through="Relationship") + too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") + +class PersonSelfRefM2MExplicit(models.Model): + name = models.CharField(max_length=5) + friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) + +class Relationship(models.Model): + first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") + second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") + date_added = models.DateTimeField() + +class ExplicitRelationship(models.Model): + first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") + second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") + date_added = models.DateTimeField() + +class RelationshipTripleFK(models.Model): + first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") + second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") + third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") + date_added = models.DateTimeField() + +class RelationshipDoubleFK(models.Model): + first = models.ForeignKey(Person, related_name="first_related_name") + second = models.ForeignKey(Person, related_name="second_related_name") + third = models.ForeignKey(Group, related_name="rel_to_set") + date_added = models.DateTimeField() model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute. invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. @@ -195,4 +252,12 @@ invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_3' clashes wi invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'. invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed +invalid_models.grouptwo: 'primary' has a manually-defined m2m relation through model Membership, which does not have foreign keys to Person and GroupTwo +invalid_models.grouptwo: 'secondary' has a manually-defined m2m relation through model MembershipMissingFK, which does not have foreign keys to Group and GroupTwo +invalid_models.missingmanualm2mmodel: 'missing_m2m' specifies an m2m relation through model MissingM2MModel, which has not been installed +invalid_models.group: The model Group has two manually-defined m2m relations through the model Membership, which is not permitted. Please consider using an extra field on your intermediary model instead. +invalid_models.group: Intermediary model RelationshipDoubleFK has more than one foreign key to Person, which is ambiguous and is not permitted. +invalid_models.personselfrefm2m: Many-to-many fields with intermediate tables cannot be symmetrical. +invalid_models.personselfrefm2m: Intermediary model RelationshipTripleFK has more than two foreign keys to PersonSelfRefM2M, which is ambiguous and is not permitted. +invalid_models.personselfrefm2mexplicit: Many-to-many fields with intermediate tables cannot be symmetrical. """ diff --git a/tests/modeltests/m2m_through/__init__.py b/tests/modeltests/m2m_through/__init__.py new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/tests/modeltests/m2m_through/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/tests/modeltests/m2m_through/models.py b/tests/modeltests/m2m_through/models.py new file mode 100644 index 0000000000..fa9fa714a5 --- /dev/null +++ b/tests/modeltests/m2m_through/models.py @@ -0,0 +1,337 @@ +from django.db import models +from datetime import datetime + +# M2M described on one of the models +class Person(models.Model): + name = models.CharField(max_length=128) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class Group(models.Model): + name = models.CharField(max_length=128) + members = models.ManyToManyField(Person, through='Membership') + custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom") + nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls") + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return self.name + +class Membership(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + date_joined = models.DateTimeField(default=datetime.now) + invite_reason = models.CharField(max_length=64, null=True) + + class Meta: + ordering = ('date_joined', 'invite_reason', 'group') + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + +class CustomMembership(models.Model): + person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") + group = models.ForeignKey(Group) + weird_fk = models.ForeignKey(Membership, null=True) + date_joined = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + + class Meta: + db_table = "test_table" + +class TestNoDefaultsOrNulls(models.Model): + person = models.ForeignKey(Person) + group = models.ForeignKey(Group) + nodefaultnonull = models.CharField(max_length=5) + +class PersonSelfRefM2M(models.Model): + name = models.CharField(max_length=5) + friends = models.ManyToManyField('self', through="Friendship", symmetrical=False) + + def __unicode__(self): + return self.name + +class Friendship(models.Model): + first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") + second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") + date_friended = models.DateTimeField() + +__test__ = {'API_TESTS':""" +>>> from datetime import datetime + +### Creation and Saving Tests ### + +>>> bob = Person.objects.create(name='Bob') +>>> jim = Person.objects.create(name='Jim') +>>> jane = Person.objects.create(name='Jane') +>>> rock = Group.objects.create(name='Rock') +>>> roll = Group.objects.create(name='Roll') + +# We start out by making sure that the Group 'rock' has no members. +>>> rock.members.all() +[] + +# To make Jim a member of Group Rock, simply create a Membership object. +>>> m1 = Membership.objects.create(person=jim, group=rock) + +# We can do the same for Jane and Rock. +>>> m2 = Membership.objects.create(person=jane, group=rock) + +# Let's check to make sure that it worked. Jane and Jim should be members of Rock. +>>> rock.members.all() +[<Person: Jane>, <Person: Jim>] + +# Now we can add a bunch more Membership objects to test with. +>>> m3 = Membership.objects.create(person=bob, group=roll) +>>> m4 = Membership.objects.create(person=jim, group=roll) +>>> m5 = Membership.objects.create(person=jane, group=roll) + +# We can get Jim's Group membership as with any ForeignKey. +>>> jim.group_set.all() +[<Group: Rock>, <Group: Roll>] + +# Querying the intermediary model works like normal. +# In this case we get Jane's membership to Rock. +>>> m = Membership.objects.get(person=jane, group=rock) +>>> m +<Membership: Jane is a member of Rock> + +# Now we set some date_joined dates for further testing. +>>> m2.invite_reason = "She was just awesome." +>>> m2.date_joined = datetime(2006, 1, 1) +>>> m2.save() + +>>> m5.date_joined = datetime(2004, 1, 1) +>>> m5.save() + +>>> m3.date_joined = datetime(2004, 1, 1) +>>> m3.save() + +# It's not only get that works. Filter works like normal as well. +>>> Membership.objects.filter(person=jim) +[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>] + + +### Forward Descriptors Tests ### + +# Due to complications with adding via an intermediary model, +# the add method is not provided. +>>> rock.members.add(bob) +Traceback (most recent call last): +... +AttributeError: 'ManyRelatedManager' object has no attribute 'add' + +# Create is also disabled as it suffers from the same problems as add. +>>> rock.members.create(name='Anne') +Traceback (most recent call last): +... +AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +# Remove has similar complications, and is not provided either. +>>> rock.members.remove(jim) +Traceback (most recent call last): +... +AttributeError: 'ManyRelatedManager' object has no attribute 'remove' + +# Here we back up the list of all members of Rock. +>>> backup = list(rock.members.all()) + +# ...and we verify that it has worked. +>>> backup +[<Person: Jane>, <Person: Jim>] + +# The clear function should still work. +>>> rock.members.clear() + +# Now there will be no members of Rock. +>>> rock.members.all() +[] + +# Assignment should not work with models specifying a through model for many of +# the same reasons as adding. +>>> rock.members = backup +Traceback (most recent call last): +... +AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +# Let's re-save those instances that we've cleared. +>>> m1.save() +>>> m2.save() + +# Verifying that those instances were re-saved successfully. +>>> rock.members.all() +[<Person: Jane>, <Person: Jim>] + + +### Reverse Descriptors Tests ### + +# Due to complications with adding via an intermediary model, +# the add method is not provided. +>>> bob.group_set.add(rock) +Traceback (most recent call last): +... +AttributeError: 'ManyRelatedManager' object has no attribute 'add' + +# Create is also disabled as it suffers from the same problems as add. +>>> bob.group_set.create(name='Funk') +Traceback (most recent call last): +... +AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +# Remove has similar complications, and is not provided either. +>>> jim.group_set.remove(rock) +Traceback (most recent call last): +... +AttributeError: 'ManyRelatedManager' object has no attribute 'remove' + +# Here we back up the list of all of Jim's groups. +>>> backup = list(jim.group_set.all()) +>>> backup +[<Group: Rock>, <Group: Roll>] + +# The clear function should still work. +>>> jim.group_set.clear() + +# Now Jim will be in no groups. +>>> jim.group_set.all() +[] + +# Assignment should not work with models specifying a through model for many of +# the same reasons as adding. +>>> jim.group_set = backup +Traceback (most recent call last): +... +AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +# Let's re-save those instances that we've cleared. +>>> m1.save() +>>> m4.save() + +# Verifying that those instances were re-saved successfully. +>>> jim.group_set.all() +[<Group: Rock>, <Group: Roll>] + +### Custom Tests ### + +# Let's see if we can query through our second relationship. +>>> rock.custom_members.all() +[] + +# We can query in the opposite direction as well. +>>> bob.custom.all() +[] + +# Let's create some membership objects in this custom relationship. +>>> cm1 = CustomMembership.objects.create(person=bob, group=rock) +>>> cm2 = CustomMembership.objects.create(person=jim, group=rock) + +# If we get the number of people in Rock, it should be both Bob and Jim. +>>> rock.custom_members.all() +[<Person: Bob>, <Person: Jim>] + +# Bob should only be in one custom group. +>>> bob.custom.all() +[<Group: Rock>] + +# Let's make sure our new descriptors don't conflict with the FK related_name. +>>> bob.custom_person_related_name.all() +[<CustomMembership: Bob is a member of Rock>] + +### SELF-REFERENTIAL TESTS ### + +# Let's first create a person who has no friends. +>>> tony = PersonSelfRefM2M.objects.create(name="Tony") +>>> tony.friends.all() +[] + +# Now let's create another person for Tony to be friends with. +>>> chris = PersonSelfRefM2M.objects.create(name="Chris") +>>> f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now()) + +# Tony should now show that Chris is his friend. +>>> tony.friends.all() +[<PersonSelfRefM2M: Chris>] + +# But we haven't established that Chris is Tony's Friend. +>>> chris.friends.all() +[] + +# So let's do that now. +>>> f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now()) + +# Having added Chris as a friend, let's make sure that his friend set reflects +# that addition. +>>> chris.friends.all() +[<PersonSelfRefM2M: Tony>] + +# Chris gets mad and wants to get rid of all of his friends. +>>> chris.friends.clear() + +# Now he should not have any more friends. +>>> chris.friends.all() +[] + +# Since this isn't a symmetrical relation, Tony's friend link still exists. +>>> tony.friends.all() +[<PersonSelfRefM2M: Chris>] + + + +### QUERY TESTS ### + +# We can query for the related model by using its attribute name (members, in +# this case). +>>> Group.objects.filter(members__name='Bob') +[<Group: Roll>] + +# To query through the intermediary model, we specify its model name. +# In this case, membership. +>>> Group.objects.filter(membership__invite_reason="She was just awesome.") +[<Group: Rock>] + +# If we want to query in the reverse direction by the related model, use its +# model name (group, in this case). +>>> Person.objects.filter(group__name="Rock") +[<Person: Jane>, <Person: Jim>] + +# If the m2m field has specified a related_name, using that will work. +>>> Person.objects.filter(custom__name="Rock") +[<Person: Bob>, <Person: Jim>] + +# To query through the intermediary model in the reverse direction, we again +# specify its model name (membership, in this case). +>>> Person.objects.filter(membership__invite_reason="She was just awesome.") +[<Person: Jane>] + +# Let's see all of the groups that Jane joined after 1 Jan 2005: +>>> Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person =jane) +[<Group: Rock>] + +# Queries also work in the reverse direction: Now let's see all of the people +# that have joined Rock since 1 Jan 2005: +>>> Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=rock) +[<Person: Jane>, <Person: Jim>] + +# Conceivably, queries through membership could return correct, but non-unique +# querysets. To demonstrate this, we query for all people who have joined a +# group after 2004: +>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)) +[<Person: Jane>, <Person: Jim>, <Person: Jim>] + +# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'): +>>> [(m.person.name, m.group.name) for m in +... Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))] +[(u'Jane', u'Rock'), (u'Jim', u'Rock'), (u'Jim', u'Roll')] + +# QuerySet's distinct() method can correct this problem. +>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct() +[<Person: Jane>, <Person: Jim>] +"""}
\ No newline at end of file diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py index 081cffb807..2dd1226e97 100644 --- a/tests/modeltests/many_to_one/models.py +++ b/tests/modeltests/many_to_one/models.py @@ -46,8 +46,12 @@ __test__ = {'API_TESTS':""" # Article objects have access to their related Reporter objects. >>> r = a.reporter + +# These are strings instead of unicode strings because that's what was used in +# the creation of this reporter (and we haven't refreshed the data from the +# database, which always returns unicode strings). >>> r.first_name, r.last_name -(u'John', u'Smith') +('John', 'Smith') # Create an Article via the Reporter object. >>> new_article = r.article_set.create(headline="John's second story", pub_date=datetime(2005, 7, 29)) @@ -176,7 +180,7 @@ False [<Article: John's second story>, <Article: Paul's story>, <Article: This is a test>] # You can also use a queryset instead of a literal list of instances. -# The queryset must be reduced to a list of values using values(), +# The queryset must be reduced to a list of values using values(), # then converted into a query >>> Article.objects.filter(reporter__in=Reporter.objects.filter(first_name='John').values('pk').query).distinct() [<Article: John's second story>, <Article: This is a test>] diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index cc9efd0f94..be2a8ba835 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -69,8 +69,10 @@ class ImageFile(models.Model): description = models.CharField(max_length=20) try: # If PIL is available, try testing PIL. - # Otherwise, it's equivalent to TextFile above. - import Image + # Checking for the existence of Image is enough for CPython, but + # for PyPy, you need to check for the underlying modules + # If PIL is not available, this test is equivalent to TextFile above. + import Image, _imaging image = models.ImageField(upload_to=tempfile.gettempdir()) except ImportError: image = models.FileField(upload_to=tempfile.gettempdir()) diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index 5958b8c27a..5b66d1560b 100644 --- a/tests/modeltests/model_formsets/models.py +++ b/tests/modeltests/model_formsets/models.py @@ -1,8 +1,16 @@ from django.db import models +try: + sorted +except NameError: + from django.utils.itercompat import sorted + class Author(models.Model): name = models.CharField(max_length=100) + class Meta: + ordering = ('name',) + def __unicode__(self): return self.name @@ -17,10 +25,14 @@ class AuthorMeeting(models.Model): name = models.CharField(max_length=100) authors = models.ManyToManyField(Author) created = models.DateField(editable=False) - + def __unicode__(self): return self.name +class CustomPrimaryKey(models.Model): + my_pk = models.CharField(max_length=10, primary_key=True) + some_field = models.CharField(max_length=100) + __test__ = {'API_TESTS': """ @@ -41,7 +53,6 @@ __test__ = {'API_TESTS': """ >>> data = { ... 'form-TOTAL_FORMS': '3', # the number of forms rendered ... 'form-INITIAL_FORMS': '0', # the number of forms with initial data -... 'form-MAX_FORMS': '0', # the max number of forms ... 'form-0-name': 'Charles Baudelaire', ... 'form-1-name': 'Arthur Rimbaud', ... 'form-2-name': '', @@ -79,7 +90,6 @@ them in alphabetical order by name. >>> data = { ... 'form-TOTAL_FORMS': '3', # the number of forms rendered ... 'form-INITIAL_FORMS': '2', # the number of forms with initial data -... 'form-MAX_FORMS': '0', # the max number of forms ... 'form-0-id': '2', ... 'form-0-name': 'Arthur Rimbaud', ... 'form-1-id': '1', @@ -123,7 +133,6 @@ deltetion, make sure we don't save that form. >>> data = { ... 'form-TOTAL_FORMS': '4', # the number of forms rendered ... 'form-INITIAL_FORMS': '3', # the number of forms with initial data -... 'form-MAX_FORMS': '0', # the max number of forms ... 'form-0-id': '2', ... 'form-0-name': 'Arthur Rimbaud', ... 'form-1-id': '1', @@ -153,7 +162,6 @@ Let's edit a record to ensure save only returns that one record. >>> data = { ... 'form-TOTAL_FORMS': '4', # the number of forms rendered ... 'form-INITIAL_FORMS': '3', # the number of forms with initial data -... 'form-MAX_FORMS': '0', # the max number of forms ... 'form-0-id': '2', ... 'form-0-name': 'Walt Whitman', ... 'form-1-id': '1', @@ -184,7 +192,6 @@ Test the behavior of commit=False and save_m2m >>> data = { ... 'form-TOTAL_FORMS': '2', # the number of forms rendered ... 'form-INITIAL_FORMS': '1', # the number of forms with initial data -... 'form-MAX_FORMS': '0', # the max number of forms ... 'form-0-id': '1', ... 'form-0-name': '2nd Tuesday of the Week Meeting', ... 'form-0-authors': [2, 1, 3, 4], @@ -201,7 +208,7 @@ True ... instance.save() >>> formset.save_m2m() >>> instances[0].authors.all() -[<Author: Charles Baudelaire>, <Author: Walt Whitman>, <Author: Paul Verlaine>, <Author: John Steinbeck>] +[<Author: Charles Baudelaire>, <Author: John Steinbeck>, <Author: Paul Verlaine>, <Author: Walt Whitman>] # delete the author we created to allow later tests to continue working. >>> new_author.delete() @@ -214,13 +221,14 @@ used. >>> AuthorFormSet = modelformset_factory(Author, max_num=2) >>> formset = AuthorFormSet(queryset=qs) ->>> formset.initial -[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}] +>>> [sorted(x.items()) for x in formset.initial] +[[('id', 1), ('name', u'Charles Baudelaire')], [('id', 3), ('name', u'Paul Verlaine')]] >>> AuthorFormSet = modelformset_factory(Author, max_num=3) >>> formset = AuthorFormSet(queryset=qs) ->>> formset.initial -[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}, {'id': 2, 'name': u'Walt Whitman'}] +>>> [sorted(x.items()) for x in formset.initial] +[[('id', 1), ('name', u'Charles Baudelaire')], [('id', 3), ('name', u'Paul Verlaine')], [('id', 2), ('name', u'Walt Whitman')]] + # Inline Formsets ############################################################ @@ -242,7 +250,6 @@ admin system's edit inline functionality works. >>> data = { ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered ... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data -... 'book_set-MAX_FORMS': '0', # the max number of forms ... 'book_set-0-title': 'Les Fleurs du Mal', ... 'book_set-1-title': '', ... 'book_set-2-title': '', @@ -277,7 +284,6 @@ book. >>> data = { ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered ... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data -... 'book_set-MAX_FORMS': '0', # the max number of forms ... 'book_set-0-id': '1', ... 'book_set-0-title': 'Les Fleurs du Mal', ... 'book_set-1-title': 'Le Spleen de Paris', @@ -293,10 +299,10 @@ True As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire. ->>> for book in author.book_set.order_by('title'): +>>> for book in author.book_set.order_by('id'): ... print book.title -Le Spleen de Paris Les Fleurs du Mal +Le Spleen de Paris The save_as_new parameter lets you re-associate the data to a new instance. This is used in the admin for save_as functionality. @@ -304,7 +310,6 @@ This is used in the admin for save_as functionality. >>> data = { ... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered ... 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data -... 'book_set-MAX_FORMS': '0', # the max number of forms ... 'book_set-0-id': '1', ... 'book_set-0-title': 'Les Fleurs du Mal', ... 'book_set-1-id': '2', @@ -321,4 +326,23 @@ True >>> [book for book in formset.save() if book.author.pk == new_author.pk] [<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>] +Test using a custom prefix on an inline formset. + +>>> formset = AuthorBooksFormSet(prefix="test") +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_test-0-title">Title:</label> <input id="id_test-0-title" type="text" name="test-0-title" maxlength="100" /><input type="hidden" name="test-0-id" id="id_test-0-id" /></p> +<p><label for="id_test-1-title">Title:</label> <input id="id_test-1-title" type="text" name="test-1-title" maxlength="100" /><input type="hidden" name="test-1-id" id="id_test-1-id" /></p> + +# Test a custom primary key ################################################### + +We need to ensure that it is displayed + +>>> CustomPrimaryKeyFormSet = modelformset_factory(CustomPrimaryKey) +>>> formset = CustomPrimaryKeyFormSet() +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_form-0-my_pk">My pk:</label> <input id="id_form-0-my_pk" type="text" name="form-0-my_pk" maxlength="10" /></p> +<p><label for="id_form-0-some_field">Some field:</label> <input id="id_form-0-some_field" type="text" name="form-0-some_field" maxlength="100" /></p> + """} diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index 4b564f2f90..9b79a6a74e 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -4,11 +4,6 @@ Django provides a framework for paginating a list of objects in a few lines of code. This is often useful for dividing search results or long lists of objects into easily readable pages. - -In Django 0.96 and earlier, a single ObjectPaginator class implemented this -functionality. In the Django development version, the behavior is split across -two classes -- Paginator and Page -- that are more easier to use. The legacy -ObjectPaginator class is deprecated. """ from django.db import models @@ -27,9 +22,9 @@ __test__ = {'API_TESTS':""" ... a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) ... a.save() -#################################### -# New/current API (Paginator/Page) # -#################################### +################## +# Paginator/Page # +################## >>> from django.core.paginator import Paginator >>> paginator = Paginator(Article.objects.all(), 5) @@ -140,84 +135,26 @@ True >>> p.end_index() 5 -################################ -# Legacy API (ObjectPaginator) # -################################ - -# Don't print out the deprecation warnings during testing. ->>> from warnings import filterwarnings ->>> filterwarnings("ignore") - ->>> from django.core.paginator import ObjectPaginator, EmptyPage ->>> paginator = ObjectPaginator(Article.objects.all(), 5) ->>> paginator.hits -9 ->>> paginator.pages -2 ->>> paginator.page_range -[1, 2] - -# Get the first page. ->>> paginator.get_page(0) -[<Article: Article 1>, <Article: Article 2>, <Article: Article 3>, <Article: Article 4>, <Article: Article 5>] ->>> paginator.has_next_page(0) -True ->>> paginator.has_previous_page(0) -False ->>> paginator.first_on_page(0) -1 ->>> paginator.last_on_page(0) -5 - -# Get the second page. ->>> paginator.get_page(1) -[<Article: Article 6>, <Article: Article 7>, <Article: Article 8>, <Article: Article 9>] ->>> paginator.has_next_page(1) -False ->>> paginator.has_previous_page(1) -True ->>> paginator.first_on_page(1) -6 ->>> paginator.last_on_page(1) -9 - -# Invalid pages raise EmptyPage. ->>> paginator.get_page(-1) -Traceback (most recent call last): -... -EmptyPage: ... ->>> paginator.get_page(2) -Traceback (most recent call last): -... -EmptyPage: ... - -# Empty paginators with allow_empty_first_page=True. ->>> paginator = ObjectPaginator(Article.objects.filter(id=0), 5) +# Paginator can be passed other objects with a count() method. +>>> class CountContainer: +... def count(self): +... return 42 +>>> paginator = Paginator(CountContainer(), 10) >>> paginator.count -0 +42 >>> paginator.num_pages -1 ->>> paginator.page_range -[1] - -# ObjectPaginator can be passed lists too. ->>> paginator = ObjectPaginator([1, 2, 3], 5) ->>> paginator.hits -3 ->>> paginator.pages -1 +5 >>> paginator.page_range -[1] - +[1, 2, 3, 4, 5] -# ObjectPaginator can be passed other objects with a count() method. ->>> class Container: +# Paginator can be passed other objects that implement __len__. +>>> class LenContainer: ... def __len__(self): ... return 42 ->>> paginator = ObjectPaginator(Container(), 10) ->>> paginator.hits +>>> paginator = Paginator(LenContainer(), 10) +>>> paginator.count 42 ->>> paginator.pages +>>> paginator.num_pages 5 >>> paginator.page_range [1, 2, 3, 4, 5] @@ -237,17 +174,7 @@ EmptyPage: ... 1 # With orphans only set to 1, we should get two pages. ->>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1) +>>> paginator = Paginator(Article.objects.all(), 10, orphans=1) >>> paginator.num_pages 2 - -# LEGACY: With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. ->>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=3) ->>> paginator.pages -1 - -# LEGACY: With orphans only set to 1, we should get two pages. ->>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1) ->>> paginator.pages -2 """} diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py index 63f9f7a361..7ed9d66674 100644 --- a/tests/modeltests/validation/models.py +++ b/tests/modeltests/validation/models.py @@ -16,6 +16,7 @@ class Person(models.Model): birthdate = models.DateField() favorite_moment = models.DateTimeField() email = models.EmailField() + best_time = models.TimeField() def __unicode__(self): return self.name @@ -28,7 +29,8 @@ __test__ = {'API_TESTS':""" ... 'name': 'John', ... 'birthdate': datetime.date(2000, 5, 3), ... 'favorite_moment': datetime.datetime(2002, 4, 3, 13, 23), -... 'email': 'john@example.com' +... 'email': 'john@example.com', +... 'best_time': datetime.time(16, 20), ... } >>> p = Person(**valid_params) >>> p.validate() @@ -130,6 +132,22 @@ datetime.datetime(2002, 4, 3, 13, 23) >>> p.favorite_moment datetime.datetime(2002, 4, 3, 0, 0) +>>> p = Person(**dict(valid_params, best_time='16:20:00')) +>>> p.validate() +{} +>>> p.best_time +datetime.time(16, 20) + +>>> p = Person(**dict(valid_params, best_time='16:20')) +>>> p.validate() +{} +>>> p.best_time +datetime.time(16, 20) + +>>> p = Person(**dict(valid_params, best_time='bar')) +>>> p.validate()['best_time'] +[u'Enter a valid time in HH:MM[:ss[.uuuuuu]] format.'] + >>> p = Person(**dict(valid_params, email='john@example.com')) >>> p.validate() {} @@ -153,5 +171,7 @@ u'john@example.com' [u'This field is required.'] >>> errors['birthdate'] [u'This field is required.'] +>>> errors['best_time'] +[u'This field is required.'] """} diff --git a/tests/regressiontests/maxlength/__init__.py b/tests/regressiontests/admin_registration/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/regressiontests/maxlength/__init__.py +++ b/tests/regressiontests/admin_registration/__init__.py diff --git a/tests/regressiontests/admin_registration/models.py b/tests/regressiontests/admin_registration/models.py new file mode 100644 index 0000000000..fdfa3691b8 --- /dev/null +++ b/tests/regressiontests/admin_registration/models.py @@ -0,0 +1,64 @@ +""" +Tests for various ways of registering models with the admin site. +""" + +from django.db import models +from django.contrib import admin + +class Person(models.Model): + name = models.CharField(max_length=200) + +class Place(models.Model): + name = models.CharField(max_length=200) + +__test__ = {'API_TESTS':""" + + +# Bare registration +>>> site = admin.AdminSite() +>>> site.register(Person) +>>> site._registry[Person] +<django.contrib.admin.options.ModelAdmin object at ...> + +# Registration with a ModelAdmin +>>> site = admin.AdminSite() +>>> class NameAdmin(admin.ModelAdmin): +... list_display = ['name'] +... save_on_top = True + +>>> site.register(Person, NameAdmin) +>>> site._registry[Person] +<regressiontests.admin_registration.models.NameAdmin object at ...> + +# You can't register the same model twice +>>> site.register(Person) +Traceback (most recent call last): + ... +AlreadyRegistered: The model Person is already registered + +# Registration using **options +>>> site = admin.AdminSite() +>>> site.register(Person, search_fields=['name']) +>>> site._registry[Person].search_fields +['name'] + +# With both admin_class and **options the **options override the fields in +# the admin class. +>>> site = admin.AdminSite() +>>> site.register(Person, NameAdmin, search_fields=["name"], list_display=['__str__']) +>>> site._registry[Person].search_fields +['name'] +>>> site._registry[Person].list_display +['__str__'] +>>> site._registry[Person].save_on_top +True + +# You can also register iterables instead of single classes -- a nice shortcut +>>> site = admin.AdminSite() +>>> site.register([Person, Place], search_fields=['name']) +>>> site._registry[Person] +<django.contrib.admin.sites.PersonAdmin object at ...> +>>> site._registry[Place] +<django.contrib.admin.sites.PlaceAdmin object at ...> + +"""} diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 442f357782..f1b167eec7 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -1,18 +1,20 @@ """ -A series of tests to establish that the command-line managment tools work as +A series of tests to establish that the command-line managment tools work as advertised - especially with regards to the handling of the DJANGO_SETTINGS_MODULE and default settings.py files. """ import os import unittest import shutil +import sys +import re from django import conf, bin, get_version from django.conf import settings class AdminScriptTestCase(unittest.TestCase): def write_settings(self, filename, apps=None): - test_dir = os.path.dirname(os.path.dirname(__file__)) + test_dir = os.path.dirname(os.path.dirname(__file__)) settings_file = open(os.path.join(test_dir,filename), 'w') settings_file.write('# Settings file automatically generated by regressiontests.admin_scripts test case\n') exports = [ @@ -27,72 +29,93 @@ class AdminScriptTestCase(unittest.TestCase): for s in exports: if hasattr(settings,s): settings_file.write("%s = '%s'\n" % (s, str(getattr(settings,s)))) - + if apps is None: apps = ['django.contrib.auth', 'django.contrib.contenttypes', 'admin_scripts'] if apps: settings_file.write("INSTALLED_APPS = %s\n" % apps) - + settings_file.close() - + def remove_settings(self, filename): - test_dir = os.path.dirname(os.path.dirname(__file__)) - os.remove(os.path.join(test_dir, filename)) - # Also try to remove the pyc file; if it exists, it could + test_dir = os.path.dirname(os.path.dirname(__file__)) + full_name = os.path.join(test_dir, filename) + os.remove(full_name) + + # Also try to remove the compiled file; if it exists, it could # mess up later tests that depend upon the .py file not existing try: - os.remove(os.path.join(test_dir, filename + 'c')) + if sys.platform.startswith('java'): + # Jython produces module$py.class files + os.remove(re.sub(r'\.py$', '$py.class', full_name)) + else: + # CPython produces module.pyc files + os.remove(full_name + 'c') except OSError: pass - + + def _sys_executable(self): + """ + Returns the command line needed to run a python interpreter, including + the options for setting sys.path on Jython, which doesn't recognize + PYTHONPATH. + """ + if sys.platform.startswith('java'): + return "%s -J-Dpython.path=%s" % \ + (sys.executable, os.environ['PYTHONPATH']) + else: + return sys.executable + def run_test(self, script, args, settings_file=None, apps=None): test_dir = os.path.dirname(os.path.dirname(__file__)) project_dir = os.path.dirname(test_dir) base_dir = os.path.dirname(project_dir) - - # Build the command line - cmd = 'python "%s"' % script - cmd += ''.join([' %s' % arg for arg in args]) - + # Remember the old environment old_django_settings_module = os.environ.get('DJANGO_SETTINGS_MODULE', None) old_python_path = os.environ.get('PYTHONPATH', None) old_cwd = os.getcwd() - + # Set the test environment if settings_file: os.environ['DJANGO_SETTINGS_MODULE'] = settings_file elif 'DJANGO_SETTINGS_MODULE' in os.environ: del os.environ['DJANGO_SETTINGS_MODULE'] - - os.environ['PYTHONPATH'] = os.pathsep.join([test_dir,base_dir]) + + if old_python_path: + os.environ['PYTHONPATH'] = os.pathsep.join([test_dir, base_dir, old_python_path]) + else: + os.environ['PYTHONPATH'] = os.pathsep.join([test_dir, base_dir]) + + # Build the command line + cmd = '%s "%s"' % (self._sys_executable(), script) + cmd += ''.join([' %s' % arg for arg in args]) # Move to the test directory and run os.chdir(test_dir) stdin, stdout, stderr = os.popen3(cmd) out, err = stdout.read(), stderr.read() - + # Restore the old environment if old_django_settings_module: os.environ['DJANGO_SETTINGS_MODULE'] = old_django_settings_module if old_python_path: os.environ['PYTHONPATH'] = old_python_path - # Move back to the old working directory os.chdir(old_cwd) - + return out, err - + def run_django_admin(self, args, settings_file=None): bin_dir = os.path.dirname(bin.__file__) return self.run_test(os.path.join(bin_dir,'django-admin.py'), args, settings_file) - + def run_manage(self, args, settings_file=None): conf_dir = os.path.dirname(conf.__file__) template_manage_py = os.path.join(conf_dir, 'project_template', 'manage.py') - test_dir = os.path.dirname(os.path.dirname(__file__)) + test_dir = os.path.dirname(os.path.dirname(__file__)) test_manage_py = os.path.join(test_dir, 'manage.py') shutil.copyfile(template_manage_py, test_manage_py) @@ -100,7 +123,7 @@ class AdminScriptTestCase(unittest.TestCase): # Cleanup - remove the generated manage.py script os.remove(test_manage_py) - + return stdout, stderr def assertNoOutput(self, stream): @@ -119,14 +142,14 @@ class AdminScriptTestCase(unittest.TestCase): class DjangoAdminNoSettings(AdminScriptTestCase): "A series of tests for django-admin.py when there is no settings.py file." - + def test_builtin_command(self): "no settings: django-admin builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') - + def test_builtin_with_bad_settings(self): "no settings: django-admin builtin commands fail if settings file (from argument) doesn't exist" args = ['sqlall','--settings=bad_settings', 'admin_scripts'] @@ -148,17 +171,17 @@ class DjangoAdminDefaultSettings(AdminScriptTestCase): """ def setUp(self): self.write_settings('settings.py') - + def tearDown(self): self.remove_settings('settings.py') - + def test_builtin_command(self): "default: django-admin builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') - + def test_builtin_with_settings(self): "default: django-admin builtin commands succeed if settings are provided as argument" args = ['sqlall','--settings=settings', 'admin_scripts'] @@ -193,14 +216,14 @@ class DjangoAdminDefaultSettings(AdminScriptTestCase): out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + def test_custom_command_with_settings(self): "default: django-admin can't execute user commands, even if settings are provided as argument" args = ['noargs_command', '--settings=settings'] out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + def test_custom_command_with_environment(self): "default: django-admin can't execute user commands, even if settings are provided in environment" args = ['noargs_command'] @@ -214,17 +237,17 @@ class DjangoAdminMinimalSettings(AdminScriptTestCase): """ def setUp(self): self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) - + def tearDown(self): self.remove_settings('settings.py') - + def test_builtin_command(self): "minimal: django-admin builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') - + def test_builtin_with_settings(self): "minimal: django-admin builtin commands fail if settings are provided as argument" args = ['sqlall','--settings=settings', 'admin_scripts'] @@ -252,21 +275,21 @@ class DjangoAdminMinimalSettings(AdminScriptTestCase): out, err = self.run_django_admin(args,'bad_settings') self.assertNoOutput(out) self.assertOutput(err, "Could not import settings 'bad_settings'") - + def test_custom_command(self): "minimal: django-admin can't execute user commands" args = ['noargs_command'] out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + def test_custom_command_with_settings(self): "minimal: django-admin can't execute user commands, even if settings are provided as argument" args = ['noargs_command', '--settings=settings'] out, err = self.run_django_admin(args) self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + def test_custom_command_with_environment(self): "minimal: django-admin can't execute user commands, even if settings are provided in environment" args = ['noargs_command'] @@ -280,10 +303,10 @@ class DjangoAdminAlternateSettings(AdminScriptTestCase): """ def setUp(self): self.write_settings('alternate_settings.py') - + def tearDown(self): self.remove_settings('alternate_settings.py') - + def test_builtin_command(self): "alternate: django-admin builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] @@ -344,17 +367,17 @@ class DjangoAdminAlternateSettings(AdminScriptTestCase): class DjangoAdminMultipleSettings(AdminScriptTestCase): """A series of tests for django-admin.py when multiple settings files (including the default 'settings.py') are available. The default settings - file is insufficient for performing the operations described, so the + file is insufficient for performing the operations described, so the alternate settings must be used by the running script. """ def setUp(self): self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) self.write_settings('alternate_settings.py') - + def tearDown(self): self.remove_settings('settings.py') self.remove_settings('alternate_settings.py') - + def test_builtin_command(self): "alternate: django-admin builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] @@ -409,7 +432,7 @@ class DjangoAdminMultipleSettings(AdminScriptTestCase): out, err = self.run_django_admin(args,'alternate_settings') self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + ########################################################################## # MANAGE.PY TESTS # This next series of test classes checks the environment processing @@ -418,14 +441,14 @@ class DjangoAdminMultipleSettings(AdminScriptTestCase): class ManageNoSettings(AdminScriptTestCase): "A series of tests for manage.py when there is no settings.py file." - + def test_builtin_command(self): "no settings: manage.py builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) self.assertOutput(err, "Can't find the file 'settings.py' in the directory containing './manage.py'") - + def test_builtin_with_bad_settings(self): "no settings: manage.py builtin commands fail if settings file (from argument) doesn't exist" args = ['sqlall','--settings=bad_settings', 'admin_scripts'] @@ -447,17 +470,17 @@ class ManageDefaultSettings(AdminScriptTestCase): """ def setUp(self): self.write_settings('settings.py') - + def tearDown(self): self.remove_settings('settings.py') - + def test_builtin_command(self): "default: manage.py builtin commands succeed when default settings are appropriate" args = ['sqlall','admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, 'CREATE TABLE') - + def test_builtin_with_settings(self): "default: manage.py builtin commands succeed if settings are provided as argument" args = ['sqlall','--settings=settings', 'admin_scripts'] @@ -492,14 +515,14 @@ class ManageDefaultSettings(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:NoArgsCommand") - + def test_custom_command_with_settings(self): "default: manage.py can execute user commands when settings are provided as argument" args = ['noargs_command', '--settings=settings'] out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:NoArgsCommand") - + def test_custom_command_with_environment(self): "default: manage.py can execute user commands when settings are provided in environment" args = ['noargs_command'] @@ -513,17 +536,17 @@ class ManageMinimalSettings(AdminScriptTestCase): """ def setUp(self): self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) - + def tearDown(self): self.remove_settings('settings.py') - + def test_builtin_command(self): "minimal: manage.py builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) self.assertOutput(err, 'App with label admin_scripts could not be found') - + def test_builtin_with_settings(self): "minimal: manage.py builtin commands fail if settings are provided as argument" args = ['sqlall','--settings=settings', 'admin_scripts'] @@ -551,21 +574,21 @@ class ManageMinimalSettings(AdminScriptTestCase): out, err = self.run_manage(args,'bad_settings') self.assertNoOutput(out) self.assertOutput(err, 'App with label admin_scripts could not be found') - + def test_custom_command(self): "minimal: manage.py can't execute user commands without appropriate settings" args = ['noargs_command'] out, err = self.run_manage(args) self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + def test_custom_command_with_settings(self): "minimal: manage.py can't execute user commands, even if settings are provided as argument" args = ['noargs_command', '--settings=settings'] out, err = self.run_manage(args) self.assertNoOutput(out) self.assertOutput(err, "Unknown command: 'noargs_command'") - + def test_custom_command_with_environment(self): "minimal: manage.py can't execute user commands, even if settings are provided in environment" args = ['noargs_command'] @@ -579,10 +602,10 @@ class ManageAlternateSettings(AdminScriptTestCase): """ def setUp(self): self.write_settings('alternate_settings.py') - + def tearDown(self): self.remove_settings('alternate_settings.py') - + def test_builtin_command(self): "alternate: manage.py builtin commands fail with an import error when no default settings provided" args = ['sqlall','admin_scripts'] @@ -643,17 +666,17 @@ class ManageAlternateSettings(AdminScriptTestCase): class ManageMultipleSettings(AdminScriptTestCase): """A series of tests for manage.py when multiple settings files (including the default 'settings.py') are available. The default settings - file is insufficient for performing the operations described, so the + file is insufficient for performing the operations described, so the alternate settings must be used by the running script. """ def setUp(self): self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) self.write_settings('alternate_settings.py') - + def tearDown(self): self.remove_settings('settings.py') self.remove_settings('alternate_settings.py') - + def test_builtin_command(self): "multiple: manage.py builtin commands fail with an import error when no settings provided" args = ['sqlall','admin_scripts'] @@ -721,10 +744,10 @@ class CommandTypes(AdminScriptTestCase): "Tests for the various types of base command types that can be defined." def setUp(self): self.write_settings('settings.py') - + def tearDown(self): self.remove_settings('settings.py') - + def test_version(self): "--version is handled as a special case" args = ['--version'] @@ -737,7 +760,10 @@ class CommandTypes(AdminScriptTestCase): "--help is handled as a special case" args = ['--help'] out, err = self.run_manage(args) - self.assertOutput(out, "Usage: manage.py [options]") + if sys.version_info < (2, 5): + self.assertOutput(out, "usage: manage.py [options]") + else: + self.assertOutput(out, "Usage: manage.py [options]") self.assertOutput(err, "Type 'manage.py help <subcommand>' for help on a specific subcommand.") def test_specific_help(self): @@ -746,14 +772,14 @@ class CommandTypes(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s).") - + def test_base_command(self): "User BaseCommands can execute when a label is provided" args = ['base_command','testlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") - + def test_base_command_no_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command'] @@ -781,7 +807,7 @@ class CommandTypes(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None)]") - + def test_noargs(self): "NoArg Commands can be executed" args = ['noargs_command'] @@ -794,16 +820,16 @@ class CommandTypes(AdminScriptTestCase): args = ['noargs_command','argument'] out, err = self.run_manage(args) self.assertOutput(err, "Error: Command doesn't accept any arguments") - + def test_app_command(self): "User AppCommands can execute when a single app name is provided" args = ['app_command', 'auth'] out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.auth.models'") - self.assertOutput(out, os.sep.join(['django','contrib','auth','models.py'])) + self.assertOutput(out, os.sep.join(['django','contrib','auth','models.py'])) self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") - + def test_app_command_no_apps(self): "User AppCommands raise an error when no app name is provided" args = ['app_command'] @@ -816,7 +842,8 @@ class CommandTypes(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.auth.models'") - self.assertOutput(out, os.sep.join(['django','contrib','auth','models.pyc']) + "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") + self.assertOutput(out, os.sep.join(['django','contrib','auth','models.py'])) + self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.contenttypes.models'") self.assertOutput(out, os.sep.join(['django','contrib','contenttypes','models.py'])) self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") @@ -826,7 +853,7 @@ class CommandTypes(AdminScriptTestCase): args = ['app_command', 'NOT_AN_APP'] out, err = self.run_manage(args) self.assertOutput(err, "App with label NOT_AN_APP could not be found") - + def test_app_command_some_invalid_appnames(self): "User AppCommands can execute when some of the provided app names are invalid" args = ['app_command', 'auth', 'NOT_AN_APP'] @@ -839,7 +866,7 @@ class CommandTypes(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None)]") - + def test_label_command_no_label(self): "User LabelCommands raise an error if no label is provided" args = ['label_command'] @@ -859,14 +886,14 @@ class ArgumentOrder(AdminScriptTestCase): django-admin command arguments are parsed in 2 parts; the core arguments (--settings, --traceback and --pythonpath) are parsed using a Lax parser. - This Lax parser ignores any unknown options. Then the full settings are + This Lax parser ignores any unknown options. Then the full settings are passed to the command parser, which extracts commands of interest to the - individual command. + individual command. """ def setUp(self): self.write_settings('settings.py', apps=['django.contrib.auth','django.contrib.contenttypes']) self.write_settings('alternate_settings.py') - + def tearDown(self): self.remove_settings('settings.py') self.remove_settings('alternate_settings.py') diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index ad50928aa9..acdf9f5742 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -102,7 +102,7 @@ class AdminViewPermissionsTest(TestCase): self.failUnlessEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_login) self.assertRedirects(login, '/test_admin/admin/') - self.assertFalse(login.context) + self.failIf(login.context) self.client.get('/test_admin/admin/logout/') # Test if user enters e-mail address @@ -124,7 +124,7 @@ class AdminViewPermissionsTest(TestCase): self.failUnlessEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.adduser_login) self.assertRedirects(login, '/test_admin/admin/') - self.assertFalse(login.context) + self.failIf(login.context) self.client.get('/test_admin/admin/logout/') # Change User @@ -132,7 +132,7 @@ class AdminViewPermissionsTest(TestCase): self.failUnlessEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.changeuser_login) self.assertRedirects(login, '/test_admin/admin/') - self.assertFalse(login.context) + self.failIf(login.context) self.client.get('/test_admin/admin/logout/') # Delete User @@ -140,7 +140,7 @@ class AdminViewPermissionsTest(TestCase): self.failUnlessEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.deleteuser_login) self.assertRedirects(login, '/test_admin/admin/') - self.assertFalse(login.context) + self.failIf(login.context) self.client.get('/test_admin/admin/logout/') # Regular User should not be able to login. diff --git a/tests/regressiontests/auth_backends/tests.py b/tests/regressiontests/auth_backends/tests.py index 3ec2a059ad..d22f0bf939 100644 --- a/tests/regressiontests/auth_backends/tests.py +++ b/tests/regressiontests/auth_backends/tests.py @@ -4,7 +4,7 @@ except NameError: from sets import Set as set # Python 2.3 fallback __test__ = {'API_TESTS': """ ->>> from django.contrib.auth.models import User, Group, Permission +>>> from django.contrib.auth.models import User, Group, Permission, AnonymousUser >>> from django.contrib.contenttypes.models import ContentType # No Permissions assigned yet, should return False except for superuser @@ -69,4 +69,10 @@ True True >>> user.has_perms(['auth.test3', 'auth.test_group']) True + +>>> user = AnonymousUser() +>>> user.has_perm('test') +False +>>> user.has_perms(['auth.test2', 'auth.test3']) +False """} diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index f050348c77..78c32288b6 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -3,11 +3,17 @@ # Unit tests for cache framework # Uses whatever cache backend is set in the test settings file. +import os +import shutil +import tempfile import time import unittest + from django.core.cache import cache -from django.utils.cache import patch_vary_headers +from django.core.cache.backends.filebased import CacheClass as FileCache from django.http import HttpResponse +from django.utils.cache import patch_vary_headers +from django.utils.hashcompat import md5_constructor # functions/classes for complex data type tests def f(): @@ -27,7 +33,7 @@ class Cache(unittest.TestCase): cache.add("addkey1", "value") cache.add("addkey1", "newvalue") self.assertEqual(cache.get("addkey1"), "value") - + def test_non_existent(self): # get with non-existent keys self.assertEqual(cache.get("does_not_exist"), None) @@ -80,9 +86,9 @@ class Cache(unittest.TestCase): cache.set('expire2', 'very quickly', 1) cache.set('expire3', 'very quickly', 1) - time.sleep(2) + time.sleep(2) self.assertEqual(cache.get("expire1"), None) - + cache.add("expire2", "newvalue") self.assertEqual(cache.get("expire2"), "newvalue") self.assertEqual(cache.has_key("expire3"), False) @@ -98,11 +104,6 @@ class Cache(unittest.TestCase): cache.set(key, value) self.assertEqual(cache.get(key), value) -import os -import md5 -import shutil -import tempfile -from django.core.cache.backends.filebased import CacheClass as FileCache class FileBasedCacheTests(unittest.TestCase): """ @@ -112,23 +113,23 @@ class FileBasedCacheTests(unittest.TestCase): self.dirname = tempfile.mktemp() os.mkdir(self.dirname) self.cache = FileCache(self.dirname, {}) - + def tearDown(self): shutil.rmtree(self.dirname) - + def test_hashing(self): """Test that keys are hashed into subdirectories correctly""" self.cache.set("foo", "bar") - keyhash = md5.new("foo").hexdigest() + keyhash = md5_constructor("foo").hexdigest() keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) self.assert_(os.path.exists(keypath)) - + def test_subdirectory_removal(self): """ Make sure that the created subdirectories are correctly removed when empty. """ self.cache.set("foo", "bar") - keyhash = md5.new("foo").hexdigest() + keyhash = md5_constructor("foo").hexdigest() keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) self.assert_(os.path.exists(keypath)) @@ -139,9 +140,9 @@ class FileBasedCacheTests(unittest.TestCase): class CacheUtils(unittest.TestCase): """TestCase for django.utils.cache functions.""" - + def test_patch_vary_headers(self): - headers = ( + headers = ( # Initial vary, new headers, resulting vary. (None, ('Accept-Encoding',), 'Accept-Encoding'), ('Accept-Encoding', ('accept-encoding',), 'Accept-Encoding'), diff --git a/tests/regressiontests/maxlength/models.py b/tests/regressiontests/context_processors/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/regressiontests/maxlength/models.py +++ b/tests/regressiontests/context_processors/__init__.py diff --git a/tests/regressiontests/context_processors/models.py b/tests/regressiontests/context_processors/models.py new file mode 100644 index 0000000000..cde172db68 --- /dev/null +++ b/tests/regressiontests/context_processors/models.py @@ -0,0 +1 @@ +# Models file for tests to run. diff --git a/tests/regressiontests/context_processors/templates/context_processors/request_attrs.html b/tests/regressiontests/context_processors/templates/context_processors/request_attrs.html new file mode 100644 index 0000000000..3978e9d680 --- /dev/null +++ b/tests/regressiontests/context_processors/templates/context_processors/request_attrs.html @@ -0,0 +1,13 @@ +{% if request %} +Have request +{% else %} +No request +{% endif %} + +{% if request.is_secure %} +Secure +{% else %} +Not secure +{% endif %} + +{{ request.path }} diff --git a/tests/regressiontests/context_processors/tests.py b/tests/regressiontests/context_processors/tests.py new file mode 100644 index 0000000000..eadd6310b1 --- /dev/null +++ b/tests/regressiontests/context_processors/tests.py @@ -0,0 +1,38 @@ +""" +Tests for Django's bundled context processors. +""" + +from django.conf import settings +from django.test import TestCase + + +class RequestContextProcessorTests(TestCase): + """ + Tests for the ``django.core.context_processors.request`` processor. + """ + + urls = 'regressiontests.context_processors.urls' + + def test_request_attributes(self): + """ + Test that the request object is available in the template and that its + attributes can't be overridden by GET and POST parameters (#3828). + """ + url = '/request_attrs/' + # We should have the request object in the template. + response = self.client.get(url) + self.assertContains(response, 'Have request') + # Test is_secure. + response = self.client.get(url) + self.assertContains(response, 'Not secure') + response = self.client.get(url, {'is_secure': 'blah'}) + self.assertContains(response, 'Not secure') + response = self.client.post(url, {'is_secure': 'blah'}) + self.assertContains(response, 'Not secure') + # Test path. + response = self.client.get(url) + self.assertContains(response, url) + response = self.client.get(url, {'path': '/blah/'}) + self.assertContains(response, url) + response = self.client.post(url, {'path': '/blah/'}) + self.assertContains(response, url) diff --git a/tests/regressiontests/context_processors/urls.py b/tests/regressiontests/context_processors/urls.py new file mode 100644 index 0000000000..7e8ba967c1 --- /dev/null +++ b/tests/regressiontests/context_processors/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +import views + + +urlpatterns = patterns('', + (r'^request_attrs/$', views.request_processor), +) diff --git a/tests/regressiontests/context_processors/views.py b/tests/regressiontests/context_processors/views.py new file mode 100644 index 0000000000..66e7132c05 --- /dev/null +++ b/tests/regressiontests/context_processors/views.py @@ -0,0 +1,8 @@ +from django.core import context_processors +from django.shortcuts import render_to_response +from django.template.context import RequestContext + + +def request_processor(request): + return render_to_response('context_processors/request_attrs.html', + RequestContext(request, {}, processors=[context_processors.request])) diff --git a/tests/regressiontests/dispatch/tests/test_dispatcher.py b/tests/regressiontests/dispatch/tests/test_dispatcher.py index 0bc99a1505..f34173972d 100644 --- a/tests/regressiontests/dispatch/tests/test_dispatcher.py +++ b/tests/regressiontests/dispatch/tests/test_dispatcher.py @@ -2,6 +2,18 @@ from django.dispatch.dispatcher import * from django.dispatch import dispatcher, robust import unittest import copy +import sys +import gc + +if sys.platform.startswith('java'): + def garbage_collect(): + """Run the garbage collector and wait a bit to let it do his work""" + import time + gc.collect() + time.sleep(0.1) +else: + def garbage_collect(): + gc.collect() def x(a): return a @@ -21,6 +33,7 @@ class DispatcherTests(unittest.TestCase): def setUp(self): # track the initial state, since it's possible that others have bleed receivers in + garbage_collect() self.sendersBack = copy.copy(dispatcher.sendersBack) self.connections = copy.copy(dispatcher.connections) self.senders = copy.copy(dispatcher.senders) @@ -86,6 +99,7 @@ class DispatcherTests(unittest.TestCase): connect(a.a, signal, b) expected = [] del a + garbage_collect() result = send('this',b, a=b) self.assertEqual(result, expected) self.assertEqual(list(getAllReceivers(b,signal)), []) @@ -101,6 +115,7 @@ class DispatcherTests(unittest.TestCase): connect(a, signal, b) expected = [] del a + garbage_collect() result = send('this',b, a=b) self.assertEqual(result, expected) self.assertEqual(list(getAllReceivers(b,signal)), []) @@ -123,6 +138,7 @@ class DispatcherTests(unittest.TestCase): del a del b del result + garbage_collect() self._testIsClean() def testRobust(self): @@ -141,4 +157,4 @@ def getSuite(): return unittest.makeSuite(DispatcherTests,'test') if __name__ == "__main__": - unittest.main () + unittest.main() diff --git a/tests/regressiontests/file_uploads/models.py b/tests/regressiontests/file_uploads/models.py index 2d5607b2a7..3701750afe 100644 --- a/tests/regressiontests/file_uploads/models.py +++ b/tests/regressiontests/file_uploads/models.py @@ -1,2 +1,9 @@ -# This file unintentionally left blank. -# Oops.
\ No newline at end of file +import tempfile +import os +from django.db import models + +UPLOAD_ROOT = tempfile.mkdtemp() +UPLOAD_TO = os.path.join(UPLOAD_ROOT, 'test_upload') + +class FileModel(models.Model): + testfile = models.FileField(upload_to=UPLOAD_TO) diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py index d2b581686f..dd6b7c4181 100644 --- a/tests/regressiontests/file_uploads/tests.py +++ b/tests/regressiontests/file_uploads/tests.py @@ -1,8 +1,15 @@ import os -import sha -import tempfile +import errno +import shutil +import unittest + +from django.core.files import temp as tempfile +from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase, client from django.utils import simplejson +from django.utils.hashcompat import sha_constructor + +from models import FileModel, UPLOAD_ROOT, UPLOAD_TO class FileUploadTests(TestCase): def test_simple_upload(self): @@ -15,7 +22,7 @@ class FileUploadTests(TestCase): def test_large_upload(self): tdir = tempfile.gettempdir() - + file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir) file1.write('a' * (2 ** 21)) file1.seek(0) @@ -38,10 +45,10 @@ class FileUploadTests(TestCase): for key in post_data.keys(): try: - post_data[key + '_hash'] = sha.new(post_data[key].read()).hexdigest() + post_data[key + '_hash'] = sha_constructor(post_data[key].read()).hexdigest() post_data[key].seek(0) except AttributeError: - post_data[key + '_hash'] = sha.new(post_data[key]).hexdigest() + post_data[key + '_hash'] = sha_constructor(post_data[key]).hexdigest() response = self.client.post('/file_uploads/verify/', post_data) @@ -51,11 +58,11 @@ class FileUploadTests(TestCase): pass self.assertEqual(response.status_code, 200) - + def test_dangerous_file_names(self): """Uploaded file names should be sanitized before ever reaching the view.""" # This test simulates possible directory traversal attacks by a - # malicious uploader We have to do some monkeybusiness here to construct + # malicious uploader We have to do some monkeybusiness here to construct # a malicious payload with an invalid file name (containing os.sep or # os.pardir). This similar to what an attacker would need to do when # trying such an attack. @@ -72,7 +79,7 @@ class FileUploadTests(TestCase): "..\\..\\hax0rd.txt", # Relative path, win-style. "../..\\hax0rd.txt" # Relative path, mixed. ] - + payload = [] for i, name in enumerate(scary_file_names): payload.extend([ @@ -86,7 +93,7 @@ class FileUploadTests(TestCase): '--' + client.BOUNDARY + '--', '', ]) - + payload = "\r\n".join(payload) r = { 'CONTENT_LENGTH': len(payload), @@ -102,7 +109,7 @@ class FileUploadTests(TestCase): for i, name in enumerate(scary_file_names): got = recieved["file%s" % i] self.assertEqual(got, "hax0rd.txt") - + def test_filename_overflow(self): """File names over 256 characters (dangerous on some platforms) get fixed up.""" name = "%s.txt" % ("f"*500) @@ -124,26 +131,26 @@ class FileUploadTests(TestCase): } got = simplejson.loads(self.client.request(**r).content) self.assert_(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) - + def test_custom_upload_handler(self): - # A small file (under the 5M quota) + # A small file (under the 5M quota) smallfile = tempfile.NamedTemporaryFile() smallfile.write('a' * (2 ** 21)) # A big file (over the quota) bigfile = tempfile.NamedTemporaryFile() bigfile.write('a' * (10 * 2 ** 20)) - + # Small file posting should work. response = self.client.post('/file_uploads/quota/', {'f': open(smallfile.name)}) got = simplejson.loads(response.content) self.assert_('f' in got) - + # Large files don't go through. response = self.client.post("/file_uploads/quota/", {'f': open(bigfile.name)}) got = simplejson.loads(response.content) self.assert_('f' not in got) - + def test_broken_custom_upload_handler(self): f = tempfile.NamedTemporaryFile() f.write('a' * (2 ** 21)) @@ -179,3 +186,42 @@ class FileUploadTests(TestCase): self.assertEqual(got.get('file1'), 1) self.assertEqual(got.get('file2'), 2) + +class DirectoryCreationTests(unittest.TestCase): + """ + Tests for error handling during directory creation + via _save_FIELD_file (ticket #6450) + """ + def setUp(self): + self.obj = FileModel() + if not os.path.isdir(UPLOAD_ROOT): + os.makedirs(UPLOAD_ROOT) + + def tearDown(self): + os.chmod(UPLOAD_ROOT, 0700) + shutil.rmtree(UPLOAD_ROOT) + + def test_readonly_root(self): + """Permission errors are not swallowed""" + os.chmod(UPLOAD_ROOT, 0500) + try: + self.obj.save_testfile_file('foo.txt', SimpleUploadedFile('foo.txt', 'x')) + except OSError, err: + self.assertEquals(err.errno, errno.EACCES) + except: + self.fail("OSError [Errno %s] not raised" % errno.EACCES) + + def test_not_a_directory(self): + """The correct IOError is raised when the upload directory name exists but isn't a directory""" + # Create a file with the upload directory name + fd = open(UPLOAD_TO, 'w') + fd.close() + try: + self.obj.save_testfile_file('foo.txt', SimpleUploadedFile('foo.txt', 'x')) + except IOError, err: + # The test needs to be done on a specific string as IOError + # is raised even without the patch (just not early enough) + self.assertEquals(err.args[0], + "%s exists and is not a directory" % UPLOAD_TO) + except: + self.fail("IOError not raised") diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py index c70ff2dff3..a9aae4f442 100644 --- a/tests/regressiontests/forms/fields.py +++ b/tests/regressiontests/forms/fields.py @@ -308,18 +308,18 @@ ValidationError: [u'This field is required.'] Traceback (most recent call last): ... ValidationError: [u'This field is required.'] ->>> f.clean('1') -Decimal("1") +>>> f.clean('1') == Decimal("1") +True >>> isinstance(f.clean('1'), Decimal) True ->>> f.clean('23') -Decimal("23") ->>> f.clean('3.14') -Decimal("3.14") ->>> f.clean(3.14) -Decimal("3.14") ->>> f.clean(Decimal('3.14')) -Decimal("3.14") +>>> f.clean('23') == Decimal("23") +True +>>> f.clean('3.14') == Decimal("3.14") +True +>>> f.clean(3.14) == Decimal("3.14") +True +>>> f.clean(Decimal('3.14')) == Decimal("3.14") +True >>> f.clean('a') Traceback (most recent call last): ... @@ -328,12 +328,12 @@ ValidationError: [u'Enter a number.'] Traceback (most recent call last): ... ValidationError: [u'Enter a number.'] ->>> f.clean('1.0 ') -Decimal("1.0") ->>> f.clean(' 1.0') -Decimal("1.0") ->>> f.clean(' 1.0 ') -Decimal("1.0") +>>> f.clean('1.0 ') == Decimal("1.0") +True +>>> f.clean(' 1.0') == Decimal("1.0") +True +>>> f.clean(' 1.0 ') == Decimal("1.0") +True >>> f.clean('1.0a') Traceback (most recent call last): ... @@ -350,18 +350,18 @@ ValidationError: [u'Ensure that there are no more than 2 decimal places.'] Traceback (most recent call last): ... ValidationError: [u'Ensure that there are no more than 2 digits before the decimal point.'] ->>> f.clean('-12.34') -Decimal("-12.34") +>>> f.clean('-12.34') == Decimal("-12.34") +True >>> f.clean('-123.45') Traceback (most recent call last): ... ValidationError: [u'Ensure that there are no more than 4 digits in total.'] ->>> f.clean('-.12') -Decimal("-0.12") ->>> f.clean('-00.12') -Decimal("-0.12") ->>> f.clean('-000.12') -Decimal("-0.12") +>>> f.clean('-.12') == Decimal("-0.12") +True +>>> f.clean('-00.12') == Decimal("-0.12") +True +>>> f.clean('-000.12') == Decimal("-0.12") +True >>> f.clean('-000.123') Traceback (most recent call last): ... @@ -380,8 +380,8 @@ ValidationError: [u'Enter a number.'] >>> f.clean(None) ->>> f.clean('1') -Decimal("1") +>>> f.clean('1') == Decimal("1") +True DecimalField accepts min_value and max_value just like IntegerField: >>> f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) @@ -394,14 +394,14 @@ ValidationError: [u'Ensure this value is less than or equal to 1.5.'] Traceback (most recent call last): ... ValidationError: [u'Ensure this value is greater than or equal to 0.5.'] ->>> f.clean('1.5') -Decimal("1.5") ->>> f.clean('0.5') -Decimal("0.5") ->>> f.clean('.5') -Decimal("0.5") ->>> f.clean('00.50') -Decimal("0.50") +>>> f.clean('1.5') == Decimal("1.5") +True +>>> f.clean('0.5') == Decimal("0.5") +True +>>> f.clean('.5') == Decimal("0.5") +True +>>> f.clean('00.50') == Decimal("0.50") +True # DateField ################################################################### @@ -802,6 +802,9 @@ ValidationError: [u'The submitted file is empty.'] >>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'))) <class 'django.core.files.uploadedfile.SimpleUploadedFile'> +>>> type(f.clean(SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह'))) +<class 'django.core.files.uploadedfile.SimpleUploadedFile'> + >>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf')) <class 'django.core.files.uploadedfile.SimpleUploadedFile'> @@ -817,15 +820,15 @@ Traceback (most recent call last): ... ValidationError: [u'This field is required.'] >>> f.clean('http://localhost') -u'http://localhost' +u'http://localhost/' >>> f.clean('http://example.com') -u'http://example.com' +u'http://example.com/' >>> f.clean('http://www.example.com') -u'http://www.example.com' +u'http://www.example.com/' >>> f.clean('http://www.example.com:8000/test') u'http://www.example.com:8000/test' >>> f.clean('http://200.8.9.10') -u'http://200.8.9.10' +u'http://200.8.9.10/' >>> f.clean('http://200.8.9.10:8000/test') u'http://200.8.9.10:8000/test' >>> f.clean('foo') @@ -855,9 +858,9 @@ u'' >>> f.clean(None) u'' >>> f.clean('http://example.com') -u'http://example.com' +u'http://example.com/' >>> f.clean('http://www.example.com') -u'http://www.example.com' +u'http://www.example.com/' >>> f.clean('foo') Traceback (most recent call last): ... @@ -883,7 +886,7 @@ URLField takes an optional verify_exists parameter, which is False by default. This verifies that the URL is live on the Internet and doesn't return a 404 or 500: >>> f = URLField(verify_exists=True) >>> f.clean('http://www.google.com') # This will fail if there's no Internet connection -u'http://www.google.com' +u'http://www.google.com/' >>> f.clean('http://example') Traceback (most recent call last): ... @@ -900,29 +903,38 @@ ValidationError: [u'This URL appears to be a broken link.'] >>> f.clean('') u'' >>> f.clean('http://www.google.com') # This will fail if there's no Internet connection -u'http://www.google.com' +u'http://www.google.com/' URLField also access min_length and max_length parameters, for convenience. >>> f = URLField(min_length=15, max_length=20) >>> f.clean('http://f.com') Traceback (most recent call last): ... -ValidationError: [u'Ensure this value has at least 15 characters (it has 12).'] +ValidationError: [u'Ensure this value has at least 15 characters (it has 13).'] >>> f.clean('http://example.com') -u'http://example.com' +u'http://example.com/' >>> f.clean('http://abcdefghijklmnopqrstuvwxyz.com') Traceback (most recent call last): ... -ValidationError: [u'Ensure this value has at most 20 characters (it has 37).'] +ValidationError: [u'Ensure this value has at most 20 characters (it has 38).'] URLField should prepend 'http://' if no scheme was given >>> f = URLField(required=False) >>> f.clean('example.com') -u'http://example.com' +u'http://example.com/' >>> f.clean('') u'' >>> f.clean('https://example.com') -u'https://example.com' +u'https://example.com/' + +URLField should append '/' if no path was given +>>> f = URLField() +>>> f.clean('http://example.com') +u'http://example.com/' + +URLField shouldn't change the path if it was given +>>> f.clean('http://example.com/test') +u'http://example.com/test' # BooleanField ################################################################ diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index 6e6e4f79bf..d834bdaccc 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1480,6 +1480,10 @@ not request.POST. >>> f.is_valid() True +>>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) +>>> print f +<tr><th>File1:</th><td><input type="file" name="file1" /></td></tr> + # Basic form processing in a view ############################################# >>> from django.template import Template, Context diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py index bbbd4cee5a..4fd0c17032 100644 --- a/tests/regressiontests/forms/formsets.py +++ b/tests/regressiontests/forms/formsets.py @@ -20,7 +20,7 @@ but we'll look at how to do so later. >>> formset = ChoiceFormSet(auto_id=False, prefix='choices') >>> print formset -<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_FORMS" value="0" /> +<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /> <tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> <tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr> @@ -34,7 +34,6 @@ the TOTAL_FORMS field appropriately. >>> data = { ... 'choices-TOTAL_FORMS': '1', # the number of forms rendered ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... } @@ -61,7 +60,6 @@ any of the forms. >>> data = { ... 'choices-TOTAL_FORMS': '1', # the number of forms rendered ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '', ... } @@ -92,7 +90,6 @@ Let's simulate what would happen if we submitted this form. >>> data = { ... 'choices-TOTAL_FORMS': '2', # the number of forms rendered ... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-1-choice': '', @@ -114,7 +111,6 @@ handle that later. >>> data = { ... 'choices-TOTAL_FORMS': '2', # the number of forms rendered ... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-1-choice': 'The Decemberists', @@ -134,7 +130,6 @@ handle that case later. >>> data = { ... 'choices-TOTAL_FORMS': '2', # the number of forms rendered ... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': '', # deleted value ... 'choices-0-votes': '', # deleted value ... 'choices-1-choice': '', @@ -172,7 +167,6 @@ number of forms to be completed. >>> data = { ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': '', ... 'choices-0-votes': '', ... 'choices-1-choice': '', @@ -193,7 +187,6 @@ We can just fill out one of the forms. >>> data = { ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-1-choice': '', @@ -214,7 +207,6 @@ And once again, if we try to partially complete a form, validation will fail. >>> data = { ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered ... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-1-choice': 'The Decemberists', @@ -275,7 +267,6 @@ To delete something, we just need to set that form's special delete field to >>> data = { ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered ... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-0-DELETE': '', @@ -325,7 +316,6 @@ something at the front of the list, you'd need to set it's order to 0. >>> data = { ... 'choices-TOTAL_FORMS': '3', # the number of forms rendered ... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-0-ORDER': '1', @@ -352,7 +342,6 @@ they will be sorted below everything else. >>> data = { ... 'choices-TOTAL_FORMS': '4', # the number of forms rendered ... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-0-ORDER': '1', @@ -414,7 +403,6 @@ Let's delete Fergie, and put The Decemberists ahead of Calexico. >>> data = { ... 'choices-TOTAL_FORMS': '4', # the number of forms rendered ... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data -... 'choices-MAX_FORMS': '0', # the max number of forms ... 'choices-0-choice': 'Calexico', ... 'choices-0-votes': '100', ... 'choices-0-ORDER': '1', @@ -473,7 +461,6 @@ We start out with a some duplicate data. >>> data = { ... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered ... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data -... 'drinks-MAX_FORMS': '0', # the max number of forms ... 'drinks-0-name': 'Gin and Tonic', ... 'drinks-1-name': 'Gin and Tonic', ... } @@ -495,7 +482,6 @@ Make sure we didn't break the valid case. >>> data = { ... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered ... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data -... 'drinks-MAX_FORMS': '0', # the max number of forms ... 'drinks-0-name': 'Gin and Tonic', ... 'drinks-1-name': 'Bloody Mary', ... } diff --git a/tests/regressiontests/forms/localflavor/at.py b/tests/regressiontests/forms/localflavor/at.py new file mode 100644 index 0000000000..54ca46898e --- /dev/null +++ b/tests/regressiontests/forms/localflavor/at.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Tests for the contrib/localflavor/ AT form fields. + +tests = r""" +# ATZipCodeField ########################################################### + +>>> from django.contrib.localflavor.at.forms import ATZipCodeField +>>> f = ATZipCodeField() +>>> f.clean('1150') +u'1150' +>>> f.clean('4020') +u'4020' +>>> f.clean('8020') +u'8020' +>>> f.clean('111222') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXX.'] +>>> f.clean('eeffee') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXX.'] +>>> f.clean(u'') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + + +>>> f = ATZipCodeField(required=False) +>>> f.clean('1150') +u'1150' +>>> f.clean('4020') +u'4020' +>>> f.clean('8020') +u'8020' +>>> f.clean('111222') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXX.'] +>>> f.clean('eeffee') +Traceback (most recent call last): +... +ValidationError: [u'Enter a zip code in the format XXXX.'] +>>> f.clean(None) +u'' +>>> f.clean('') +u'' +>>> f.clean(u'') +u'' + +# ATStateSelect ################################################################## + +>>> from django.contrib.localflavor.at.forms import ATStateSelect +>>> f = ATStateSelect() +>>> f.render('bundesland', 'WI') +u'<select name="bundesland">\n<option value="BL">Burgenland</option>\n<option value="KA">Carinthia</option>\n<option value="NO">Lower Austria</option>\n<option value="OO">Upper Austria</option>\n<option value="SA">Salzburg</option>\n<option value="ST">Styria</option>\n<option value="TI">Tyrol</option>\n<option value="VO">Vorarlberg</option>\n<option value="WI" selected="selected">Vienna</option>\n</select>' + +# ATSocialSecurityNumberField ################################################ + +>>> from django.contrib.localflavor.at.forms import ATSocialSecurityNumberField +>>> f = ATSocialSecurityNumberField() +>>> f.clean('1237 010180') +u'1237 010180' +>>> f.clean('1237 010181') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Austrian Social Security Number in XXXX XXXXXX format.'] +>>> f.clean('12370 010180') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid Austrian Social Security Number in XXXX XXXXXX format.'] +""" diff --git a/tests/regressiontests/forms/localflavor/ro.py b/tests/regressiontests/forms/localflavor/ro.py new file mode 100644 index 0000000000..e885030029 --- /dev/null +++ b/tests/regressiontests/forms/localflavor/ro.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# Tests for the contrib/localflavor/ RO form fields. + +tests = r""" +>>> from django.contrib.localflavor.ro.forms import * + +##ROCIFField ################################################################ + +f = ROCIFField() +f.clean('21694681') +u'21694681' +f.clean('RO21694681') +u'21694681' +f.clean('21694680') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CIF'] +f.clean('21694680000') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 10 characters (it has 11).'] +f.clean('0') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 2 characters (it has 1).'] +f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROCNPField ################################################################# + +f = ROCNPField() +f.clean('1981211204489') +u'1981211204489' +f.clean('1981211204487') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CNP'] +f.clean('1981232204489') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CNP'] +f.clean('9981211204489') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid CNP'] +f.clean('9981211209') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 13 characters (it has 10).'] +f.clean('19812112044891') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 13 characters (it has 14).'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROCountyField ############################################################## + +f = ROCountyField() +f.clean('CJ') +'CJ' +f.clean('cj') +'CJ' +f.clean('Argeş') +'AG' +f.clean('argeş') +'AG' +f.clean('Arges') +Traceback (most recent call last): +... +ValidationError: [u'Enter a Romanian county code or name.'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROCountySelect ############################################################# + +f = ROCountySelect() +f.render('county','CJ') +u'<select name="county">\n<option value="AB">Alba</option>\n<option value="AR">A +rad</option>\n<option value="AG">Arge\u015f</option>\n<option value="BC">Bac\u01 +03u</option>\n<option value="BH">Bihor</option>\n<option value="BN">Bistri\u0163 +a-N\u0103s\u0103ud</option>\n<option value="BT">Boto\u015fani</option>\n<option +value="BV">Bra\u015fov</option>\n<option value="BR">Br\u0103ila</option>\n<optio +n value="B">Bucure\u015fti</option>\n<option value="BZ">Buz\u0103u</option>\n<op +tion value="CS">Cara\u015f-Severin</option>\n<option value="CL">C\u0103l\u0103ra +\u015fi</option>\n<option value="CJ" selected="selected">Cluj</option>\n<option +value="CT">Constan\u0163a</option>\n<option value="CV">Covasna</option>\n<option + value="DB">D\xe2mbovi\u0163a</option>\n<option value="DJ">Dolj</option>\n<optio +n value="GL">Gala\u0163i</option>\n<option value="GR">Giurgiu</option>\n<option +value="GJ">Gorj</option>\n<option value="HR">Harghita</option>\n<option value="H +D">Hunedoara</option>\n<option value="IL">Ialomi\u0163a</option>\n<option value= +"IS">Ia\u015fi</option>\n<option value="IF">Ilfov</option>\n<option value="MM">M +aramure\u015f</option>\n<option value="MH">Mehedin\u0163i</option>\n<option valu +e="MS">Mure\u015f</option>\n<option value="NT">Neam\u0163</option>\n<option valu +e="OT">Olt</option>\n<option value="PH">Prahova</option>\n<option value="SM">Sat +u Mare</option>\n<option value="SJ">S\u0103laj</option>\n<option value="SB">Sibi +u</option>\n<option value="SV">Suceava</option>\n<option value="TR">Teleorman</o +ption>\n<option value="TM">Timi\u015f</option>\n<option value="TL">Tulcea</optio +n>\n<option value="VS">Vaslui</option>\n<option value="VL">V\xe2lcea</option>\n< +option value="VN">Vrancea</option>\n</select>' + +##ROIBANField ################################################################# + +f = ROIBANField() +f.clean('RO56RZBR0000060003291177') +u'RO56RZBR0000060003291177' +f.clean('RO56RZBR0000060003291176') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format'] + +f.clean('RO56-RZBR-0000-0600-0329-1177') +u'RO56RZBR0000060003291177' +f.clean('AT61 1904 3002 3457 3201') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format'] + +f.clean('RO56RZBR000006000329117') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 24 characters (it has 23).'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROPhoneNumberField ########################################################## + +f = ROPhoneNumberField() +f.clean('0264485936') +u'0264485936' +f.clean('(0264)-485936') +u'0264485936' +f.clean('02644859368') +Traceback (most recent call last): +... +ValidationError: [u'Phone numbers must be in XXXX-XXXXXX format.'] +f.clean('026448593') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 10 characters (it has 9).'] +f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] + +##ROPostalCodeField ########################################################### + +f = ROPostalCodeField() +f.clean('400473') +u'400473' +f.clean('40047') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at least 6 characters (it has 5).'] +f.clean('4004731') +Traceback (most recent call last): +... +ValidationError: [u'Ensure this value has at most 6 characters (it has 7).'] +f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +""" diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index 98b9233d80..a6baa79811 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -1,6 +1,10 @@ +# -*- coding: utf-8 -*- import datetime from django.db import models +# Can't import as "forms" due to implementation details in the test suite (the +# current file is called "forms" and is already imported). +from django import forms as django_forms class BoundaryModel(models.Model): positive_integer = models.PositiveIntegerField(null=True, blank=True) @@ -14,8 +18,23 @@ class ChoiceModel(models.Model): """For ModelChoiceField and ModelMultipleChoiceField tests.""" name = models.CharField(max_length=10) +class FileModel(models.Model): + file = models.FileField(upload_to='/') + +class FileForm(django_forms.Form): + file1 = django_forms.FileField() + __test__ = {'API_TESTS': """ >>> from django.forms import form_for_model, form_for_instance +>>> from django.core.files.uploadedfile import SimpleUploadedFile + +# FileModel with unicode filename and data ######################### +>>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) +>>> f.is_valid() +True +>>> f.cleaned_data +{'file1': <SimpleUploadedFile: 我隻氣墊船裝滿晒鱔.txt (text/plain)>} +>>> m = FileModel.objects.create(file=f.cleaned_data['file1']) # Boundary conditions on a PostitiveIntegerField ######################### >>> BoundaryForm = form_for_model(BoundaryModel) diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index ff8213c8d9..6a8b017f44 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -4,6 +4,7 @@ from fields import tests as fields_tests from forms import tests as form_tests from error_messages import tests as custom_error_message_tests from localflavor.ar import tests as localflavor_ar_tests +from localflavor.at import tests as localflavor_at_tests from localflavor.au import tests as localflavor_au_tests from localflavor.br import tests as localflavor_br_tests from localflavor.ca import tests as localflavor_ca_tests @@ -19,6 +20,7 @@ from localflavor.it import tests as localflavor_it_tests from localflavor.jp import tests as localflavor_jp_tests from localflavor.nl import tests as localflavor_nl_tests from localflavor.pl import tests as localflavor_pl_tests +from localflavor.ro import tests as localflavor_ro_tests from localflavor.sk import tests as localflavor_sk_tests from localflavor.uk import tests as localflavor_uk_tests from localflavor.us import tests as localflavor_us_tests @@ -35,6 +37,7 @@ __test__ = { 'form_tests': form_tests, 'custom_error_message_tests': custom_error_message_tests, 'localflavor_ar_tests': localflavor_ar_tests, + 'localflavor_at_tests': localflavor_at_tests, 'localflavor_au_tests': localflavor_au_tests, 'localflavor_br_tests': localflavor_br_tests, 'localflavor_ca_tests': localflavor_ca_tests, @@ -50,6 +53,7 @@ __test__ = { 'localflavor_jp_tests': localflavor_jp_tests, 'localflavor_nl_tests': localflavor_nl_tests, 'localflavor_pl_tests': localflavor_pl_tests, + 'localflavor_ro_tests': localflavor_ro_tests, 'localflavor_sk_tests': localflavor_sk_tests, 'localflavor_uk_tests': localflavor_uk_tests, 'localflavor_us_tests': localflavor_us_tests, diff --git a/tests/regressiontests/m2m_through_regress/__init__.py b/tests/regressiontests/m2m_through_regress/__init__.py new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/tests/regressiontests/m2m_through_regress/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py new file mode 100644 index 0000000000..16d2c4ec1b --- /dev/null +++ b/tests/regressiontests/m2m_through_regress/models.py @@ -0,0 +1,204 @@ +from django.db import models +from datetime import datetime +from django.contrib.auth.models import User + +# Forward declared intermediate model +class Membership(models.Model): + person = models.ForeignKey('Person') + group = models.ForeignKey('Group') + date_joined = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + +class UserMembership(models.Model): + user = models.ForeignKey(User) + group = models.ForeignKey('Group') + date_joined = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return "%s is a user and member of %s" % (self.user.username, self.group.name) + +class Person(models.Model): + name = models.CharField(max_length=128) + + def __unicode__(self): + return self.name + +class Group(models.Model): + name = models.CharField(max_length=128) + # Membership object defined as a class + members = models.ManyToManyField(Person, through=Membership) + user_members = models.ManyToManyField(User, through='UserMembership') + + def __unicode__(self): + return self.name + +__test__ = {'API_TESTS':""" +# Create some dummy data +>>> bob = Person.objects.create(name='Bob') +>>> jim = Person.objects.create(name='Jim') + +>>> rock = Group.objects.create(name='Rock') +>>> roll = Group.objects.create(name='Roll') + +>>> frank = User.objects.create_user('frank','frank@example.com','password') +>>> jane = User.objects.create_user('jane','jane@example.com','password') + +# Now test that the forward declared Membership works +>>> Membership.objects.create(person=bob, group=rock) +<Membership: Bob is a member of Rock> + +>>> Membership.objects.create(person=bob, group=roll) +<Membership: Bob is a member of Roll> + +>>> Membership.objects.create(person=jim, group=rock) +<Membership: Jim is a member of Rock> + +>>> bob.group_set.all() +[<Group: Rock>, <Group: Roll>] + +>>> roll.members.all() +[<Person: Bob>] + +# Error messages use the model name, not repr of the class name +>>> bob.group_set = [] +Traceback (most recent call last): +... +AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +>>> roll.members = [] +Traceback (most recent call last): +... +AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +>>> rock.members.create(name='Anne') +Traceback (most recent call last): +... +AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +>>> bob.group_set.create(name='Funk') +Traceback (most recent call last): +... +AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +# Now test that the intermediate with a relationship outside +# the current app (i.e., UserMembership) workds +>>> UserMembership.objects.create(user=frank, group=rock) +<UserMembership: frank is a user and member of Rock> + +>>> UserMembership.objects.create(user=frank, group=roll) +<UserMembership: frank is a user and member of Roll> + +>>> UserMembership.objects.create(user=jane, group=rock) +<UserMembership: jane is a user and member of Rock> + +>>> frank.group_set.all() +[<Group: Rock>, <Group: Roll>] + +>>> roll.user_members.all() +[<User: frank>] + +"""} +from django.db import models +from datetime import datetime +from django.contrib.auth.models import User + +# Forward declared intermediate model +class Membership(models.Model): + person = models.ForeignKey('Person') + group = models.ForeignKey('Group') + date_joined = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return "%s is a member of %s" % (self.person.name, self.group.name) + +class UserMembership(models.Model): + user = models.ForeignKey(User) + group = models.ForeignKey('Group') + date_joined = models.DateTimeField(default=datetime.now) + + def __unicode__(self): + return "%s is a user and member of %s" % (self.user.username, self.group.name) + +class Person(models.Model): + name = models.CharField(max_length=128) + + def __unicode__(self): + return self.name + +class Group(models.Model): + name = models.CharField(max_length=128) + # Membership object defined as a class + members = models.ManyToManyField(Person, through=Membership) + user_members = models.ManyToManyField(User, through='UserMembership') + + def __unicode__(self): + return self.name + +__test__ = {'API_TESTS':""" +# Create some dummy data +>>> bob = Person.objects.create(name='Bob') +>>> jim = Person.objects.create(name='Jim') + +>>> rock = Group.objects.create(name='Rock') +>>> roll = Group.objects.create(name='Roll') + +>>> frank = User.objects.create_user('frank','frank@example.com','password') +>>> jane = User.objects.create_user('jane','jane@example.com','password') + +# Now test that the forward declared Membership works +>>> Membership.objects.create(person=bob, group=rock) +<Membership: Bob is a member of Rock> + +>>> Membership.objects.create(person=bob, group=roll) +<Membership: Bob is a member of Roll> + +>>> Membership.objects.create(person=jim, group=rock) +<Membership: Jim is a member of Rock> + +>>> bob.group_set.all() +[<Group: Rock>, <Group: Roll>] + +>>> roll.members.all() +[<Person: Bob>] + +# Error messages use the model name, not repr of the class name +>>> bob.group_set = [] +Traceback (most recent call last): +... +AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +>>> roll.members = [] +Traceback (most recent call last): +... +AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +>>> rock.members.create(name='Anne') +Traceback (most recent call last): +... +AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +>>> bob.group_set.create(name='Funk') +Traceback (most recent call last): +... +AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead. + +# Now test that the intermediate with a relationship outside +# the current app (i.e., UserMembership) workds +>>> UserMembership.objects.create(user=frank, group=rock) +<UserMembership: frank is a user and member of Rock> + +>>> UserMembership.objects.create(user=frank, group=roll) +<UserMembership: frank is a user and member of Roll> + +>>> UserMembership.objects.create(user=jane, group=rock) +<UserMembership: jane is a user and member of Rock> + +>>> frank.group_set.all() +[<Group: Rock>, <Group: Roll>] + +>>> roll.user_members.all() +[<User: frank>] + +"""}
\ No newline at end of file diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 9d2e2abe96..be59234342 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -3,6 +3,7 @@ r""" # Tests for the django.core.mail. >>> from django.core.mail import EmailMessage +>>> from django.utils.translation import ugettext_lazy # Test normal ascii character case: @@ -36,6 +37,12 @@ r""" >>> message = email.message() Traceback (most recent call last): ... -BadHeaderError: Header values can't contain newlines (got 'Subject\nInjection Test' for header 'Subject') +BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection Test' for header 'Subject') + +>>> email = EmailMessage(ugettext_lazy('Subject\nInjection Test'), 'Content', 'from@example.com', ['to@example.com']) +>>> message = email.message() +Traceback (most recent call last): + ... +BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection Test' for header 'Subject') """ diff --git a/tests/regressiontests/many_to_one_regress/models.py b/tests/regressiontests/many_to_one_regress/models.py index 429bdd7558..b87b36cb3b 100644 --- a/tests/regressiontests/many_to_one_regress/models.py +++ b/tests/regressiontests/many_to_one_regress/models.py @@ -55,31 +55,46 @@ __test__ = {'API_TESTS':""" <Child: Child object> # -# Tests of ForeignKey assignment and the related-object cache (see #6886) +# Tests of ForeignKey assignment and the related-object cache (see #6886). # >>> p = Parent.objects.create(name="Parent") >>> c = Child.objects.create(name="Child", parent=p) -# Look up the object again so that we get a "fresh" object +# Look up the object again so that we get a "fresh" object. >>> c = Child.objects.get(name="Child") >>> p = c.parent -# Accessing the related object again returns the exactly same object +# Accessing the related object again returns the exactly same object. >>> c.parent is p True -# But if we kill the cache, we get a new object +# But if we kill the cache, we get a new object. >>> del c._parent_cache >>> c.parent is p False -# Assigning a new object results in that object getting cached immediately +# Assigning a new object results in that object getting cached immediately. >>> p2 = Parent.objects.create(name="Parent 2") >>> c.parent = p2 >>> c.parent is p2 True -# Assigning None fails: Child.parent is null=False +# Assigning None succeeds if field is null=True. +>>> p.bestchild = None +>>> p.bestchild is None +True + +# bestchild should still be None after saving. +>>> p.save() +>>> p.bestchild is None +True + +# bestchild should still be None after fetching the object again. +>>> p = Parent.objects.get(name="Parent") +>>> p.bestchild is None +True + +# Assigning None fails: Child.parent is null=False. >>> c.parent = None Traceback (most recent call last): ... @@ -91,8 +106,31 @@ Traceback (most recent call last): ... ValueError: Cannot assign "<First: First object>": "Child.parent" must be a "Parent" instance. -# Test of multiple ForeignKeys to the same model (bug #7125) +# Creation using keyword argument should cache the related object. +>>> p = Parent.objects.get(name="Parent") +>>> c = Child(parent=p) +>>> c.parent is p +True + +# Creation using keyword argument and unsaved related instance (#8070). +>>> p = Parent() +>>> c = Child(parent=p) +>>> c.parent is p +True +# Creation using attname keyword argument and an id will cause the related +# object to be fetched. +>>> p = Parent.objects.get(name="Parent") +>>> c = Child(parent_id=p.id) +>>> c.parent is p +False +>>> c.parent == p +True + + +# +# Test of multiple ForeignKeys to the same model (bug #7125). +# >>> c1 = Category.objects.create(name='First') >>> c2 = Category.objects.create(name='Second') >>> c3 = Category.objects.create(name='Third') diff --git a/tests/regressiontests/max_lengths/tests.py b/tests/regressiontests/max_lengths/tests.py index 0ef407f573..fb62ce634a 100644 --- a/tests/regressiontests/max_lengths/tests.py +++ b/tests/regressiontests/max_lengths/tests.py @@ -13,7 +13,7 @@ class MaxLengthArgumentsTests(TestCase): self.verify_max_length(PersonWithDefaultMaxLengths, 'homepage', 200) self.verify_max_length(PersonWithDefaultMaxLengths, 'avatar', 100) - def test_custom_maxlengths(self): + def test_custom_max_lengths(self): self.verify_max_length(PersonWithCustomMaxLengths, 'email', 384) self.verify_max_length(PersonWithCustomMaxLengths, 'vcard', 1024) self.verify_max_length(PersonWithCustomMaxLengths, 'homepage', 256) diff --git a/tests/regressiontests/maxlength/tests.py b/tests/regressiontests/maxlength/tests.py deleted file mode 100644 index c7ed1f91c0..0000000000 --- a/tests/regressiontests/maxlength/tests.py +++ /dev/null @@ -1,160 +0,0 @@ -# Test access to max_length while still providing full backwards compatibility -# with legacy maxlength attribute. -""" - -Don't print out the deprecation warnings during testing. ->>> from warnings import filterwarnings ->>> filterwarnings("ignore") - -# legacy_maxlength function - ->>> from django.utils.maxlength import legacy_maxlength - ->>> legacy_maxlength(None, None) - - ->>> legacy_maxlength(10, None) -10 - ->>> legacy_maxlength(None, 10) -10 - ->>> legacy_maxlength(10, 12) -Traceback (most recent call last): -... -TypeError: Field cannot take both the max_length argument and the legacy maxlength argument. - ->>> legacy_maxlength(0, 10) -Traceback (most recent call last): -... -TypeError: Field cannot take both the max_length argument and the legacy maxlength argument. - ->>> legacy_maxlength(0, None) -0 - ->>> legacy_maxlength(None, 0) -0 - -#=============================================================================== -# Fields -#=============================================================================== - -# Set up fields ->>> from django.db.models import fields ->>> new = fields.Field(max_length=15) ->>> old = fields.Field(maxlength=10) - -# Ensure both max_length and legacy maxlength are not able to both be specified ->>> fields.Field(maxlength=10, max_length=15) -Traceback (most recent call last): - ... -TypeError: Field cannot take both the max_length argument and the legacy maxlength argument. - -# Test max_length ->>> new.max_length -15 ->>> old.max_length -10 - -# Test accessing maxlength ->>> new.maxlength -15 ->>> old.maxlength -10 - -# Test setting maxlength ->>> new.maxlength += 1 ->>> old.maxlength += 1 ->>> new.max_length -16 ->>> old.max_length -11 - -# SlugField __init__ passes through max_length so test that too ->>> fields.SlugField('new', max_length=15).max_length -15 ->>> fields.SlugField('empty').max_length -50 ->>> fields.SlugField('old', maxlength=10).max_length -10 - -#=============================================================================== -# (old)forms -#=============================================================================== - ->>> from django import oldforms - -# Test max_length attribute - ->>> oldforms.TextField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vTextField" name="new" size="30" value="" maxlength="15" />' - ->>> oldforms.IntegerField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vIntegerField" name="new" size="10" value="" maxlength="15" />' - ->>> oldforms.SmallIntegerField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vSmallIntegerField" name="new" size="5" value="" maxlength="15" />' - ->>> oldforms.PositiveIntegerField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vPositiveIntegerField" name="new" size="10" value="" maxlength="15" />' - ->>> oldforms.PositiveSmallIntegerField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vPositiveSmallIntegerField" name="new" size="5" value="" maxlength="15" />' - ->>> oldforms.DatetimeField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vDatetimeField" name="new" size="30" value="" maxlength="15" />' - ->>> oldforms.EmailField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vEmailField" name="new" size="50" value="" maxlength="15" />' ->>> oldforms.EmailField('new').render('') -u'<input type="text" id="id_new" class="vEmailField" name="new" size="50" value="" maxlength="75" />' - ->>> oldforms.URLField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vURLField" name="new" size="50" value="" maxlength="15" />' ->>> oldforms.URLField('new').render('') -u'<input type="text" id="id_new" class="vURLField" name="new" size="50" value="" maxlength="200" />' - ->>> oldforms.IPAddressField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vIPAddressField" name="new" size="15" value="" maxlength="15" />' ->>> oldforms.IPAddressField('new').render('') -u'<input type="text" id="id_new" class="vIPAddressField" name="new" size="15" value="" maxlength="15" />' - ->>> oldforms.CommaSeparatedIntegerField('new', max_length=15).render('') -u'<input type="text" id="id_new" class="vCommaSeparatedIntegerField" name="new" size="20" value="" maxlength="15" />' - - -# Test legacy maxlength attribute - ->>> oldforms.TextField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vTextField" name="old" size="30" value="" maxlength="10" />' - ->>> oldforms.IntegerField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vIntegerField" name="old" size="10" value="" maxlength="10" />' - ->>> oldforms.SmallIntegerField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vSmallIntegerField" name="old" size="5" value="" maxlength="10" />' - ->>> oldforms.PositiveIntegerField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vPositiveIntegerField" name="old" size="10" value="" maxlength="10" />' - ->>> oldforms.PositiveSmallIntegerField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vPositiveSmallIntegerField" name="old" size="5" value="" maxlength="10" />' - ->>> oldforms.DatetimeField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vDatetimeField" name="old" size="30" value="" maxlength="10" />' - ->>> oldforms.EmailField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vEmailField" name="old" size="50" value="" maxlength="10" />' - ->>> oldforms.URLField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vURLField" name="old" size="50" value="" maxlength="10" />' - ->>> oldforms.IPAddressField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vIPAddressField" name="old" size="15" value="" maxlength="10" />' - ->>> oldforms.CommaSeparatedIntegerField('old', maxlength=10).render('') -u'<input type="text" id="id_old" class="vCommaSeparatedIntegerField" name="old" size="20" value="" maxlength="10" />' -""" -if __name__ == "__main__": - import doctest - doctest.testmod() diff --git a/tests/regressiontests/model_fields/models.py b/tests/regressiontests/model_fields/models.py index 7e07227961..455e2b3ded 100644 --- a/tests/regressiontests/model_fields/models.py +++ b/tests/regressiontests/model_fields/models.py @@ -1,8 +1,14 @@ from django.db import models +try: + import decimal +except ImportError: + from django.utils import _decimal as decimal # Python 2.3 fallback + class Foo(models.Model): a = models.CharField(max_length=10) + d = models.DecimalField(max_digits=5, decimal_places=3) def get_foo(): return Foo.objects.get(id=1) @@ -11,9 +17,25 @@ class Bar(models.Model): b = models.CharField(max_length=10) a = models.ForeignKey(Foo, default=get_foo) +class Whiz(models.Model): + CHOICES = ( + ('Group 1', ( + (1,'First'), + (2,'Second'), + ) + ), + ('Group 2', ( + (3,'Third'), + (4,'Fourth'), + ) + ), + (0,'Other'), + ) + c = models.IntegerField(choices=CHOICES, null=True) + __test__ = {'API_TESTS':""" # Create a couple of Places. ->>> f = Foo.objects.create(a='abc') +>>> f = Foo.objects.create(a='abc', d=decimal.Decimal("12.34")) >>> f.id 1 >>> b = Bar(b = "bcd") @@ -21,4 +43,40 @@ __test__ = {'API_TESTS':""" <Foo: Foo object> >>> b.save() +# Regression tests for #7913 +# Check that get_choices and get_flatchoices interact with +# get_FIELD_display to return the expected values. + +# Test a nested value +>>> w = Whiz(c=1) +>>> w.save() +>>> w.get_c_display() +u'First' + +# Test a top level value +>>> w.c = 0 +>>> w.get_c_display() +u'Other' + +# Test an invalid data value +>>> w.c = 9 +>>> w.get_c_display() +9 + +# Test a blank data value +>>> w.c = None +>>> print w.get_c_display() +None + +# Test an empty data value +>>> w.c = '' +>>> w.get_c_display() +u'' + +# Regression test for #8023: should be able to filter decimal fields using +# strings (which is what gets passed through from, e.g., the admin interface). +>>> Foo.objects.filter(d=u'1.23') +[] + + """} diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py index c2ba9ee008..5aedcd15fc 100644 --- a/tests/regressiontests/model_fields/tests.py +++ b/tests/regressiontests/model_fields/tests.py @@ -1,15 +1,19 @@ """ >>> from django.db.models.fields import * +>>> try: +... from decimal import Decimal +... except ImportError: +... from django.utils._decimal import Decimal # DecimalField ->>> f = DecimalField() +>>> f = DecimalField(max_digits=4, decimal_places=2) ->>> f.to_python(3) -Decimal("3") +>>> f.to_python(3) == Decimal("3") +True ->>> f.to_python("3.14") -Decimal("3.14") +>>> f.to_python("3.14") == Decimal("3.14") +True >>> f.to_python("abc") Traceback (most recent call last): @@ -20,16 +24,26 @@ ValidationError: [u'This value must be a decimal number.'] >>> x = f.to_python(2) >>> y = f.to_python('2.6') ->>> f.get_db_prep_save(x) +>>> f._format(x) u'2.0' ->>> f.get_db_prep_save(y) +>>> f._format(y) u'2.6' ->>> f.get_db_prep_save(None) ->>> f.get_db_prep_lookup('exact', x) -[u'2.0'] ->>> f.get_db_prep_lookup('exact', y) -[u'2.6'] +>>> f._format(None) >>> f.get_db_prep_lookup('exact', None) [None] +# DateTimeField and TimeField to_python should support usecs: +>>> f = DateTimeField() +>>> f.to_python('2001-01-02 03:04:05.000006') +datetime.datetime(2001, 1, 2, 3, 4, 5, 6) +>>> f.to_python('2001-01-02 03:04:05.999999') +datetime.datetime(2001, 1, 2, 3, 4, 5, 999999) + +>>> f = TimeField() +>>> f.to_python('01:02:03.000004') +datetime.time(1, 2, 3, 4) +>>> f.to_python('01:02:03.999999') +datetime.time(1, 2, 3, 999999) + + """ diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py index b78b493e15..716c7f5ad8 100644 --- a/tests/regressiontests/model_inheritance_regress/models.py +++ b/tests/regressiontests/model_inheritance_regress/models.py @@ -43,12 +43,22 @@ class ParkingLot(Place): def __unicode__(self): return u"%s the parking lot" % self.name +class Supplier(models.Model): + restaurant = models.ForeignKey(Restaurant) + class Parent(models.Model): created = models.DateTimeField(default=datetime.datetime.now) class Child(Parent): name = models.CharField(max_length=10) +class SelfRefParent(models.Model): + parent_data = models.IntegerField() + self_data = models.ForeignKey('self', null=True) + +class SelfRefChild(SelfRefParent): + child_data = models.IntegerField() + __test__ = {'API_TESTS':""" # Regression for #7350, #7202 # Check that when you create a Parent object with a specific reference to an @@ -159,4 +169,29 @@ Traceback (most recent call last): ... DoesNotExist: ItalianRestaurant matching query does not exist. +# Regression test for #6755 +>>> r = Restaurant(serves_pizza=False) +>>> r.save() +>>> r.id == r.place_ptr_id +True +>>> orig_id = r.id +>>> r = Restaurant(place_ptr_id=orig_id, serves_pizza=True) +>>> r.save() +>>> r.id == orig_id +True +>>> r.id == r.place_ptr_id +True + +# Regression test for #7488. This looks a little crazy, but it's the equivalent +# of what the admin interface has to do for the edit-inline case. +>>> Supplier.objects.filter(restaurant=Restaurant(name='xx', address='yy')) +[] + +# Regression test for #7853 +# If the parent class has a self-referential link, make sure that any updates +# to that link via the child update the right table. + +>>> obj = SelfRefChild.objects.create(child_data=37, parent_data=42) +>>> obj.delete() + """} diff --git a/tests/regressiontests/model_regress/models.py b/tests/regressiontests/model_regress/models.py index 2252531564..9e11b43d2b 100644 --- a/tests/regressiontests/model_regress/models.py +++ b/tests/regressiontests/model_regress/models.py @@ -29,6 +29,9 @@ class Movie(models.Model): class Party(models.Model): when = models.DateField() +class Event(models.Model): + when = models.DateTimeField() + __test__ = {'API_TESTS': """ (NOTE: Part of the regression test here is merely parsing the model declaration. The verbose_name, in particular, did not always work.) @@ -68,5 +71,21 @@ u'' >>> [p.when for p in Party.objects.filter(when__year = 1998)] [datetime.date(1998, 12, 31)] +# Check that get_next_by_FIELD and get_previous_by_FIELD don't crash when we +# have usecs values stored on the database +# +# [It crashed after the Field.get_db_prep_* refactor, because on most backends +# DateTimeFields supports usecs, but DateTimeField.to_python didn't recognize +# them. (Note that Model._get_next_or_previous_by_FIELD coerces values to +# strings)] +# +>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 16, 0, 0)) +>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 6, 1, 1)) +>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 13, 1, 1)) +>>> e = Event.objects.create(when = datetime.datetime(2000, 1, 1, 12, 0, 20, 24)) +>>> e.get_next_by_when().when +datetime.datetime(2000, 1, 1, 13, 1, 1) +>>> e.get_previous_by_when().when +datetime.datetime(2000, 1, 1, 6, 1, 1) """ } diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py index 6a7da7d362..0fdd9afdf1 100644 --- a/tests/regressiontests/modeladmin/models.py +++ b/tests/regressiontests/modeladmin/models.py @@ -346,6 +346,20 @@ Traceback (most recent call last): ... ImproperlyConfigured: Both fieldsets and fields are specified in ValidationTestModelAdmin. +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = [(None, {'fields': ['name', 'name']})] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: There are duplicate field(s) in ValidationTestModelAdmin.fieldsets + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fields = ["name", "name"] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: There are duplicate field(s) in ValidationTestModelAdmin.fields + # form >>> class FakeForm(object): diff --git a/tests/regressiontests/null_fk/models.py b/tests/regressiontests/null_fk/models.py index 1bc266c033..529dde5039 100644 --- a/tests/regressiontests/null_fk/models.py +++ b/tests/regressiontests/null_fk/models.py @@ -1,8 +1,5 @@ """ -Regression tests for proper working of ForeignKey(null=True). Tests these bugs: - - * #7369: FK non-null after null relationship on select_related() generates an invalid query - +Regression tests for proper working of ForeignKey(null=True). """ from django.db import models @@ -38,7 +35,8 @@ __test__ = {'API_TESTS':""" # Starting from comment, make sure that a .select_related(...) with a specified # set of fields will properly LEFT JOIN multiple levels of NULLs (and the things -# that come after the NULLs, or else data that should exist won't). +# that come after the NULLs, or else data that should exist won't). Regression +# test for #7369. >>> c = Comment.objects.select_related().get(id=1) >>> c.post <Post: First Post> @@ -47,9 +45,11 @@ __test__ = {'API_TESTS':""" None >>> comments = Comment.objects.select_related('post__forum__system_info').all() ->>> [(c.id, c.post.id) for c in comments] -[(1, 1), (2, None)] ->>> [(c.comment_text, c.post.title) for c in comments] -[(u'My first comment', u'First Post'), (u'My second comment', None)] +>>> [(c.id, c.comment_text, c.post) for c in comments] +[(1, u'My first comment', <Post: First Post>), (2, u'My second comment', None)] + +# Regression test for #7530, #7716. +>>> Comment.objects.select_related('post').filter(post__isnull=True)[0].post is None +True """} diff --git a/tests/regressiontests/one_to_one_regress/models.py b/tests/regressiontests/one_to_one_regress/models.py index 99022882f2..66d00d7223 100644 --- a/tests/regressiontests/one_to_one_regress/models.py +++ b/tests/regressiontests/one_to_one_regress/models.py @@ -22,6 +22,10 @@ class Bar(models.Model): def __unicode__(self): return u"%s the bar" % self.place.name +class UndergroundBar(models.Model): + place = models.OneToOneField(Place, null=True) + serves_cocktails = models.BooleanField() + class Favorites(models.Model): name = models.CharField(max_length = 50) restaurants = models.ManyToManyField(Restaurant) @@ -42,7 +46,7 @@ __test__ = {'API_TESTS':""" >>> f.restaurants.all() [<Restaurant: Demon Dogs the restaurant>] -# Regression test for #7173: Check that the name of the cache for the +# Regression test for #7173: Check that the name of the cache for the # reverse object is correct. >>> b = Bar(place=p1, serves_cocktails=False) >>> b.save() @@ -53,7 +57,7 @@ __test__ = {'API_TESTS':""" # # Regression test for #6886 (the related-object cache) -# +# # Look up the objects again so that we get "fresh" objects >>> p = Place.objects.get(name="Demon Dogs") @@ -76,6 +80,12 @@ False >>> p.restaurant is r2 True +# Assigning None succeeds if field is null=True. +>>> ug_bar = UndergroundBar.objects.create(place=p, serves_cocktails=False) +>>> ug_bar.place = None +>>> ug_bar.place is None +True + # Assigning None fails: Place.restaurant is null=False >>> p.restaurant = None Traceback (most recent call last): @@ -88,4 +98,25 @@ Traceback (most recent call last): ... ValueError: Cannot assign "<Place: Demon Dogs the place>": "Place.restaurant" must be a "Restaurant" instance. +# Creation using keyword argument should cache the related object. +>>> p = Place.objects.get(name="Demon Dogs") +>>> r = Restaurant(place=p) +>>> r.place is p +True + +# Creation using keyword argument and unsaved related instance (#8070). +>>> p = Place() +>>> r = Restaurant(place=p) +>>> r.place is p +True + +# Creation using attname keyword argument and an id will cause the related +# object to be fetched. +>>> p = Place.objects.get(name="Demon Dogs") +>>> r = Restaurant(place_id=p.id) +>>> r.place is p +False +>>> r.place == p +True + """} diff --git a/tests/regressiontests/pagination_regress/__init__.py b/tests/regressiontests/pagination_regress/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/pagination_regress/__init__.py diff --git a/tests/regressiontests/pagination_regress/models.py b/tests/regressiontests/pagination_regress/models.py new file mode 100644 index 0000000000..cde172db68 --- /dev/null +++ b/tests/regressiontests/pagination_regress/models.py @@ -0,0 +1 @@ +# Models file for tests to run. diff --git a/tests/regressiontests/pagination_regress/tests.py b/tests/regressiontests/pagination_regress/tests.py new file mode 100644 index 0000000000..08436df355 --- /dev/null +++ b/tests/regressiontests/pagination_regress/tests.py @@ -0,0 +1,157 @@ +from unittest import TestCase + +from django.core.paginator import Paginator, EmptyPage + +class PaginatorTests(TestCase): + """ + Tests for the Paginator and Page classes. + """ + + def check_paginator(self, params, output): + """ + Helper method that instantiates a Paginator object from the passed + params and then checks that its attributes match the passed output. + """ + count, num_pages, page_range = output + paginator = Paginator(*params) + self.check_attribute('count', paginator, count, params) + self.check_attribute('num_pages', paginator, num_pages, params) + self.check_attribute('page_range', paginator, page_range, params) + + def check_attribute(self, name, paginator, expected, params): + """ + Helper method that checks a single attribute and gives a nice error + message upon test failure. + """ + got = getattr(paginator, name) + self.assertEqual(expected, got, + "For '%s', expected %s but got %s. Paginator parameters were: %s" + % (name, expected, got, params)) + + def test_paginator(self): + """ + Tests the paginator attributes using varying inputs. + """ + nine = [1, 2, 3, 4, 5, 6, 7, 8, 9] + ten = nine + [10] + eleven = ten + [11] + tests = ( + # Each item is two tuples: + # First tuple is Paginator parameters - object_list, per_page, + # orphans, and allow_empty_first_page. + # Second tuple is resulting Paginator attributes - count, + # num_pages, and page_range. + # Ten items, varying orphans, no empty first page. + ((ten, 4, 0, False), (10, 3, [1, 2, 3])), + ((ten, 4, 1, False), (10, 3, [1, 2, 3])), + ((ten, 4, 2, False), (10, 2, [1, 2])), + ((ten, 4, 5, False), (10, 2, [1, 2])), + ((ten, 4, 6, False), (10, 1, [1])), + # Ten items, varying orphans, allow empty first page. + ((ten, 4, 0, True), (10, 3, [1, 2, 3])), + ((ten, 4, 1, True), (10, 3, [1, 2, 3])), + ((ten, 4, 2, True), (10, 2, [1, 2])), + ((ten, 4, 5, True), (10, 2, [1, 2])), + ((ten, 4, 6, True), (10, 1, [1])), + # One item, varying orphans, no empty first page. + (([1], 4, 0, False), (1, 1, [1])), + (([1], 4, 1, False), (1, 1, [1])), + (([1], 4, 2, False), (1, 1, [1])), + # One item, varying orphans, allow empty first page. + (([1], 4, 0, True), (1, 1, [1])), + (([1], 4, 1, True), (1, 1, [1])), + (([1], 4, 2, True), (1, 1, [1])), + # Zero items, varying orphans, no empty first page. + (([], 4, 0, False), (0, 0, [])), + (([], 4, 1, False), (0, 0, [])), + (([], 4, 2, False), (0, 0, [])), + # Zero items, varying orphans, allow empty first page. + (([], 4, 0, True), (0, 1, [1])), + (([], 4, 1, True), (0, 1, [1])), + (([], 4, 2, True), (0, 1, [1])), + # Number if items one less than per_page. + (([], 1, 0, True), (0, 1, [1])), + (([], 1, 0, False), (0, 0, [])), + (([1], 2, 0, True), (1, 1, [1])), + ((nine, 10, 0, True), (9, 1, [1])), + # Number if items equal to per_page. + (([1], 1, 0, True), (1, 1, [1])), + (([1, 2], 2, 0, True), (2, 1, [1])), + ((ten, 10, 0, True), (10, 1, [1])), + # Number if items one more than per_page. + (([1, 2], 1, 0, True), (2, 2, [1, 2])), + (([1, 2, 3], 2, 0, True), (3, 2, [1, 2])), + ((eleven, 10, 0, True), (11, 2, [1, 2])), + # Number if items one more than per_page with one orphan. + (([1, 2], 1, 1, True), (2, 1, [1])), + (([1, 2, 3], 2, 1, True), (3, 1, [1])), + ((eleven, 10, 1, True), (11, 1, [1])), + ) + for params, output in tests: + self.check_paginator(params, output) + + def check_indexes(self, params, page_num, indexes): + """ + Helper method that instantiates a Paginator object from the passed + params and then checks that the start and end indexes of the passed + page_num match those given as a 2-tuple in indexes. + """ + paginator = Paginator(*params) + if page_num == 'first': + page_num = 1 + elif page_num == 'last': + page_num = paginator.num_pages + page = paginator.page(page_num) + start, end = indexes + msg = ("For %s of page %s, expected %s but got %s." + " Paginator parameters were: %s") + self.assertEqual(start, page.start_index(), + msg % ('start index', page_num, start, page.start_index(), params)) + self.assertEqual(end, page.end_index(), + msg % ('end index', page_num, end, page.end_index(), params)) + + def test_page_indexes(self): + """ + Tests that paginator pages have the correct start and end indexes. + """ + ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + tests = ( + # Each item is three tuples: + # First tuple is Paginator parameters - object_list, per_page, + # orphans, and allow_empty_first_page. + # Second tuple is the start and end indexes of the first page. + # Third tuple is the start and end indexes of the last page. + # Ten items, varying per_page, no orphans. + ((ten, 1, 0, True), (1, 1), (10, 10)), + ((ten, 2, 0, True), (1, 2), (9, 10)), + ((ten, 3, 0, True), (1, 3), (10, 10)), + ((ten, 5, 0, True), (1, 5), (6, 10)), + # Ten items, varying per_page, with orphans. + ((ten, 1, 1, True), (1, 1), (9, 10)), + ((ten, 1, 2, True), (1, 1), (8, 10)), + ((ten, 3, 1, True), (1, 3), (7, 10)), + ((ten, 3, 2, True), (1, 3), (7, 10)), + ((ten, 3, 4, True), (1, 3), (4, 10)), + ((ten, 5, 1, True), (1, 5), (6, 10)), + ((ten, 5, 2, True), (1, 5), (6, 10)), + ((ten, 5, 5, True), (1, 10), (1, 10)), + # One item, varying orphans, no empty first page. + (([1], 4, 0, False), (1, 1), (1, 1)), + (([1], 4, 1, False), (1, 1), (1, 1)), + (([1], 4, 2, False), (1, 1), (1, 1)), + # One item, varying orphans, allow empty first page. + (([1], 4, 0, True), (1, 1), (1, 1)), + (([1], 4, 1, True), (1, 1), (1, 1)), + (([1], 4, 2, True), (1, 1), (1, 1)), + # Zero items, varying orphans, allow empty first page. + (([], 4, 0, True), (0, 0), (0, 0)), + (([], 4, 1, True), (0, 0), (0, 0)), + (([], 4, 2, True), (0, 0), (0, 0)), + ) + for params, first, last in tests: + self.check_indexes(params, 'first', first) + self.check_indexes(params, 'last', last) + # When no items and no empty first page, we should get EmptyPage error. + self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 0, False), 1, None) + self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 1, False), 1, None) + self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 2, False), 1, None) diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 847d515422..3a4acf350a 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -190,6 +190,32 @@ class CustomPk(models.Model): class Related(models.Model): custom = models.ForeignKey(CustomPk) +# An inter-related setup with a model subclass that has a nullable +# path to another model, and a return path from that model. + +class Celebrity(models.Model): + name = models.CharField("Name", max_length=20) + greatest_fan = models.ForeignKey("Fan", null=True, unique=True) + +class TvChef(Celebrity): + pass + +class Fan(models.Model): + fan_of = models.ForeignKey(Celebrity) + +# Multiple foreign keys +class LeafA(models.Model): + data = models.CharField(max_length=10) + + def __unicode__(self): + return self.data + +class LeafB(models.Model): + data = models.CharField(max_length=10) + +class Join(models.Model): + a = models.ForeignKey(LeafA) + b = models.ForeignKey(LeafB) __test__ = {'API_TESTS':""" >>> t1 = Tag.objects.create(name='t1') @@ -321,6 +347,16 @@ constraints. >>> Number.objects.filter(Q(num__gt=7) & Q(num__lt=12) | Q(num__lt=4)) [<Number: 8>] +Bug #7872 +Another variation on the disjunctive filtering theme. + +# For the purposes of this regression test, it's important that there is no +# Join object releated to the LeafA we create. +>>> LeafA.objects.create(data='first') +<LeafA: first> +>>> LeafA.objects.filter(Q(data='first')|Q(join__b__data='second')) +[<LeafA: first>] + Bug #6074 Merging two empty result sets shouldn't leave a queryset with no constraints (which would match everything). @@ -417,9 +453,9 @@ Bug #5324, #6704 >>> query.LOUTER not in [x[2] for x in query.alias_map.values()] True -Similarly, when one of the joins cannot possibly, ever, involve NULL values (Author -> ExtraInfo, in the following), it should never be promoted to a left outer join. So hte following query should only involve one "left outer" join (Author -> Item is 0-to-many). +Similarly, when one of the joins cannot possibly, ever, involve NULL values (Author -> ExtraInfo, in the following), it should never be promoted to a left outer join. So the following query should only involve one "left outer" join (Author -> Item is 0-to-many). >>> qs = Author.objects.filter(id=a1.id).filter(Q(extra__note=n1)|Q(item__note=n3)) ->>> len([x[2] for x in qs.query.alias_map.values() if x[2] == query.LOUTER]) +>>> len([x[2] for x in qs.query.alias_map.values() if x[2] == query.LOUTER and qs.query.alias_refcount[x[1]]]) 1 The previous changes shouldn't affect nullable foreign key joins. @@ -785,6 +821,15 @@ Bug #7204, #7506 -- make sure querysets with related fields can be pickled. If this doesn't crash, it's a Good Thing. >>> out = pickle.dumps(Item.objects.all()) +We should also be able to pickle things that use select_related(). The only +tricky thing here is to ensure that we do the related selections properly after +unpickling. +>>> qs = Item.objects.select_related() +>>> query = qs.query.as_sql()[0] +>>> query2 = pickle.loads(pickle.dumps(qs.query)) +>>> query2.as_sql()[0] == query +True + Bug #7277 >>> ann1 = Annotation.objects.create(name='a1', tag=t1) >>> ann1.notes.add(n1) @@ -822,6 +867,26 @@ Bug #7759 -- count should work with a partially read result set. ... break True +Bug #7791 -- there were "issues" when ordering and distinct-ing on fields +related via ForeignKeys. +>>> len(Note.objects.order_by('extrainfo__info').distinct()) +3 + +Bug #7778 - Model subclasses could not be deleted if a nullable foreign key +relates to a model that relates back. + +>>> num_celebs = Celebrity.objects.count() +>>> tvc = TvChef.objects.create(name="Huey") +>>> Celebrity.objects.count() == num_celebs + 1 +True +>>> f1 = Fan.objects.create(fan_of=tvc) +>>> f2 = Fan.objects.create(fan_of=tvc) +>>> tvc.delete() + +# The parent object should have been deleted as well. +>>> Celebrity.objects.count() == num_celebs +True + """} # In Python 2.3, exceptions raised in __len__ are swallowed (Python issue diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index aaaef1d8b0..a091113515 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -20,8 +20,9 @@ META:{...}> ... def __init__(self, *args, **kwargs): ... super(FakeModPythonRequest, self).__init__(*args, **kwargs) ... self._get = self._post = self._meta = self._cookies = {} ->>> class Dummy: pass -... +>>> class Dummy: +... def get_options(self): +... return {} >>> req = Dummy() >>> req.uri = 'bogus' >>> print repr(FakeModPythonRequest(req)) diff --git a/tests/regressiontests/reverse_single_related/__init__.py b/tests/regressiontests/reverse_single_related/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/reverse_single_related/__init__.py diff --git a/tests/regressiontests/reverse_single_related/models.py b/tests/regressiontests/reverse_single_related/models.py new file mode 100644 index 0000000000..b2b75392aa --- /dev/null +++ b/tests/regressiontests/reverse_single_related/models.py @@ -0,0 +1,54 @@ +""" +Regression tests for an object that cannot access a single related object due +to a restrictive default manager. +""" + +from django.db import models + + +class SourceManager(models.Manager): + def get_query_set(self): + return super(SourceManager, self).get_query_set().filter(is_public=True) + +class Source(models.Model): + is_public = models.BooleanField() + objects = SourceManager() + +class Item(models.Model): + source = models.ForeignKey(Source) + + +__test__ = {'API_TESTS':""" + +>>> public_source = Source.objects.create(is_public=True) +>>> public_item = Item.objects.create(source=public_source) + +>>> private_source = Source.objects.create(is_public=False) +>>> private_item = Item.objects.create(source=private_source) + +# Only one source is available via all() due to the custom default manager. + +>>> Source.objects.all() +[<Source: Source object>] + +>>> public_item.source +<Source: Source object> + +# Make sure that an item can still access its related source even if the default +# manager doesn't normally allow it. + +>>> private_item.source +<Source: Source object> + +# If the manager is marked "use_for_related_fields", it'll get used instead +# of the "bare" queryset. Usually you'd define this as a property on the class, +# but this approximates that in a way that's easier in tests. + +>>> Source.objects.use_for_related_fields = True +>>> private_item = Item.objects.get(pk=private_item.pk) +>>> private_item.source +Traceback (most recent call last): + ... +DoesNotExist: Source matching query does not exist. + +"""} diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 9bc5eec1eb..f990d57a17 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -72,13 +72,13 @@ def inherited_create(pk, klass, data): # 1) we're testing inheritance, not field behaviour, so none # of the field values need to be protected. # 2) saving the child class and having the parent created - # automatically is easier than manually creating both. + # automatically is easier than manually creating both. models.Model.save(instance) created = [instance] for klass,field in instance._meta.parents.items(): created.append(klass.objects.get(id=pk)) return created - + # A set of functions that can be used to compare # test data objects of various kinds def data_compare(testcase, pk, klass, data): @@ -111,7 +111,7 @@ def inherited_compare(testcase, pk, klass, data): instance = klass.objects.get(id=pk) for key,value in data.items(): testcase.assertEqual(value, getattr(instance,key)) - + # Define some data types. Each data type is # actually a pair of functions; one to create # and one to compare objects of that type @@ -274,7 +274,7 @@ The end."""), (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)), (data_obj, 810, ModifyingSaveData, 42), - + (inherited_obj, 900, InheritAbstractModel, {'child_data':37,'parent_data':42}), (inherited_obj, 910, ExplicitInheritBaseModel, {'child_data':37,'parent_data':42}), (inherited_obj, 920, InheritBaseModel, {'child_data':37,'parent_data':42}), @@ -302,17 +302,19 @@ def serializerTest(format, self): objects = [] instance_count = {} transaction.enter_transaction_management() - transaction.managed(True) - for (func, pk, klass, datum) in test_data: - objects.extend(func[0](pk, klass, datum)) - instance_count[klass] = 0 - transaction.commit() - transaction.leave_transaction_management() + try: + transaction.managed(True) + for (func, pk, klass, datum) in test_data: + objects.extend(func[0](pk, klass, datum)) + instance_count[klass] = 0 + transaction.commit() + finally: + transaction.leave_transaction_management() # Get a count of the number of objects created for each class for klass in instance_count: instance_count[klass] = klass.objects.count() - + # Add the generic tagged objects to the object list objects.extend(Tag.objects.all()) @@ -322,11 +324,13 @@ def serializerTest(format, self): # Flush the database and recreate from the serialized data management.call_command('flush', verbosity=0, interactive=False) transaction.enter_transaction_management() - transaction.managed(True) - for obj in serializers.deserialize(format, serialized_data): - obj.save() - transaction.commit() - transaction.leave_transaction_management() + try: + transaction.managed(True) + for obj in serializers.deserialize(format, serialized_data): + obj.save() + transaction.commit() + finally: + transaction.leave_transaction_management() # Assert that the deserialized data is the same # as the original source diff --git a/tests/regressiontests/templates/loaders.py b/tests/regressiontests/templates/loaders.py index db37116b94..fdaf9ac83f 100644 --- a/tests/regressiontests/templates/loaders.py +++ b/tests/regressiontests/templates/loaders.py @@ -14,6 +14,7 @@ import sys import pkg_resources import imp import StringIO +import os.path from django.template import TemplateDoesNotExist from django.template.loaders.eggs import load_template_source as lts_egg @@ -57,8 +58,8 @@ class EggLoader(unittest.TestCase): self.empty_egg = create_egg("egg_empty", {}) self.egg_1 = create_egg("egg_1", { - 'templates/y.html' : StringIO.StringIO("y"), - 'templates/x.txt' : StringIO.StringIO("x"), + os.path.normcase('templates/y.html') : StringIO.StringIO("y"), + os.path.normcase('templates/x.txt') : StringIO.StringIO("x"), }) self._old_installed_apps = settings.INSTALLED_APPS settings.INSTALLED_APPS = [] diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 186b8aacb5..13b8c6b488 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -11,6 +11,7 @@ import unittest from datetime import datetime, timedelta from django import template +from django.core import urlresolvers from django.template import loader from django.template.loaders import app_directories, filesystem from django.utils.translation import activate, deactivate, ugettext as _ @@ -565,6 +566,14 @@ class Templates(unittest.TestCase): # ifchanged for the day. 'ifchanged-param04': ('{% for d in days %}{% ifchanged d.day %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d.day h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), + # Test the else clause of ifchanged. + 'ifchanged-else01': ('{% for id in ids %}{{ id }}{% ifchanged id %}-first{% else %}-other{% endifchanged %},{% endfor %}', {'ids': [1,1,2,2,2,3]}, '1-first,1-other,2-first,2-other,2-other,3-first,'), + + 'ifchanged-else02': ('{% for id in ids %}{{ id }}-{% ifchanged id %}{% cycle red,blue %}{% else %}grey{% endifchanged %},{% endfor %}', {'ids': [1,1,2,2,2,3]}, '1-red,1-grey,2-blue,2-grey,2-grey,3-red,'), + 'ifchanged-else03': ('{% for id in ids %}{{ id }}{% ifchanged id %}-{% cycle red,blue %}{% else %}{% endifchanged %},{% endfor %}', {'ids': [1,1,2,2,2,3]}, '1-red,1,2-blue,2,2,3-red,'), + + 'ifchanged-else04': ('{% for id in ids %}{% ifchanged %}***{{ id }}*{% else %}...{% endifchanged %}{{ forloop.counter }}{% endfor %}', {'ids': [1,1,2,2,2,3,4]}, '***1*1...2***2*3...4...5***3*6***4*7'), + ### IFEQUAL TAG ########################################################### 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""), 'ifequal02': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 1}, "yes"), @@ -712,7 +721,7 @@ class Templates(unittest.TestCase): # Inheritance from a template that doesn't have any blocks 'inheritance27': ("{% extends 'inheritance26' %}", {}, 'no tags'), - + ### I18N ################################################################## # {% spaceless %} tag @@ -880,8 +889,8 @@ class Templates(unittest.TestCase): # Failures 'url-fail01': ('{% url %}', {}, template.TemplateSyntaxError), - 'url-fail02': ('{% url no_such_view %}', {}, ''), - 'url-fail03': ('{% url regressiontests.templates.views.client no_such_param="value" %}', {}, ''), + 'url-fail02': ('{% url no_such_view %}', {}, urlresolvers.NoReverseMatch), + 'url-fail03': ('{% url regressiontests.templates.views.client %}', {}, urlresolvers.NoReverseMatch), ### CACHE TAG ###################################################### 'cache01': ('{% load cache %}{% cache -1 test %}cache01{% endcache %}', {}, 'cache01'), @@ -893,16 +902,16 @@ class Templates(unittest.TestCase): 'cache07': ('{% load cache %}{% cache 2 test foo %}cache07{% endcache %}', {'foo': 1}, 'cache05'), # Allow first argument to be a variable. - 'cache08': ('{% load cache %}{% cache time test foo %}cache08{% endcache %}', {'foo': 2, 'time': 2}, 'cache06'), - 'cache09': ('{% load cache %}{% cache time test foo %}cache09{% endcache %}', {'foo': 3, 'time': -1}, 'cache09'), - 'cache10': ('{% load cache %}{% cache time test foo %}cache10{% endcache %}', {'foo': 3, 'time': -1}, 'cache10'), + 'cache08': ('{% load cache %}{% cache time test foo %}cache08{% endcache %}', {'foo': 2, 'time': 2}, 'cache06'), + 'cache09': ('{% load cache %}{% cache time test foo %}cache09{% endcache %}', {'foo': 3, 'time': -1}, 'cache09'), + 'cache10': ('{% load cache %}{% cache time test foo %}cache10{% endcache %}', {'foo': 3, 'time': -1}, 'cache10'), # Raise exception if we don't have at least 2 args, first one integer. 'cache11': ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError), 'cache12': ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError), 'cache13': ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError), - 'cache14': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': 'fail'}, template.TemplateSyntaxError), - 'cache15': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': []}, template.TemplateSyntaxError), + 'cache14': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': 'fail'}, template.TemplateSyntaxError), + 'cache15': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': []}, template.TemplateSyntaxError), ### AUTOESCAPE TAG ############################################## 'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"), diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py index 1eb55e312e..3518df3b9f 100644 --- a/tests/regressiontests/test_client_regress/models.py +++ b/tests/regressiontests/test_client_regress/models.py @@ -1,12 +1,10 @@ """ Regression tests for the Test Client, especially the customized assertions. - """ + from django.test import Client, TestCase from django.core.urlresolvers import reverse from django.core.exceptions import SuspiciousOperation -import os -import sha class AssertContainsTests(TestCase): def test_contains(self): @@ -24,7 +22,7 @@ class AssertContainsTests(TestCase): self.assertNotContains(response, 'once') except AssertionError, e: self.assertEquals(str(e), "Response should not contain 'once'") - + try: self.assertContains(response, 'never', 1) except AssertionError, e: @@ -287,7 +285,7 @@ class URLEscapingTests(TestCase): class ExceptionTests(TestCase): fixtures = ['testdata.json'] - + def test_exception_cleared(self): "#5836 - A stale user exception isn't re-raised by the test client." @@ -300,7 +298,7 @@ class ExceptionTests(TestCase): pass # At this point, an exception has been raised, and should be cleared. - + # This next operation should be successful; if it isn't we have a problem. login = self.client.login(username='staff', password='password') self.failUnless(login, 'Could not log in') @@ -309,7 +307,7 @@ class ExceptionTests(TestCase): except SuspiciousOperation: self.fail("Staff should be able to visit this page") -# We need two different tests to check URLconf subsitution - one to check +# We need two different tests to check URLconf substitution - one to check # it was changed, and another one (without self.urls) to check it was reverted on # teardown. This pair of tests relies upon the alphabetical ordering of test execution. class UrlconfSubstitutionTests(TestCase): diff --git a/tests/regressiontests/test_utils/__init__.py b/tests/regressiontests/test_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/test_utils/__init__.py diff --git a/tests/regressiontests/test_utils/models.py b/tests/regressiontests/test_utils/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/test_utils/models.py diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py new file mode 100644 index 0000000000..a2539bf8c6 --- /dev/null +++ b/tests/regressiontests/test_utils/tests.py @@ -0,0 +1,72 @@ +r""" +# Some checks of the doctest output normalizer. +# Standard doctests do fairly +>>> from django.utils import simplejson +>>> from django.utils.xmlutils import SimplerXMLGenerator +>>> from StringIO import StringIO + +>>> def produce_long(): +... return 42L + +>>> def produce_int(): +... return 42 + +>>> def produce_json(): +... return simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2), 'whiz': 42}]) + +>>> def produce_xml(): +... stream = StringIO() +... xml = SimplerXMLGenerator(stream, encoding='utf-8') +... xml.startDocument() +... xml.startElement("foo", {"aaa" : "1.0", "bbb": "2.0"}) +... xml.startElement("bar", {"ccc" : "3.0"}) +... xml.characters("Hello") +... xml.endElement("bar") +... xml.startElement("whiz", {}) +... xml.characters("Goodbye") +... xml.endElement("whiz") +... xml.endElement("foo") +... xml.endDocument() +... return stream.getvalue() + +>>> def produce_xml_fragment(): +... stream = StringIO() +... xml = SimplerXMLGenerator(stream, encoding='utf-8') +... xml.startElement("foo", {"aaa": "1.0", "bbb": "2.0"}) +... xml.characters("Hello") +... xml.endElement("foo") +... xml.startElement("bar", {"ccc": "3.0", "ddd": "4.0"}) +... xml.endElement("bar") +... return stream.getvalue() + +# Long values are normalized and are comparable to normal integers ... +>>> produce_long() +42 + +# ... and vice versa +>>> produce_int() +42L + +# JSON output is normalized for field order, so it doesn't matter +# which order json dictionary attributes are listed in output +>>> produce_json() +'["foo", {"bar": ["baz", null, 1.0, 2], "whiz": 42}]' + +>>> produce_json() +'["foo", {"whiz": 42, "bar": ["baz", null, 1.0, 2]}]' + +# XML output is normalized for attribute order, so it doesn't matter +# which order XML element attributes are listed in output +>>> produce_xml() +'<?xml version="1.0" encoding="UTF-8"?>\n<foo aaa="1.0" bbb="2.0"><bar ccc="3.0">Hello</bar><whiz>Goodbye</whiz></foo>' + +>>> produce_xml() +'<?xml version="1.0" encoding="UTF-8"?>\n<foo bbb="2.0" aaa="1.0"><bar ccc="3.0">Hello</bar><whiz>Goodbye</whiz></foo>' + +>>> produce_xml_fragment() +'<foo aaa="1.0" bbb="2.0">Hello</foo><bar ccc="3.0" ddd="4.0"></bar>' + +>>> produce_xml_fragment() +'<foo bbb="2.0" aaa="1.0">Hello</foo><bar ddd="4.0" ccc="3.0"></bar>' + +"""
\ No newline at end of file diff --git a/tests/regressiontests/utils/datastructures.py b/tests/regressiontests/utils/datastructures.py index 5d31d21318..8ac6b9ec7e 100644 --- a/tests/regressiontests/utils/datastructures.py +++ b/tests/regressiontests/utils/datastructures.py @@ -50,9 +50,3 @@ ['second-two', 'one'] """ -# Python 2.3 doesn't have sorted() -try: - sorted -except NameError: - from django.utils.itercompat import sorted -
\ No newline at end of file diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index cd4762e02f..485c5faa75 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -11,6 +11,13 @@ import datastructures import itercompat from decorators import DecoratorFromMiddlewareTests +# We need this because "datastructures" uses sorted() and the tests are run in +# the scope of this module. +try: + sorted +except NameError: + from django.utils.itercompat import sorted # For Python 2.3 + # Extra tests __test__ = { 'timesince': timesince, |