diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2012-12-18 09:02:07 +0000 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2012-12-18 09:02:07 +0000 |
| commit | b62e82365ad56ca930f7abb1d1dbdf9ce5a7c7c3 (patch) | |
| tree | 76bce44f4c7ee6172dcf116be6e4a26e380ef20a /tests | |
| parent | 6a632e04578776e877adc5e2dc53f008c890a0d4 (diff) | |
| parent | c64b57d16688025b2d48668d5c4cb9eda7484612 (diff) | |
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts:
django/db/models/loading.py
django/db/models/options.py
Diffstat (limited to 'tests')
139 files changed, 2689 insertions, 639 deletions
diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py index ebd70d14d9..1c83b980a7 100644 --- a/tests/modeltests/basic/tests.py +++ b/tests/modeltests/basic/tests.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from datetime import datetime -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db.models.fields import Field, FieldDoesNotExist from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six @@ -128,6 +128,40 @@ class ModelTest(TestCase): b = Article.objects.get(pk=a.id) self.assertEqual(a, b) + # Create a very similar object + a = Article( + id=None, + headline='Area man programs in Python', + pub_date=datetime(2005, 7, 28), + ) + a.save() + + self.assertEqual(Article.objects.count(), 2) + + # Django raises an Article.MultipleObjectsReturned exception if the + # lookup matches more than one object + self.assertRaisesRegexp( + MultipleObjectsReturned, + "get\(\) returned more than one Article -- it returned 2!", + Article.objects.get, + headline__startswith='Area', + ) + + self.assertRaisesRegexp( + MultipleObjectsReturned, + "get\(\) returned more than one Article -- it returned 2!", + Article.objects.get, + pub_date__year=2005, + ) + + self.assertRaisesRegexp( + MultipleObjectsReturned, + "get\(\) returned more than one Article -- it returned 2!", + Article.objects.get, + pub_date__year=2005, + pub_date__month=7, + ) + def test_object_creation(self): # Create an Article. a = Article( diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py index 99eb07e370..a351496442 100644 --- a/tests/modeltests/expressions/tests.py +++ b/tests/modeltests/expressions/tests.py @@ -158,6 +158,7 @@ class ExpressionsTests(TestCase): "Max Mustermann", ], lambda c: six.text_type(c.point_of_contact), + ordered=False ) c = Company.objects.all()[0] @@ -170,7 +171,8 @@ class ExpressionsTests(TestCase): "Foobar Ltd.", "Test GmbH", ], - lambda c: c.name + lambda c: c.name, + ordered=False ) Company.objects.exclude( @@ -219,3 +221,42 @@ class ExpressionsTests(TestCase): ) acme.num_employees = F("num_employees") + 16 self.assertRaises(TypeError, acme.save) + + def test_ticket_18375_join_reuse(self): + # Test that reverse multijoin F() references and the lookup target + # the same join. Pre #18375 the F() join was generated first, and the + # lookup couldn't reuse that join. + qs = Employee.objects.filter( + company_ceo_set__num_chairs=F('company_ceo_set__num_employees')) + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_ticket_18375_kwarg_ordering(self): + # The next query was dict-randomization dependent - if the "gte=1" + # was seen first, then the F() will reuse the join generated by the + # gte lookup, if F() was seen first, then it generated a join the + # other lookups could not reuse. + qs = Employee.objects.filter( + company_ceo_set__num_chairs=F('company_ceo_set__num_employees'), + company_ceo_set__num_chairs__gte=1) + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_ticket_18375_kwarg_ordering_2(self): + # Another similar case for F() than above. Now we have the same join + # in two filter kwargs, one in the lhs lookup, one in F. Here pre + # #18375 the amount of joins generated was random if dict + # randomization was enabled, that is the generated query dependend + # on which clause was seen first. + qs = Employee.objects.filter( + company_ceo_set__num_employees=F('pk'), + pk=F('company_ceo_set__num_employees') + ) + self.assertEqual(str(qs.query).count('JOIN'), 1) + + def test_ticket_18375_chained_filters(self): + # Test that F() expressions do not reuse joins from previous filter. + qs = Employee.objects.filter( + company_ceo_set__num_employees=F('pk') + ).filter( + company_ceo_set__num_employees=F('company_ceo_set__num_employees') + ) + self.assertEqual(str(qs.query).count('JOIN'), 2) diff --git a/tests/modeltests/field_subclassing/tests.py b/tests/modeltests/field_subclassing/tests.py index 48755123f2..0ec317dea5 100644 --- a/tests/modeltests/field_subclassing/tests.py +++ b/tests/modeltests/field_subclassing/tests.py @@ -77,7 +77,8 @@ class CustomField(TestCase): "12", "23", ], - lambda m: str(m.data) + lambda m: str(m.data), + ordered=False ) def test_field_subclassing(self): diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py index f9b0ac8a46..415ed6dcf2 100644 --- a/tests/modeltests/fixtures/tests.py +++ b/tests/modeltests/fixtures/tests.py @@ -96,8 +96,8 @@ class FixtureLoadingTests(TestCase): management.call_command('loaddata', 'fixture6.json', verbosity=0, commit=False) self.assertQuerysetEqual(Tag.objects.all(), [ '<Tag: <Article: Copyright is fine the way it is> tagged "copyright">', - '<Tag: <Article: Copyright is fine the way it is> tagged "law">' - ]) + '<Tag: <Article: Copyright is fine the way it is> tagged "law">', + ], ordered=False) # Load fixture 7, XML file with dynamic ContentType fields. Testing ManyToOne. management.call_command('loaddata', 'fixture7.xml', verbosity=0, commit=False) @@ -105,8 +105,8 @@ class FixtureLoadingTests(TestCase): '<Tag: <Article: Copyright is fine the way it is> tagged "copyright">', '<Tag: <Article: Copyright is fine the way it is> tagged "legal">', '<Tag: <Article: Django conquers world!> tagged "django">', - '<Tag: <Article: Django conquers world!> tagged "world domination">' - ]) + '<Tag: <Article: Django conquers world!> tagged "world domination">', + ], ordered=False) # Load fixture 8, JSON file with dynamic Permission fields. Testing ManyToMany. management.call_command('loaddata', 'fixture8.json', verbosity=0, commit=False) @@ -114,7 +114,7 @@ class FixtureLoadingTests(TestCase): '<Visa: Django Reinhardt Can add user, Can change user, Can delete user>', '<Visa: Stephane Grappelli Can add user>', '<Visa: Prince >' - ]) + ], ordered=False) # Load fixture 9, XML file with dynamic Permission fields. Testing ManyToMany. management.call_command('loaddata', 'fixture9.xml', verbosity=0, commit=False) @@ -122,7 +122,7 @@ class FixtureLoadingTests(TestCase): '<Visa: Django Reinhardt Can add user, Can change user, Can delete user>', '<Visa: Stephane Grappelli Can add user, Can delete user>', '<Visa: Artist formerly known as "Prince" Can change user>' - ]) + ], ordered=False) self.assertQuerysetEqual(Book.objects.all(), [ '<Book: Achieving self-awareness of Python programs>', @@ -226,9 +226,9 @@ class FixtureLoadingTests(TestCase): def test_ambiguous_compressed_fixture(self): # The name "fixture5" is ambigous, so loading it will raise an error - with six.assertRaisesRegex(self, management.CommandError, - "Multiple fixtures named 'fixture5'"): + with self.assertRaises(management.CommandError) as cm: management.call_command('loaddata', 'fixture5', verbosity=0, commit=False) + self.assertIn("Multiple fixtures named 'fixture5'", cm.exception.args[0]) def test_db_loading(self): # Load db fixtures 1 and 2. These will load using the 'default' database identifier implicitly @@ -250,9 +250,9 @@ class FixtureLoadingTests(TestCase): # is closed at the end of each test. if connection.vendor == 'mysql': connection.cursor().execute("SET sql_mode = 'TRADITIONAL'") - with six.assertRaisesRegex(self, IntegrityError, - "Could not load fixtures.Article\(pk=1\): .*$"): + with self.assertRaises(IntegrityError) as cm: management.call_command('loaddata', 'invalid.json', verbosity=0, commit=False) + self.assertIn("Could not load fixtures.Article(pk=1):", cm.exception.args[0]) def test_loading_using(self): # Load db fixtures 1 and 2. These will load using the 'default' database identifier explicitly @@ -280,7 +280,7 @@ class FixtureLoadingTests(TestCase): self.assertQuerysetEqual(Tag.objects.all(), [ '<Tag: <Article: Time to reform copyright> tagged "copyright">', '<Tag: <Article: Time to reform copyright> tagged "law">' - ]) + ], ordered=False) # Dump the current contents of the database as a JSON fixture self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) @@ -308,9 +308,9 @@ class FixtureTransactionTests(TransactionTestCase): # Try to load fixture 2 using format discovery; this will fail # because there are two fixture2's in the fixtures directory - with six.assertRaisesRegex(self, management.CommandError, - "Multiple fixtures named 'fixture2'"): + with self.assertRaises(management.CommandError) as cm: management.call_command('loaddata', 'fixture2', verbosity=0) + self.assertIn("Multiple fixtures named 'fixture2'", cm.exception.args[0]) # object list is unaffected self.assertQuerysetEqual(Article.objects.all(), [ diff --git a/tests/modeltests/generic_relations/tests.py b/tests/modeltests/generic_relations/tests.py index 14871e4e09..73b0a483a2 100644 --- a/tests/modeltests/generic_relations/tests.py +++ b/tests/modeltests/generic_relations/tests.py @@ -169,8 +169,8 @@ class GenericRelationsTests(TestCase): # Filtering works self.assertQuerysetEqual(tiger.comparisons.filter(comparative="cooler"), [ "<Comparison: tiger is cooler than cheetah>", - "<Comparison: tiger is cooler than bear>" - ]) + "<Comparison: tiger is cooler than bear>", + ], ordered=False) # Filtering and deleting works subjective = ["cooler"] @@ -178,7 +178,7 @@ class GenericRelationsTests(TestCase): self.assertQuerysetEqual(Comparison.objects.all(), [ "<Comparison: cheetah is faster than tiger>", "<Comparison: tiger is stronger than cheetah>" - ]) + ], ordered=False) # If we delete cheetah, Comparisons with cheetah as 'first_obj' will be # deleted since Animal has an explicit GenericRelation to Comparison diff --git a/tests/modeltests/get_object_or_404/tests.py b/tests/modeltests/get_object_or_404/tests.py index 280720fd15..3b234c6cd3 100644 --- a/tests/modeltests/get_object_or_404/tests.py +++ b/tests/modeltests/get_object_or_404/tests.py @@ -80,3 +80,28 @@ class GetObjectOr404Tests(TestCase): get_list_or_404(Article.objects.all(), title__icontains="Run"), [article] ) + + def test_bad_class(self): + # Given an argument klass that is not a Model, Manager, or Queryset + # raises a helpful ValueError message + self.assertRaisesMessage(ValueError, + "Object is of type 'str', but must be a Django Model, Manager, " + "or QuerySet", + get_object_or_404, "Article", title__icontains="Run" + ) + + class CustomClass(object): + pass + + self.assertRaisesMessage(ValueError, + "Object is of type 'CustomClass', but must be a Django Model, " + "Manager, or QuerySet", + get_object_or_404, CustomClass, title__icontains="Run" + ) + + # Works for lists too + self.assertRaisesMessage(ValueError, + "Object is of type 'list', but must be a Django Model, Manager, " + "or QuerySet", + get_list_or_404, [Article], title__icontains="Run" + ) diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py index ccb6396352..3c21e1ddb8 100644 --- a/tests/modeltests/invalid_models/invalid_models/models.py +++ b/tests/modeltests/invalid_models/invalid_models/models.py @@ -356,6 +356,13 @@ class HardReferenceModel(models.Model): m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4') +class BadIndexTogether1(models.Model): + class Meta: + index_together = [ + ["field_that_does_not_exist"], + ] + + model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. @@ -470,6 +477,7 @@ invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'in invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'. invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract. +invalid_models.badindextogether1: "index_together" refers to field_that_does_not_exist, a field that doesn't exist. """ if not connection.features.interprets_empty_strings_as_nulls: diff --git a/tests/modeltests/m2m_recursive/tests.py b/tests/modeltests/m2m_recursive/tests.py index 5742836929..a3f2c670d6 100644 --- a/tests/modeltests/m2m_recursive/tests.py +++ b/tests/modeltests/m2m_recursive/tests.py @@ -28,7 +28,8 @@ class RecursiveM2MTests(TestCase): "Chuck", "David" ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is friends with Bill? self.assertQuerysetEqual( @@ -43,7 +44,8 @@ class RecursiveM2MTests(TestCase): "Anne", "David" ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is friends with David? self.assertQuerysetEqual( @@ -51,7 +53,8 @@ class RecursiveM2MTests(TestCase): "Anne", "Chuck", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Bill is already friends with Anne - add Anne again, but in the # reverse direction @@ -64,7 +67,8 @@ class RecursiveM2MTests(TestCase): "Chuck", "David", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is friends with Bill? self.assertQuerysetEqual( @@ -81,7 +85,8 @@ class RecursiveM2MTests(TestCase): "Chuck", "David", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is friends with Bill? self.assertQuerysetEqual( @@ -125,7 +130,8 @@ class RecursiveM2MTests(TestCase): "Chuck", "David", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is stalking Anne? self.assertQuerysetEqual( @@ -172,7 +178,8 @@ class RecursiveM2MTests(TestCase): "Anne", "Chuck", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Bill is already being stalked by Anne - add Anne again, but in the # reverse direction @@ -184,7 +191,8 @@ class RecursiveM2MTests(TestCase): "Chuck", "David", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is stalking Anne? self.assertQuerysetEqual( @@ -215,7 +223,8 @@ class RecursiveM2MTests(TestCase): "Chuck", "David", ], - attrgetter("name") + attrgetter("name"), + ordered=False ) # Who is stalking Anne? self.assertQuerysetEqual( diff --git a/tests/modeltests/many_to_one/tests.py b/tests/modeltests/many_to_one/tests.py index 4fb19dbc69..44ae689dd4 100644 --- a/tests/modeltests/many_to_one/tests.py +++ b/tests/modeltests/many_to_one/tests.py @@ -267,7 +267,9 @@ class ManyToOneTests(TestCase): ["<Reporter: John Smith>"]) self.assertQuerysetEqual( Reporter.objects.filter(article__headline__startswith='T'), - ["<Reporter: John Smith>", "<Reporter: John Smith>"]) + ["<Reporter: John Smith>", "<Reporter: John Smith>"], + ordered=False + ) self.assertQuerysetEqual( Reporter.objects.filter(article__headline__startswith='T').distinct(), ["<Reporter: John Smith>"]) @@ -285,7 +287,9 @@ class ManyToOneTests(TestCase): "<Reporter: John Smith>", "<Reporter: John Smith>", "<Reporter: John Smith>", - ]) + ], + ordered=False + ) self.assertQuerysetEqual( Reporter.objects.filter(article__reporter__first_name__startswith='John').distinct(), ["<Reporter: John Smith>"]) diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 90b019b611..132da2d318 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -91,15 +91,6 @@ class WriterProfile(models.Model): def __str__(self): return "%s is %s" % (self.writer, self.age) -from django.contrib.localflavor.us.models import PhoneNumberField -@python_2_unicode_compatible -class PhoneNumber(models.Model): - phone = PhoneNumberField() - description = models.CharField(max_length=20) - - def __str__(self): - return self.phone - @python_2_unicode_compatible class TextFile(models.Model): description = models.CharField(max_length=20) diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index 947d0cf3c3..9699b155c0 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -10,6 +10,7 @@ from django.core.validators import ValidationError from django.db import connection from django.db.models.query import EmptyQuerySet from django.forms.models import model_to_dict +from django.utils._os import upath from django.utils.unittest import skipUnless from django.test import TestCase from django.utils import six @@ -17,7 +18,7 @@ from django.utils import six from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book, Category, CommaSeparatedInteger, CustomFieldForExclusionModel, DerivedBook, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, - ImprovedArticleWithParentLink, Inventory, PhoneNumber, Post, Price, + ImprovedArticleWithParentLink, Inventory, Post, Price, Product, TextFile, Writer, WriterProfile, test_images) if test_images: @@ -147,10 +148,6 @@ class WriterProfileForm(forms.ModelForm): class Meta: model = WriterProfile -class PhoneNumberForm(forms.ModelForm): - class Meta: - model = PhoneNumber - class TextFileForm(forms.ModelForm): class Meta: model = TextFile @@ -166,7 +163,7 @@ class ModelFormWithMedia(forms.ModelForm): 'all': ('/some/form/css',) } class Meta: - model = PhoneNumber + model = TextFile class CommaSeparatedIntegerForm(forms.ModelForm): class Meta: @@ -561,6 +558,42 @@ class UniqueTest(TestCase): "slug": "Django 1.0"}, instance=p) self.assertTrue(form.is_valid()) +class ModelToDictTests(TestCase): + """ + Tests for forms.models.model_to_dict + """ + def test_model_to_dict_many_to_many(self): + categories=[ + Category(name='TestName1', slug='TestName1', url='url1'), + Category(name='TestName2', slug='TestName2', url='url2'), + Category(name='TestName3', slug='TestName3', url='url3') + ] + for c in categories: + c.save() + writer = Writer(name='Test writer') + writer.save() + + art = Article( + headline='Test article', + slug='test-article', + pub_date=datetime.date(1988, 1, 4), + writer=writer, + article='Hello.' + ) + art.save() + for c in categories: + art.categories.add(c) + art.save() + + with self.assertNumQueries(1): + d = model_to_dict(art) + + #Ensure all many-to-many categories appear in model_to_dict + for c in categories: + self.assertIn(c.pk, d['categories']) + #Ensure many-to-many relation appears as a list + self.assertIsInstance(d['categories'], list) + class OldFormForXTests(TestCase): def test_base_form(self): self.assertEqual(Category.objects.count(), 0) @@ -1011,9 +1044,12 @@ class OldFormForXTests(TestCase): self.assertQuerysetEqual(f.clean([c1.id]), ["Entertainment"]) self.assertQuerysetEqual(f.clean([c2.id]), ["It's a test"]) self.assertQuerysetEqual(f.clean([str(c1.id)]), ["Entertainment"]) - self.assertQuerysetEqual(f.clean([str(c1.id), str(c2.id)]), ["Entertainment", "It's a test"]) - self.assertQuerysetEqual(f.clean([c1.id, str(c2.id)]), ["Entertainment", "It's a test"]) - self.assertQuerysetEqual(f.clean((c1.id, str(c2.id))), ["Entertainment", "It's a test"]) + self.assertQuerysetEqual(f.clean([str(c1.id), str(c2.id)]), ["Entertainment", "It's a test"], + ordered=False) + self.assertQuerysetEqual(f.clean([c1.id, str(c2.id)]), ["Entertainment", "It's a test"], + ordered=False) + self.assertQuerysetEqual(f.clean((c1.id, str(c2.id))), ["Entertainment", "It's a test"], + ordered=False) with self.assertRaises(ValidationError): f.clean(['100']) with self.assertRaises(ValidationError): @@ -1024,7 +1060,10 @@ class OldFormForXTests(TestCase): # Add a Category object *after* the ModelMultipleChoiceField has already been # instantiated. This proves clean() checks the database during clean() rather # than caching it at time of instantiation. - c6 = Category.objects.create(id=6, name='Sixth', url='6th') + # Note, we are using an id of 1006 here since tests that run before + # this may create categories with primary keys up to 6. Use + # a number that is will not conflict. + c6 = Category.objects.create(id=1006, name='Sixth', url='6th') self.assertEqual(c6.name, 'Sixth') self.assertQuerysetEqual(f.clean([c6.id]), ["Sixth"]) @@ -1109,12 +1148,6 @@ class OldFormForXTests(TestCase): </select></p> <p><label for="id_age">Age:</label> <input type="text" name="age" value="65" id="id_age" /></p>''' % (w_woodward.pk, w_bernstein.pk, bw.pk, w_royko.pk)) - def test_phone_number_field(self): - f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'}) - self.assertEqual(f.is_valid(), True) - self.assertEqual(f.cleaned_data['phone'], '312-555-1212') - self.assertEqual(f.cleaned_data['description'], 'Assistance') - def test_file_field(self): # Test conditions when files is either not given or empty. @@ -1243,9 +1276,9 @@ class OldFormForXTests(TestCase): # it comes to validation. This specifically tests that #6302 is fixed for # both file fields and image fields. - with open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb') as fp: + with open(os.path.join(os.path.dirname(upath(__file__)), "test.png"), 'rb') as fp: image_data = fp.read() - with open(os.path.join(os.path.dirname(__file__), "test2.png"), 'rb') as fp: + with open(os.path.join(os.path.dirname(upath(__file__)), "test2.png"), 'rb') as fp: image_data2 = fp.read() f = ImageFileForm( diff --git a/tests/modeltests/pagination/tests.py b/tests/modeltests/pagination/tests.py deleted file mode 100644 index 12ce0e3ecb..0000000000 --- a/tests/modeltests/pagination/tests.py +++ /dev/null @@ -1,134 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -from datetime import datetime - -from django.core.paginator import Paginator, InvalidPage, EmptyPage -from django.test import TestCase -from django.utils import six - -from .models import Article - - -class CountContainer(object): - def count(self): - return 42 - -class LenContainer(object): - def __len__(self): - return 42 - -class PaginationTests(TestCase): - def setUp(self): - # Prepare a list of objects for pagination. - for x in range(1, 10): - a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) - a.save() - - def test_paginator(self): - paginator = Paginator(Article.objects.all(), 5) - self.assertEqual(9, paginator.count) - self.assertEqual(2, paginator.num_pages) - self.assertEqual([1, 2], list(paginator.page_range)) - - def test_first_page(self): - paginator = Paginator(Article.objects.all(), 5) - p = paginator.page(1) - self.assertEqual("<Page 1 of 2>", six.text_type(p)) - self.assertQuerysetEqual(p.object_list, [ - "<Article: Article 1>", - "<Article: Article 2>", - "<Article: Article 3>", - "<Article: Article 4>", - "<Article: Article 5>" - ] - ) - self.assertTrue(p.has_next()) - self.assertFalse(p.has_previous()) - self.assertTrue(p.has_other_pages()) - self.assertEqual(2, p.next_page_number()) - self.assertRaises(InvalidPage, p.previous_page_number) - self.assertEqual(1, p.start_index()) - self.assertEqual(5, p.end_index()) - - def test_last_page(self): - paginator = Paginator(Article.objects.all(), 5) - p = paginator.page(2) - self.assertEqual("<Page 2 of 2>", six.text_type(p)) - self.assertQuerysetEqual(p.object_list, [ - "<Article: Article 6>", - "<Article: Article 7>", - "<Article: Article 8>", - "<Article: Article 9>" - ] - ) - self.assertFalse(p.has_next()) - self.assertTrue(p.has_previous()) - self.assertTrue(p.has_other_pages()) - self.assertRaises(InvalidPage, p.next_page_number) - self.assertEqual(1, p.previous_page_number()) - self.assertEqual(6, p.start_index()) - self.assertEqual(9, p.end_index()) - - def test_empty_page(self): - paginator = Paginator(Article.objects.all(), 5) - self.assertRaises(EmptyPage, paginator.page, 0) - self.assertRaises(EmptyPage, paginator.page, 3) - - # Empty paginators with allow_empty_first_page=True. - paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True) - self.assertEqual(0, paginator.count) - self.assertEqual(1, paginator.num_pages) - self.assertEqual([1], list(paginator.page_range)) - - # Empty paginators with allow_empty_first_page=False. - paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False) - self.assertEqual(0, paginator.count) - self.assertEqual(0, paginator.num_pages) - self.assertEqual([], list(paginator.page_range)) - - def test_invalid_page(self): - paginator = Paginator(Article.objects.all(), 5) - self.assertRaises(InvalidPage, paginator.page, 7) - - def test_orphans(self): - # Add a few more records to test out the orphans feature. - for x in range(10, 13): - Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() - - # With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. - paginator = Paginator(Article.objects.all(), 10, orphans=3) - self.assertEqual(1, paginator.num_pages) - - # With orphans only set to 1, we should get two pages. - paginator = Paginator(Article.objects.all(), 10, orphans=1) - self.assertEqual(2, paginator.num_pages) - - def test_paginate_list(self): - # Paginators work with regular lists/tuples, too -- not just with QuerySets. - paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 3) - self.assertEqual(9, paginator.count) - self.assertEqual(3, paginator.num_pages) - self.assertEqual([1, 2, 3], list(paginator.page_range)) - p = paginator.page(2) - self.assertEqual("<Page 2 of 3>", six.text_type(p)) - self.assertEqual([4, 5, 6], p.object_list) - self.assertTrue(p.has_next()) - self.assertTrue(p.has_previous()) - self.assertTrue(p.has_other_pages()) - self.assertEqual(3, p.next_page_number()) - self.assertEqual(1, p.previous_page_number()) - self.assertEqual(4, p.start_index()) - self.assertEqual(6, p.end_index()) - - def test_paginate_misc_classes(self): - # Paginator can be passed other objects with a count() method. - paginator = Paginator(CountContainer(), 10) - self.assertEqual(42, paginator.count) - self.assertEqual(5, paginator.num_pages) - self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range)) - - # Paginator can be passed other objects that implement __len__. - paginator = Paginator(LenContainer(), 10) - self.assertEqual(42, paginator.count) - self.assertEqual(5, paginator.num_pages) - self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range)) diff --git a/tests/modeltests/prefetch_related/models.py b/tests/modeltests/prefetch_related/models.py index 85488f0879..e58997d200 100644 --- a/tests/modeltests/prefetch_related/models.py +++ b/tests/modeltests/prefetch_related/models.py @@ -125,6 +125,10 @@ class TaggedItem(models.Model): related_name='taggeditem_set3') created_by_fkey = models.PositiveIntegerField(null=True) created_by = generic.GenericForeignKey('created_by_ct', 'created_by_fkey',) + favorite_ct = models.ForeignKey(ContentType, null=True, + related_name='taggeditem_set4') + favorite_fkey = models.CharField(max_length=64, null=True) + favorite = generic.GenericForeignKey('favorite_ct', 'favorite_fkey') def __str__(self): return self.tag @@ -132,7 +136,11 @@ class TaggedItem(models.Model): class Bookmark(models.Model): url = models.URLField() - tags = generic.GenericRelation(TaggedItem) + tags = generic.GenericRelation(TaggedItem, related_name='bookmarks') + favorite_tags = generic.GenericRelation(TaggedItem, + content_type_field='favorite_ct', + object_id_field='favorite_fkey', + related_name='favorite_bookmarks') class Comment(models.Model): diff --git a/tests/modeltests/prefetch_related/tests.py b/tests/modeltests/prefetch_related/tests.py index 614a5fc1d6..e81560f01f 100644 --- a/tests/modeltests/prefetch_related/tests.py +++ b/tests/modeltests/prefetch_related/tests.py @@ -319,6 +319,16 @@ class GenericRelationTests(TestCase): for t in b.tags.all()] self.assertEqual(sorted(tags), ["django", "python"]) + def test_charfield_GFK(self): + b = Bookmark.objects.create(url='http://www.djangoproject.com/') + t1 = TaggedItem.objects.create(content_object=b, tag='django') + t2 = TaggedItem.objects.create(content_object=b, favorite=b, tag='python') + + with self.assertNumQueries(3): + bookmark = Bookmark.objects.filter(pk=b.pk).prefetch_related('tags', 'favorite_tags')[0] + self.assertEqual(sorted([i.tag for i in bookmark.tags.all()]), ["django", "python"]) + self.assertEqual([i.tag for i in bookmark.favorite_tags.all()], ["python"]) + class MultiTableInheritanceTest(TestCase): diff --git a/tests/modeltests/proxy_model_inheritance/tests.py b/tests/modeltests/proxy_model_inheritance/tests.py index 39fee7ee6d..239bc67809 100644 --- a/tests/modeltests/proxy_model_inheritance/tests.py +++ b/tests/modeltests/proxy_model_inheritance/tests.py @@ -8,6 +8,7 @@ from django.core.management import call_command from django.db.models.loading import cache, load_app from django.test import TestCase, TransactionTestCase from django.test.utils import override_settings +from django.utils._os import upath from .models import (ConcreteModel, ConcreteModelSubclass, ConcreteModelSubclassProxy) @@ -23,7 +24,7 @@ class ProxyModelInheritanceTests(TransactionTestCase): def setUp(self): self.old_sys_path = sys.path[:] - sys.path.append(os.path.dirname(os.path.abspath(__file__))) + sys.path.append(os.path.dirname(os.path.abspath(upath(__file__)))) for app in settings.INSTALLED_APPS: load_app(app) diff --git a/tests/modeltests/raw_query/models.py b/tests/modeltests/raw_query/models.py index 823fc76eee..e7e221dc6e 100644 --- a/tests/modeltests/raw_query/models.py +++ b/tests/modeltests/raw_query/models.py @@ -13,7 +13,7 @@ class Author(models.Model): # treated differently than fields. for k in kwargs: assert k in [f.attname for f in self._meta.fields], \ - "Author.__init__ got an unexpected paramater: %s" % k + "Author.__init__ got an unexpected parameter: %s" % k class Book(models.Model): title = models.CharField(max_length=255) diff --git a/tests/modeltests/str/models.py b/tests/modeltests/str/models.py index 488012e861..1c158ea008 100644 --- a/tests/modeltests/str/models.py +++ b/tests/modeltests/str/models.py @@ -28,6 +28,14 @@ class Article(models.Model): return self.headline @python_2_unicode_compatible +class BrokenArticle(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + + def __unicode__(self): # instead of __str__ + return self.headline + +@python_2_unicode_compatible class InternationalArticle(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() diff --git a/tests/modeltests/str/tests.py b/tests/modeltests/str/tests.py index 2c11ac8c78..31869583aa 100644 --- a/tests/modeltests/str/tests.py +++ b/tests/modeltests/str/tests.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.utils import six from django.utils.unittest import skipIf -from .models import Article, InternationalArticle +from .models import Article, BrokenArticle, InternationalArticle class SimpleTests(TestCase): @@ -21,6 +21,16 @@ class SimpleTests(TestCase): self.assertEqual(str(a), str('Area man programs in Python')) self.assertEqual(repr(a), str('<Article: Area man programs in Python>')) + @skipIf(six.PY3, "tests Model's default __str__ method under Python 2") + def test_broken(self): + # Regression test for #19362. + a = BrokenArticle.objects.create( + headline='Girl wins €12.500 in lottery', + pub_date=datetime.datetime(2005, 7, 28) + ) + self.assertRaisesRegexp(RuntimeError, "Did you apply " + "@python_2_unicode_compatible without defining __str__\?", str, a) + def test_international(self): a = InternationalArticle.objects.create( headline='Girl wins €12.500 in lottery', diff --git a/tests/regressiontests/admin_changelist/admin.py b/tests/regressiontests/admin_changelist/admin.py index 9ecfbc6e12..5751d04bce 100644 --- a/tests/regressiontests/admin_changelist/admin.py +++ b/tests/regressiontests/admin_changelist/admin.py @@ -32,6 +32,7 @@ class ParentAdmin(admin.ModelAdmin): class ChildAdmin(admin.ModelAdmin): list_display = ['name', 'parent'] list_per_page = 10 + list_filter = ['parent', 'age'] def queryset(self, request): return super(ChildAdmin, self).queryset(request).select_related("parent__name") @@ -90,3 +91,14 @@ class SwallowAdmin(admin.ModelAdmin): list_display = ('origin', 'load', 'speed') site.register(Swallow, SwallowAdmin) + +class DynamicListFilterChildAdmin(admin.ModelAdmin): + list_filter = ('parent', 'name', 'age') + + def get_list_filter(self, request): + my_list_filter = super(DynamicListFilterChildAdmin, self).get_list_filter(request) + if request.user.username == 'noparents': + my_list_filter = list(my_list_filter) + my_list_filter.remove('parent') + return my_list_filter + diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index 2b1c1a9bcf..e8d4cbff16 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -17,7 +17,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, - SwallowAdmin) + SwallowAdmin, DynamicListFilterChildAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject) @@ -541,3 +541,26 @@ class ChangeListTests(TestCase): check_results_order() OrderedObjectAdmin.ordering = ['id', 'bool'] check_results_order(ascending=True) + + def test_dynamic_list_filter(self): + """ + Regression tests for ticket #17646: dynamic list_filter support. + """ + parent = Parent.objects.create(name='parent') + for i in range(10): + Child.objects.create(name='child %s' % i, parent=parent) + + user_noparents = self._create_superuser('noparents') + user_parents = self._create_superuser('parents') + + # Test with user 'noparents' + m = DynamicListFilterChildAdmin(Child, admin.site) + request = self._mocked_authenticated_request('/child/', user_noparents) + response = m.changelist_view(request) + self.assertEqual(response.context_data['cl'].list_filter, ['name', 'age']) + + # Test with user 'parents' + m = DynamicListFilterChildAdmin(Child, admin.site) + request = self._mocked_authenticated_request('/child/', user_parents) + response = m.changelist_view(request) + self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age')) diff --git a/tests/regressiontests/admin_filters/tests.py b/tests/regressiontests/admin_filters/tests.py index 6a36f62cea..4a3e6135c3 100644 --- a/tests/regressiontests/admin_filters/tests.py +++ b/tests/regressiontests/admin_filters/tests.py @@ -78,6 +78,21 @@ class DecadeListFilterParameterEndsWith__Isnull(DecadeListFilter): parameter_name = 'decade__isnull' # Ends with '__isnull" +class DepartmentListFilterLookupWithNonStringValue(SimpleListFilter): + title = 'department' + parameter_name = 'department' + + def lookups(self, request, model_admin): + return set([ + (employee.department.id, # Intentionally not a string (Refs #19318) + employee.department.code) + for employee in model_admin.queryset(request).all() + ]) + + def queryset(self, request, queryset): + if self.value(): + return queryset.filter(department__id=self.value()) + class CustomUserAdmin(UserAdmin): list_filter = ('books_authored', 'books_contributed') @@ -118,6 +133,10 @@ class EmployeeAdmin(ModelAdmin): list_filter = ['department'] +class DepartmentFilterEmployeeAdmin(EmployeeAdmin): + list_filter = [DepartmentListFilterLookupWithNonStringValue, ] + + class ListFiltersTests(TestCase): def setUp(self): @@ -140,6 +159,14 @@ class ListFiltersTests(TestCase): self.gipsy_book.contributors = [self.bob, self.lisa] self.gipsy_book.save() + # Departments + self.dev = Department.objects.create(code='DEV', description='Development') + self.design = Department.objects.create(code='DSN', description='Design') + + # Employees + self.john = Employee.objects.create(name='John Blue', department=self.dev) + self.jack = Employee.objects.create(name='Jack Red', department=self.design) + def get_changelist(self, request, model, modeladmin): return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links, modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, @@ -638,6 +665,28 @@ class ListFiltersTests(TestCase): self.assertEqual(choices[2]['selected'], True) self.assertEqual(choices[2]['query_string'], '?decade__isnull=the+90s') + def test_lookup_with_non_string_value(self): + """ + Ensure choices are set the selected class when using non-string values + for lookups in SimpleListFilters. + Refs #19318 + """ + + modeladmin = DepartmentFilterEmployeeAdmin(Employee, site) + request = self.request_factory.get('/', {'department': self.john.pk}) + changelist = self.get_changelist(request, Employee, modeladmin) + + queryset = changelist.get_query_set(request) + + self.assertEqual(list(queryset), [self.john]) + + filterspec = changelist.get_filters(request)[0][-1] + self.assertEqual(force_text(filterspec.title), 'department') + choices = list(filterspec.choices(changelist)) + self.assertEqual(choices[2]['display'], 'DEV') + self.assertEqual(choices[2]['selected'], True) + self.assertEqual(choices[2]['query_string'], '?department=%s' % self.john.pk) + def test_fk_with_to_field(self): """ Ensure that a filter on a FK respects the FK's to_field attribute. @@ -645,17 +694,12 @@ class ListFiltersTests(TestCase): """ modeladmin = EmployeeAdmin(Employee, site) - dev = Department.objects.create(code='DEV', description='Development') - design = Department.objects.create(code='DSN', description='Design') - john = Employee.objects.create(name='John Blue', department=dev) - jack = Employee.objects.create(name='Jack Red', department=design) - request = self.request_factory.get('/', {}) changelist = self.get_changelist(request, Employee, modeladmin) # Make sure the correct queryset is returned queryset = changelist.get_query_set(request) - self.assertEqual(list(queryset), [jack, john]) + self.assertEqual(list(queryset), [self.jack, self.john]) filterspec = changelist.get_filters(request)[0][-1] self.assertEqual(force_text(filterspec.title), 'department') @@ -680,7 +724,7 @@ class ListFiltersTests(TestCase): # Make sure the correct queryset is returned queryset = changelist.get_query_set(request) - self.assertEqual(list(queryset), [john]) + self.assertEqual(list(queryset), [self.john]) filterspec = changelist.get_filters(request)[0][-1] self.assertEqual(force_text(filterspec.title), 'department') diff --git a/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.ico b/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.ico Binary files differnew file mode 100644 index 0000000000..1db49645b5 --- /dev/null +++ b/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.ico diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 3bb8bb0b50..d0ca9d26df 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -19,13 +19,15 @@ from django.conf import settings from django.db import connection from django.test.simple import DjangoTestSuiteRunner from django.utils import unittest +from django.utils.encoding import force_str, force_text +from django.utils._os import upath from django.test import LiveServerTestCase -test_dir = os.path.dirname(os.path.dirname(__file__)) +test_dir = os.path.dirname(os.path.dirname(upath(__file__))) class AdminScriptTestCase(unittest.TestCase): def write_settings(self, filename, apps=None, is_dir=False, sdict=None): - test_dir = os.path.dirname(os.path.dirname(__file__)) + test_dir = os.path.dirname(os.path.dirname(upath(__file__))) if is_dir: settings_dir = os.path.join(test_dir, filename) os.mkdir(settings_dir) @@ -94,6 +96,7 @@ class AdminScriptTestCase(unittest.TestCase): return paths 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) ext_backend_base_dirs = self._ext_backend_paths() @@ -134,11 +137,17 @@ class AdminScriptTestCase(unittest.TestCase): return out, err def run_django_admin(self, args, settings_file=None): - bin_dir = os.path.abspath(os.path.dirname(bin.__file__)) + bin_dir = os.path.abspath(os.path.dirname(upath(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__) + def safe_remove(path): + try: + os.remove(path) + except OSError: + pass + + conf_dir = os.path.dirname(upath(conf.__file__)) template_manage_py = os.path.join(conf_dir, 'project_template', 'manage.py') test_manage_py = os.path.join(test_dir, 'manage.py') @@ -150,13 +159,9 @@ class AdminScriptTestCase(unittest.TestCase): "{{ project_name }}", "regressiontests") with open(test_manage_py, 'w') as fp: fp.write(manage_py_contents) + self.addCleanup(safe_remove, test_manage_py) - stdout, stderr = self.run_test('./manage.py', args, settings_file) - - # Cleanup - remove the generated manage.py script - os.remove(test_manage_py) - - return stdout, stderr + return self.run_test('./manage.py', args, settings_file) def assertNoOutput(self, stream): "Utility assertion: assert that the given stream is empty" @@ -164,10 +169,12 @@ class AdminScriptTestCase(unittest.TestCase): def assertOutput(self, stream, msg): "Utility assertion: assert that the given message exists in the output" + stream = force_text(stream) self.assertTrue(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream)) def assertNotInOutput(self, stream, msg): "Utility assertion: assert that the given message doesn't exist in the output" + stream = force_text(stream) self.assertFalse(msg in stream, "'%s' matches actual output text '%s'" % (msg, stream)) ########################################################################## @@ -1410,9 +1417,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): "Make sure the startproject management command creates a project" args = ['startproject', 'testproject'] testproject_dir = os.path.join(test_dir, 'testproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) @@ -1423,16 +1430,11 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): def test_invalid_project_name(self): "Make sure the startproject management command validates a project name" - - def cleanup(p): - if os.path.exists(p): - shutil.rmtree(p) - args = ['startproject', '7testproject'] testproject_dir = os.path.join(test_dir, '7testproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(cleanup, testproject_dir) self.assertOutput(err, "Error: '7testproject' is not a valid project name. Please make sure the name begins with a letter or underscore.") self.assertFalse(os.path.exists(testproject_dir)) @@ -1441,9 +1443,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): args = ['startproject', 'testproject', 'othertestproject'] testproject_dir = os.path.join(test_dir, 'othertestproject') os.mkdir(testproject_dir) + self.addCleanup(shutil.rmtree, testproject_dir) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'manage.py'))) @@ -1457,9 +1459,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template') args = ['startproject', '--template', template_path, 'customtestproject'] testproject_dir = os.path.join(test_dir, 'customtestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'additional_dir'))) @@ -1469,9 +1471,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template' + os.sep) args = ['startproject', '--template', template_path, 'customtestproject'] testproject_dir = os.path.join(test_dir, 'customtestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'additional_dir'))) @@ -1481,9 +1483,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template.tgz') args = ['startproject', '--template', template_path, 'tarballtestproject'] testproject_dir = os.path.join(test_dir, 'tarballtestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py'))) @@ -1494,9 +1496,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): args = ['startproject', '--template', template_path, 'tarballtestproject', 'altlocation'] testproject_dir = os.path.join(test_dir, 'altlocation') os.mkdir(testproject_dir) + self.addCleanup(shutil.rmtree, testproject_dir) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py'))) @@ -1507,9 +1509,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): args = ['startproject', '--template', template_url, 'urltestproject'] testproject_dir = os.path.join(test_dir, 'urltestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py'))) @@ -1520,9 +1522,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): args = ['startproject', '--template', template_url, 'urltestproject'] testproject_dir = os.path.join(test_dir, 'urltestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py'))) @@ -1532,9 +1534,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template') args = ['startproject', '--template', template_path, 'customtestproject', '-e', 'txt', '-n', 'Procfile'] testproject_dir = os.path.join(test_dir, 'customtestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'additional_dir'))) @@ -1551,12 +1553,12 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): args = ['startproject', '--template', template_path, 'another_project', 'project_dir'] testproject_dir = os.path.join(test_dir, 'project_dir') os.mkdir(testproject_dir) - out, err = self.run_django_admin(args) self.addCleanup(shutil.rmtree, testproject_dir) + out, err = self.run_django_admin(args) self.assertNoOutput(err) test_manage_py = os.path.join(testproject_dir, 'manage.py') with open(test_manage_py, 'r') as fp: - content = fp.read() + content = force_text(fp.read()) self.assertIn("project_name = 'another_project'", content) self.assertIn("project_directory = '%s'" % testproject_dir, content) @@ -1564,19 +1566,18 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): "Make sure template context variables are not html escaped" # We're using a custom command so we need the alternate settings self.write_settings('alternate_settings.py') + self.addCleanup(self.remove_settings, 'alternate_settings.py') template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template') args = ['custom_startproject', '--template', template_path, 'another_project', 'project_dir', '--extra', '<&>', '--settings=alternate_settings'] testproject_dir = os.path.join(test_dir, 'project_dir') os.mkdir(testproject_dir) - out, err = self.run_manage(args) self.addCleanup(shutil.rmtree, testproject_dir) + out, err = self.run_manage(args) self.assertNoOutput(err) test_manage_py = os.path.join(testproject_dir, 'additional_dir', 'extra.py') with open(test_manage_py, 'r') as fp: content = fp.read() self.assertIn("<&>", content) - # tidy up alternate settings - self.remove_settings('alternate_settings.py') def test_custom_project_destination_missing(self): """ @@ -1596,9 +1597,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template') args = ['startproject', '--template', template_path, '--extension=txt', 'customtestproject'] testproject_dir = os.path.join(test_dir, 'customtestproject') + self.addCleanup(shutil.rmtree, testproject_dir, True) out, err = self.run_django_admin(args) - self.addCleanup(shutil.rmtree, testproject_dir) self.assertNoOutput(err) self.assertTrue(os.path.isdir(testproject_dir)) path = os.path.join(testproject_dir, 'ticket-18091-non-ascii-template.txt') @@ -1612,8 +1613,8 @@ class DiffSettings(AdminScriptTestCase): def test_basic(self): "Runs without error and emits settings diff." self.write_settings('settings_to_diff.py', sdict={'FOO': '"bar"'}) + self.addCleanup(self.remove_settings, 'settings_to_diff.py') args = ['diffsettings', '--settings=settings_to_diff'] out, err = self.run_manage(args) - self.remove_settings('settings_to_diff.py') self.assertNoOutput(err) self.assertOutput(out, "FOO = 'bar' ###") diff --git a/tests/regressiontests/admin_scripts/urls.py b/tests/regressiontests/admin_scripts/urls.py index 692638ceca..a45dc3e9a6 100644 --- a/tests/regressiontests/admin_scripts/urls.py +++ b/tests/regressiontests/admin_scripts/urls.py @@ -1,7 +1,8 @@ import os from django.conf.urls import patterns +from django.utils._os import upath -here = os.path.dirname(__file__) +here = os.path.dirname(upath(__file__)) urlpatterns = patterns('', (r'^custom_templates/(?P<path>.*)$', 'django.views.static.serve', { diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index ef8a91d1db..e9e122a9f0 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -274,6 +274,10 @@ class UtilTests(unittest.TestCase): six.text_type(log_entry).startswith('Deleted ') ) + # Make sure custom action_flags works + log_entry.action_flag = 4 + self.assertEqual(six.text_type(log_entry), 'LogEntry Object') + def test_safestring_in_field_label(self): # safestring should not be escaped class MyForm(forms.Form): diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py index a5476e9eb7..1e4e9b5c75 100644 --- a/tests/regressiontests/admin_views/admin.py +++ b/tests/regressiontests/admin_views/admin.py @@ -27,7 +27,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, - RelatedPrepopulated, UndeletableObject, Simple) + RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice) def callable_year(dt_value): @@ -346,7 +346,10 @@ class LinkInline(admin.TabularInline): model = Link extra = 1 - readonly_fields = ("posted",) + readonly_fields = ("posted", "multiline") + + def multiline(self, instance): + return "InlineMultiline\ntest\nstring" class SubPostInline(admin.TabularInline): @@ -388,7 +391,10 @@ class PrePopulatedPostAdmin(admin.ModelAdmin): class PostAdmin(admin.ModelAdmin): list_display = ['title', 'public'] - readonly_fields = ('posted', 'awesomeness_level', 'coolness', 'value', lambda obj: "foo") + readonly_fields = ( + 'posted', 'awesomeness_level', 'coolness', 'value', 'multiline', + lambda obj: "foo" + ) inlines = [ LinkInline @@ -402,6 +408,10 @@ class PostAdmin(admin.ModelAdmin): def value(self, instance): return 1000 + + def multiline(self, instance): + return "Multiline\ntest\nstring" + value.short_description = 'Value in $US' @@ -585,6 +595,34 @@ def callable_on_unknown(obj): class AttributeErrorRaisingAdmin(admin.ModelAdmin): list_display = [callable_on_unknown, ] +class MessageTestingAdmin(admin.ModelAdmin): + actions = ["message_debug", "message_info", "message_success", + "message_warning", "message_error", "message_extra_tags"] + + def message_debug(self, request, selected): + self.message_user(request, "Test debug", level="debug") + + def message_info(self, request, selected): + self.message_user(request, "Test info", level="info") + + def message_success(self, request, selected): + self.message_user(request, "Test success", level="success") + + def message_warning(self, request, selected): + self.message_user(request, "Test warning", level="warning") + + def message_error(self, request, selected): + self.message_user(request, "Test error", level="error") + + def message_extra_tags(self, request, selected): + self.message_user(request, "Test tags", extra_tags="extra_tag") + + +class ChoiceList(admin.ModelAdmin): + list_display = ['choice'] + readonly_fields = ['choice'] + fields = ['choice'] + site = admin.AdminSite(name="admin") site.register(Article, ArticleAdmin) @@ -660,6 +698,8 @@ site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin) site.register(AdminOrderedCallable, AdminOrderedCallableAdmin) site.register(Color2, CustomTemplateFilterColorAdmin) site.register(Simple, AttributeErrorRaisingAdmin) +site.register(UserMessenger, MessageTestingAdmin) +site.register(Choice, ChoiceList) # Register core models we need in our tests from django.contrib.auth.models import User, Group diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py index 031fb50f0f..d69d690af0 100644 --- a/tests/regressiontests/admin_views/customadmin.py +++ b/tests/regressiontests/admin_views/customadmin.py @@ -16,7 +16,7 @@ class Admin2(admin.AdminSite): login_form = forms.CustomAdminAuthenticationForm login_template = 'custom_admin/login.html' logout_template = 'custom_admin/logout.html' - index_template = 'custom_admin/index.html' + index_template = ['custom_admin/index.html'] # a list, to test fix for #18697 password_change_template = 'custom_admin/password_change_form.html' password_change_done_template = 'custom_admin/password_change_done.html' @@ -40,6 +40,10 @@ class UserLimitedAdmin(UserAdmin): return qs.filter(is_superuser=False) +class CustomPwdTemplateUserAdmin(UserAdmin): + change_user_password_template = ['admin/auth/user/change_password.html'] # a list, to test fix for #18697 + + site = Admin2(name="admin2") site.register(models.Article, base_admin.ArticleAdmin) @@ -50,3 +54,6 @@ site.register(models.ChapterXtra1, base_admin.ChapterXtra1Admin) site.register(User, UserLimitedAdmin) site.register(models.UndeletableObject, base_admin.UndeletableObjectAdmin) site.register(models.Simple, base_admin.AttributeErrorRaisingAdmin) + +simple_site = Admin2(name='admin4') +simple_site.register(User, CustomPwdTemplateUserAdmin) diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 2b143004d9..c641cfc0e6 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -651,8 +651,16 @@ class UndeletableObject(models.Model): """ name = models.CharField(max_length=255) +class UserMessenger(models.Model): + """ + Dummy class for testing message_user functions on ModelAdmin + """ class Simple(models.Model): """ Simple model with nothing on it for use in testing """ + +class Choice(models.Model): + choice = models.IntegerField(blank=True, null=True, + choices=((1, 'Yes'), (0, 'No'), (None, 'No opinion'))) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 72dc6a3f97..669695ea9b 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -33,6 +33,7 @@ from django.utils.cache import get_max_age from django.utils.encoding import iri_to_uri, force_bytes from django.utils.html import escape from django.utils.http import urlencode +from django.utils._os import upath from django.utils import six from django.test.utils import override_settings @@ -46,11 +47,11 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, - Simple, UndeletableObject) + Simple, UndeletableObject, Choice) ERROR_MESSAGE = "Please enter the correct username and password \ -for a staff account. Note that both fields are case-sensitive." +for a staff account. Note that both fields may be case-sensitive." @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -633,7 +634,7 @@ class AdminViewFormUrlTest(TestCase): Refs #17515. """ template_dirs = settings.TEMPLATE_DIRS + ( - os.path.join(os.path.dirname(__file__), 'templates'),) + os.path.join(os.path.dirname(upath(__file__)), 'templates'),) with self.settings(TEMPLATE_DIRS=template_dirs): response = self.client.get("/test_admin/admin/admin_views/color2/") self.assertTrue('custom_filter_template.html' in [t.name for t in response.templates]) @@ -770,7 +771,10 @@ class CustomModelAdminTest(AdminViewBasicTest): self.assertContains(response, 'Hello from a custom logout template') def testCustomAdminSiteIndexViewAndTemplate(self): - response = self.client.get('/test_admin/admin2/') + try: + response = self.client.get('/test_admin/admin2/') + except TypeError: + self.fail('AdminSite.index_template should accept a list of template paths') self.assertIsInstance(response, TemplateResponse) self.assertTemplateUsed(response, 'custom_admin/index.html') self.assertContains(response, 'Hello from a custom index template *bar*') @@ -792,6 +796,15 @@ class CustomModelAdminTest(AdminViewBasicTest): response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) self.assertEqual(response.content, b"Django is a magical pony!") + def test_pwd_change_custom_template(self): + self.client.login(username='super', password='secret') + su = User.objects.get(username='super') + try: + response = self.client.get('/test_admin/admin4/auth/user/%s/password/' % su.pk) + except TypeError: + self.fail('ModelAdmin.change_user_password_template should accept a list of template paths') + self.assertEqual(response.status_code, 200) + def get_perm(Model, perm): """Return the permission object, for the Model""" @@ -3149,6 +3162,11 @@ class ReadonlyTest(TestCase): self.assertContains(response, "Very awesome.") self.assertContains(response, "Unkown coolness.") self.assertContains(response, "foo") + + # Checks that multiline text in a readonly field gets <br /> tags + self.assertContains(response, "Multiline<br />test<br />string") + self.assertContains(response, "InlineMultiline<br />test<br />string") + self.assertContains(response, formats.localize(datetime.date.today() - datetime.timedelta(days=7)) ) @@ -3198,6 +3216,15 @@ class ReadonlyTest(TestCase): response = self.client.get('/test_admin/admin2/auth/user/%s/password/' % su.pk) self.assertEqual(response.status_code, 404) + def test_change_form_renders_correct_null_choice_value(self): + """ + Regression test for #17911. + """ + choice = Choice.objects.create(choice=None) + response = self.client.get('/test_admin/admin/admin_views/choice/%s/' % choice.pk) + self.assertContains(response, '<p>No opinion</p>', html=True) + self.assertNotContains(response, '<p>(None)</p>') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class RawIdFieldsTest(TestCase): @@ -3378,7 +3405,11 @@ class CSSTest(TestCase): def tearDown(self): self.client.logout() - def test_css_classes(self): + def test_field_prefix_css_classes(self): + """ + Ensure that fields have a CSS class name with a 'field-' prefix. + Refs #16371. + """ response = self.client.get('/test_admin/admin/admin_views/post/add/') # The main form @@ -3394,6 +3425,23 @@ class CSSTest(TestCase): self.assertContains(response, '<td class="field-url">') self.assertContains(response, '<td class="field-posted">') + def test_index_css_classes(self): + """ + Ensure that CSS class names are used for each app and model on the + admin index pages. + Refs #17050. + """ + # General index page + response = self.client.get("/test_admin/admin/") + self.assertContains(response, '<div class="app-admin_views module">') + self.assertContains(response, '<tr class="model-actor">') + self.assertContains(response, '<tr class="model-album">') + + # App index page + response = self.client.get("/test_admin/admin/admin_views/") + self.assertContains(response, '<div class="app-admin_views module">') + self.assertContains(response, '<tr class="model-actor">') + self.assertContains(response, '<tr class="model-album">') try: import docutils @@ -3693,3 +3741,61 @@ class AdminViewLogoutTest(TestCase): self.assertEqual(response.template_name, 'admin/login.html') self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/') self.assertContains(response, '<input type="hidden" name="next" value="/test_admin/admin/" />') + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class AdminUserMessageTest(TestCase): + urls = "regressiontests.admin_views.urls" + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def send_message(self, level): + """ + Helper that sends a post to the dummy test methods and asserts that a + message with the level has appeared in the response. + """ + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'message_%s' % level, + 'index': 0, + } + + response = self.client.post('/test_admin/admin/admin_views/usermessenger/', + action_data, follow=True) + self.assertContains(response, + '<li class="%s">Test %s</li>' % (level, level), + html=True) + + @override_settings(MESSAGE_LEVEL=10) # Set to DEBUG for this request + def test_message_debug(self): + self.send_message('debug') + + def test_message_info(self): + self.send_message('info') + + def test_message_success(self): + self.send_message('success') + + def test_message_warning(self): + self.send_message('warning') + + def test_message_error(self): + self.send_message('error') + + def test_message_extra_tags(self): + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'message_extra_tags', + 'index': 0, + } + + response = self.client.post('/test_admin/admin/admin_views/usermessenger/', + action_data, follow=True) + self.assertContains(response, + '<li class="extra_tag info">Test tags</li>', + html=True) diff --git a/tests/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py index da6e2cbcf9..441834cd2a 100644 --- a/tests/regressiontests/admin_views/urls.py +++ b/tests/regressiontests/admin_views/urls.py @@ -11,4 +11,5 @@ urlpatterns = patterns('', (r'^test_admin/admin/', include(admin.site.urls)), (r'^test_admin/admin2/', include(customadmin.site.urls)), (r'^test_admin/admin3/', include(admin.site.urls), dict(form_url='pony')), + (r'^test_admin/admin4/', include(customadmin.simple_site.urls)), ) diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py index 2e452798b7..2977b86f3e 100644 --- a/tests/regressiontests/admin_widgets/models.py +++ b/tests/regressiontests/admin_widgets/models.py @@ -20,6 +20,7 @@ class Member(models.Model): @python_2_unicode_compatible class Band(models.Model): name = models.CharField(max_length=100) + style = models.CharField(max_length=20) members = models.ManyToManyField(Member) def __str__(self): diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py index 4b115431c1..76aa9f448f 100644 --- a/tests/regressiontests/admin_widgets/tests.py +++ b/tests/regressiontests/admin_widgets/tests.py @@ -10,7 +10,7 @@ from django.contrib.admin import widgets from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase from django.core.files.storage import default_storage from django.core.files.uploadedfile import SimpleUploadedFile -from django.db.models import DateField +from django.db.models import CharField, DateField from django.test import TestCase as DjangoTestCase from django.test.utils import override_settings from django.utils import translation @@ -112,6 +112,23 @@ class AdminFormfieldForDBFieldTests(TestCase): self.assertFormfield(models.Event, 'start_date', forms.TextInput, formfield_overrides={DateField: {'widget': forms.TextInput}}) + def testFormfieldOverridesWidgetInstances(self): + """ + Test that widget instances in formfield_overrides are not shared between + different fields. (#19423) + """ + class BandAdmin(admin.ModelAdmin): + formfield_overrides = { + CharField: {'widget': forms.TextInput(attrs={'size':'10'})} + } + ma = BandAdmin(models.Band, admin.site) + f1 = ma.formfield_for_dbfield(models.Band._meta.get_field('name'), request=None) + f2 = ma.formfield_for_dbfield(models.Band._meta.get_field('style'), request=None) + self.assertNotEqual(f1.widget, f2.widget) + self.assertEqual(f1.widget.attrs['maxlength'], '100') + self.assertEqual(f2.widget.attrs['maxlength'], '20') + self.assertEqual(f2.widget.attrs['size'], '10') + def testFieldWithChoices(self): self.assertFormfield(models.Member, 'gender', forms.Select) @@ -266,6 +283,37 @@ class AdminSplitDateTimeWidgetTest(DjangoTestCase): ) +class AdminURLWidgetTest(DjangoTestCase): + def test_render(self): + w = widgets.AdminURLFieldWidget() + self.assertHTMLEqual( + conditional_escape(w.render('test', '')), + '<input class="vURLField" name="test" type="text" />' + ) + self.assertHTMLEqual( + conditional_escape(w.render('test', 'http://example.com')), + '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>' + ) + + def test_render_idn(self): + w = widgets.AdminURLFieldWidget() + self.assertHTMLEqual( + conditional_escape(w.render('test', 'http://example-äüö.com')), + '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>' + ) + + def test_render_quoting(self): + w = widgets.AdminURLFieldWidget() + self.assertHTMLEqual( + conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')), + '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/<sometag>some text</sometag></a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>' + ) + self.assertHTMLEqual( + conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')), + '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/<sometag>some text</sometag></a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>' + ) + + class AdminFileWidgetTest(DjangoTestCase): def test_render(self): band = models.Band.objects.create(name='Linkin Park') diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index af0f421502..596ebbfaec 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -470,7 +470,7 @@ class AggregationTests(TestCase): # Regression for #15709 - Ensure each group_by field only exists once # per query qs = Book.objects.values('publisher').annotate(max_pages=Max('pages')).order_by() - grouping, gb_params = qs.query.get_compiler(qs.db).get_grouping() + grouping, gb_params = qs.query.get_compiler(qs.db).get_grouping([]) self.assertEqual(len(grouping), 1) def test_duplicate_alias(self): @@ -889,3 +889,96 @@ class AggregationTests(TestCase): self.assertIs(qs.query.alias_map['aggregation_regress_book'].join_type, None) # Check that the query executes without problems. self.assertEqual(len(qs.exclude(publisher=-1)), 6) + + @skipUnlessDBFeature("allows_group_by_pk") + def test_aggregate_duplicate_columns(self): + # Regression test for #17144 + + results = Author.objects.annotate(num_contacts=Count('book_contact_set')) + + # There should only be one GROUP BY clause, for the `id` column. + # `name` and `age` should not be grouped on. + grouping, gb_params = results.query.get_compiler(using='default').get_grouping([]) + self.assertEqual(len(grouping), 1) + assert 'id' in grouping[0] + assert 'name' not in grouping[0] + assert 'age' not in grouping[0] + + # The query group_by property should also only show the `id`. + self.assertEqual(results.query.group_by, [('aggregation_regress_author', 'id')]) + + # Ensure that we get correct results. + self.assertEqual( + [(a.name, a.num_contacts) for a in results.order_by('name')], + [ + ('Adrian Holovaty', 1), + ('Brad Dayley', 1), + ('Jacob Kaplan-Moss', 0), + ('James Bennett', 1), + ('Jeffrey Forcier', 1), + ('Paul Bissex', 0), + ('Peter Norvig', 2), + ('Stuart Russell', 0), + ('Wesley J. Chun', 0), + ] + ) + + @skipUnlessDBFeature("allows_group_by_pk") + def test_aggregate_duplicate_columns_only(self): + # Works with only() too. + results = Author.objects.only('id', 'name').annotate(num_contacts=Count('book_contact_set')) + grouping, gb_params = results.query.get_compiler(using='default').get_grouping([]) + self.assertEqual(len(grouping), 1) + assert 'id' in grouping[0] + assert 'name' not in grouping[0] + assert 'age' not in grouping[0] + + # The query group_by property should also only show the `id`. + self.assertEqual(results.query.group_by, [('aggregation_regress_author', 'id')]) + + # Ensure that we get correct results. + self.assertEqual( + [(a.name, a.num_contacts) for a in results.order_by('name')], + [ + ('Adrian Holovaty', 1), + ('Brad Dayley', 1), + ('Jacob Kaplan-Moss', 0), + ('James Bennett', 1), + ('Jeffrey Forcier', 1), + ('Paul Bissex', 0), + ('Peter Norvig', 2), + ('Stuart Russell', 0), + ('Wesley J. Chun', 0), + ] + ) + + @skipUnlessDBFeature("allows_group_by_pk") + def test_aggregate_duplicate_columns_select_related(self): + # And select_related() + results = Book.objects.select_related('contact').annotate( + num_authors=Count('authors')) + grouping, gb_params = results.query.get_compiler(using='default').get_grouping([]) + self.assertEqual(len(grouping), 1) + assert 'id' in grouping[0] + assert 'name' not in grouping[0] + assert 'contact' not in grouping[0] + + # The query group_by property should also only show the `id`. + self.assertEqual(results.query.group_by, [('aggregation_regress_book', 'id')]) + + # Ensure that we get correct results. + self.assertEqual( + [(b.name, b.num_authors) for b in results.order_by('name')], + [ + ('Artificial Intelligence: A Modern Approach', 2), + ('Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp', 1), + ('Practical Django Projects', 1), + ('Python Web Development with Django', 3), + ('Sams Teach Yourself Django in 24 Hours', 1), + ('The Definitive Guide to Django: Web Development Done Right', 2) + ] + ) + + def test_reverse_join_trimming(self): + qs = Author.objects.annotate(Count('book_contact_set__contact')) + self.assertIn(' JOIN ', str(qs.query)) diff --git a/tests/regressiontests/app_loading/tests.py b/tests/regressiontests/app_loading/tests.py index 0e66a5aad3..6dd0be2194 100644 --- a/tests/regressiontests/app_loading/tests.py +++ b/tests/regressiontests/app_loading/tests.py @@ -7,13 +7,14 @@ import time from django.conf import Settings from django.db.models.loading import cache, load_app, get_model, get_models +from django.utils._os import upath from django.utils.unittest import TestCase class EggLoadingTest(TestCase): def setUp(self): self.old_path = sys.path[:] - self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__)) # This test adds dummy applications to the app cache. These # need to be removed in order to prevent bad interactions diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index d284cfaac8..934c37d147 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -23,9 +23,28 @@ from django.utils import unittest from . import models +class DummyBackendTest(TestCase): + def test_no_databases(self): + """ + Test that empty DATABASES setting default to the dummy backend. + """ + DATABASES = {} + conns = ConnectionHandler(DATABASES) + self.assertEqual(conns[DEFAULT_DB_ALIAS].settings_dict['ENGINE'], + 'django.db.backends.dummy') + + class OracleChecks(unittest.TestCase): @unittest.skipUnless(connection.vendor == 'oracle', + "No need to check Oracle quote_name semantics") + def test_quote_name(self): + # Check that '%' chars are escaped for query execution. + name = '"SOME%NAME"' + quoted_name = connection.ops.quote_name(name) + self.assertEquals(quoted_name % (), name) + + @unittest.skipUnless(connection.vendor == 'oracle', "No need to check Oracle cursor semantics") def test_dbms_session(self): # If the backend is Oracle, test that we can call a standard @@ -540,21 +559,30 @@ class ThreadTests(TestCase): """ connections_set = set() connection.cursor() - connections_set.add(connection.connection) + connections_set.add(connection) def runner(): - from django.db import connection + # Passing django.db.connection between threads doesn't work while + # connections[DEFAULT_DB_ALIAS] does. + from django.db import connections + connection = connections[DEFAULT_DB_ALIAS] + # Allow thread sharing so the connection can be closed by the + # main thread. + connection.allow_thread_sharing = True connection.cursor() - connections_set.add(connection.connection) + connections_set.add(connection) for x in range(2): t = threading.Thread(target=runner) t.start() t.join() - self.assertEqual(len(connections_set), 3) + # Check that each created connection got different inner connection. + self.assertEqual( + len(set([conn.connection for conn in connections_set])), + 3) # Finish by closing the connections opened by the other threads (the # connection opened in the main thread will automatically be closed on # teardown). for conn in connections_set: - if conn != connection.connection: + if conn is not connection: conn.close() def test_connections_thread_local(self): @@ -581,7 +609,7 @@ class ThreadTests(TestCase): # connection opened in the main thread will automatically be closed on # teardown). for conn in connections_set: - if conn != connection: + if conn is not connection: conn.close() def test_pass_connection_between_threads(self): diff --git a/tests/regressiontests/bug639/tests.py b/tests/regressiontests/bug639/tests.py index b7547696d4..fcc1e0f7d1 100644 --- a/tests/regressiontests/bug639/tests.py +++ b/tests/regressiontests/bug639/tests.py @@ -11,6 +11,7 @@ import shutil from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import unittest +from django.utils._os import upath from .models import Photo, PhotoForm, temp_storage_dir @@ -23,7 +24,7 @@ class Bug639Test(unittest.TestCase): called. """ # Grab an image for testing. - filename = os.path.join(os.path.dirname(__file__), "test.jpg") + filename = os.path.join(os.path.dirname(upath(__file__)), "test.jpg") with open(filename, "rb") as fp: img = fp.read() diff --git a/tests/regressiontests/bulk_create/tests.py b/tests/regressiontests/bulk_create/tests.py index 5d61242b9b..d4772934a1 100644 --- a/tests/regressiontests/bulk_create/tests.py +++ b/tests/regressiontests/bulk_create/tests.py @@ -82,6 +82,14 @@ class BulkCreateTests(TestCase): with self.assertRaises(ValueError): Country.objects.bulk_create([valid_country, invalid_country]) + def test_batch_same_vals(self): + # Sqlite had a problem where all the same-valued models were + # collapsed to one insert. + Restaurant.objects.bulk_create([ + Restaurant(name='foo') for i in range(0, 2) + ]) + self.assertEqual(Restaurant.objects.count(), 2) + def test_large_batch(self): with override_settings(DEBUG=True): connection.queries = [] @@ -95,6 +103,14 @@ class BulkCreateTests(TestCase): self.assertEqual(TwoFields.objects.filter(f2__gte=901).count(), 101) @skipUnlessDBFeature('has_bulk_insert') + def test_large_single_field_batch(self): + # SQLite had a problem with more than 500 UNIONed selects in single + # query. + Restaurant.objects.bulk_create([ + Restaurant() for i in range(0, 501) + ]) + + @skipUnlessDBFeature('has_bulk_insert') def test_large_batch_efficiency(self): with override_settings(DEBUG=True): connection.queries = [] diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index a6eff8950b..fc92ecf870 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -257,6 +257,7 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get('answer'), 42) self.assertEqual(self.cache.incr('answer', 10), 52) self.assertEqual(self.cache.get('answer'), 52) + self.assertEqual(self.cache.incr('answer', -10), 42) self.assertRaises(ValueError, self.cache.incr, 'does_not_exist') def test_decr(self): @@ -266,8 +267,13 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get('answer'), 42) self.assertEqual(self.cache.decr('answer', 10), 32) self.assertEqual(self.cache.get('answer'), 32) + self.assertEqual(self.cache.decr('answer', -10), 42) self.assertRaises(ValueError, self.cache.decr, 'does_not_exist') + def test_close(self): + self.assertTrue(hasattr(self.cache, 'close')) + self.cache.close() + def test_data_types(self): # Many different data types can be cached stuff = { diff --git a/tests/regressiontests/comment_tests/tests/app_api_tests.py b/tests/regressiontests/comment_tests/tests/app_api_tests.py index 8a63e8c188..a756068790 100644 --- a/tests/regressiontests/comment_tests/tests/app_api_tests.py +++ b/tests/regressiontests/comment_tests/tests/app_api_tests.py @@ -4,6 +4,9 @@ from django.conf import settings from django.contrib import comments from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm +from django.core.exceptions import ImproperlyConfigured +from django.test.utils import override_settings +from django.utils import six from . import CommentTestCase @@ -14,6 +17,14 @@ class CommentAppAPITests(CommentTestCase): def testGetCommentApp(self): self.assertEqual(comments.get_comment_app(), comments) + @override_settings( + COMMENTS_APP='missing_app', + INSTALLED_APPS=list(settings.INSTALLED_APPS) + ['missing_app'], + ) + def testGetMissingCommentApp(self): + with six.assertRaisesRegex(self, ImproperlyConfigured, 'missing_app'): + _ = comments.get_comment_app() + def testGetForm(self): self.assertEqual(comments.get_form(), CommentForm) @@ -33,20 +44,14 @@ class CommentAppAPITests(CommentTestCase): self.assertEqual(comments.get_approve_url(c), "/approve/12345/") +@override_settings( + COMMENTS_APP='regressiontests.comment_tests.custom_comments', + INSTALLED_APPS=list(settings.INSTALLED_APPS) + [ + 'regressiontests.comment_tests.custom_comments'], +) class CustomCommentTest(CommentTestCase): urls = 'regressiontests.comment_tests.urls' - def setUp(self): - self.old_comments_app = getattr(settings, 'COMMENTS_APP', None) - settings.COMMENTS_APP = 'regressiontests.comment_tests.custom_comments' - settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + [settings.COMMENTS_APP,] - - def tearDown(self): - del settings.INSTALLED_APPS[-1] - settings.COMMENTS_APP = self.old_comments_app - if settings.COMMENTS_APP is None: - del settings._wrapped.COMMENTS_APP - def testGetCommentApp(self): from regressiontests.comment_tests import custom_comments self.assertEqual(comments.get_comment_app(), custom_comments) diff --git a/tests/regressiontests/comment_tests/tests/comment_view_tests.py b/tests/regressiontests/comment_tests/tests/comment_view_tests.py index 429f3b2bf2..5c1954026d 100644 --- a/tests/regressiontests/comment_tests/tests/comment_view_tests.py +++ b/tests/regressiontests/comment_tests/tests/comment_view_tests.py @@ -222,6 +222,13 @@ class CommentViewTests(CommentTestCase): match = re.search(r"^http://testserver/somewhere/else/\?c=\d+$", location) self.assertTrue(match != None, "Unexpected redirect location: %s" % location) + data["next"] = "http://badserver/somewhere/else/" + data["comment"] = "This is another comment with an unsafe next url" + response = self.client.post("/post/", data) + location = response["Location"] + match = post_redirect_re.match(location) + self.assertTrue(match != None, "Unsafe redirection to: %s" % location) + def testCommentDoneView(self): a = Article.objects.get(pk=1) data = self.getValidData(a) diff --git a/tests/regressiontests/comment_tests/tests/feed_tests.py b/tests/regressiontests/comment_tests/tests/feed_tests.py index 1ec316eab8..c93d7abf7d 100644 --- a/tests/regressiontests/comment_tests/tests/feed_tests.py +++ b/tests/regressiontests/comment_tests/tests/feed_tests.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +from xml.etree import ElementTree as ET + from django.conf import settings from django.contrib.comments.models import Comment from django.contrib.contenttypes.models import ContentType @@ -31,8 +33,21 @@ class CommentFeedTests(CommentTestCase): response = self.client.get(self.feed_url) self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'], 'application/rss+xml; charset=utf-8') - self.assertContains(response, '<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">') - self.assertContains(response, '<title>example.com comments</title>') - self.assertContains(response, '<link>http://example.com/</link>') - self.assertContains(response, '</rss>') + + rss_elem = ET.fromstring(response.content) + + self.assertEqual(rss_elem.tag, "rss") + self.assertEqual(rss_elem.attrib, {"version": "2.0"}) + + channel_elem = rss_elem.find("channel") + + title_elem = channel_elem.find("title") + self.assertEqual(title_elem.text, "example.com comments") + + link_elem = channel_elem.find("link") + self.assertEqual(link_elem.text, "http://example.com/") + + atomlink_elem = channel_elem.find("{http://www.w3.org/2005/Atom}link") + self.assertEqual(atomlink_elem.attrib, {"href": "http://example.com/rss/comments/", "rel": "self"}) + self.assertNotContains(response, "A comment for the second site.") diff --git a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py index 2ada20a662..0f83d5e210 100644 --- a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py +++ b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py @@ -4,7 +4,6 @@ from django.contrib.auth.models import User, Permission from django.contrib.comments import signals from django.contrib.comments.models import Comment, CommentFlag from django.contrib.contenttypes.models import ContentType -from django.conf import settings from . import CommentTestCase @@ -30,6 +29,30 @@ class FlagViewTests(CommentTestCase): self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1) return c + def testFlagPostNext(self): + """ + POST the flag view, explicitly providing a next url. + """ + comments = self.createSomeComments() + pk = comments[0].pk + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/flag/%d/" % pk, {'next': "/go/here/"}) + self.assertEqual(response["Location"], + "http://testserver/go/here/?c=%d" % pk) + + def testFlagPostUnsafeNext(self): + """ + POSTing to the flag view with an unsafe next url will ignore the + provided url when redirecting. + """ + comments = self.createSomeComments() + pk = comments[0].pk + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/flag/%d/" % pk, + {'next': "http://elsewhere/bad"}) + self.assertEqual(response["Location"], + "http://testserver/flagged/?c=%d" % pk) + def testFlagPostTwice(self): """Users don't get to flag comments more than once.""" c = self.testFlagPost() @@ -49,7 +72,7 @@ class FlagViewTests(CommentTestCase): def testFlaggedView(self): comments = self.createSomeComments() pk = comments[0].pk - response = self.client.get("/flagged/", data={"c":pk}) + response = self.client.get("/flagged/", data={"c": pk}) self.assertTemplateUsed(response, "comments/flagged.html") def testFlagSignals(self): @@ -101,6 +124,33 @@ class DeleteViewTests(CommentTestCase): self.assertTrue(c.is_removed) self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_DELETION, user__username="normaluser").count(), 1) + def testDeletePostNext(self): + """ + POSTing the delete view will redirect to an explicitly provided a next + url. + """ + comments = self.createSomeComments() + pk = comments[0].pk + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/delete/%d/" % pk, {'next': "/go/here/"}) + self.assertEqual(response["Location"], + "http://testserver/go/here/?c=%d" % pk) + + def testDeletePostUnsafeNext(self): + """ + POSTing to the delete view with an unsafe next url will ignore the + provided url when redirecting. + """ + comments = self.createSomeComments() + pk = comments[0].pk + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/delete/%d/" % pk, + {'next': "http://elsewhere/bad"}) + self.assertEqual(response["Location"], + "http://testserver/deleted/?c=%d" % pk) + def testDeleteSignals(self): def receive(sender, **kwargs): received_signals.append(kwargs.get('signal')) @@ -116,13 +166,13 @@ class DeleteViewTests(CommentTestCase): def testDeletedView(self): comments = self.createSomeComments() pk = comments[0].pk - response = self.client.get("/deleted/", data={"c":pk}) + response = self.client.get("/deleted/", data={"c": pk}) self.assertTemplateUsed(response, "comments/deleted.html") class ApproveViewTests(CommentTestCase): def testApprovePermissions(self): - """The delete view should only be accessible to 'moderators'""" + """The approve view should only be accessible to 'moderators'""" comments = self.createSomeComments() pk = comments[0].pk self.client.login(username="normaluser", password="normaluser") @@ -134,7 +184,7 @@ class ApproveViewTests(CommentTestCase): self.assertEqual(response.status_code, 200) def testApprovePost(self): - """POSTing the delete view should mark the comment as removed""" + """POSTing the approve view should mark the comment as removed""" c1, c2, c3, c4 = self.createSomeComments() c1.is_public = False; c1.save() @@ -146,6 +196,36 @@ class ApproveViewTests(CommentTestCase): self.assertTrue(c.is_public) self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_APPROVAL, user__username="normaluser").count(), 1) + def testApprovePostNext(self): + """ + POSTing the approve view will redirect to an explicitly provided a next + url. + """ + c1, c2, c3, c4 = self.createSomeComments() + c1.is_public = False; c1.save() + + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/approve/%d/" % c1.pk, + {'next': "/go/here/"}) + self.assertEqual(response["Location"], + "http://testserver/go/here/?c=%d" % c1.pk) + + def testApprovePostUnsafeNext(self): + """ + POSTing to the approve view with an unsafe next url will ignore the + provided url when redirecting. + """ + c1, c2, c3, c4 = self.createSomeComments() + c1.is_public = False; c1.save() + + makeModerator("normaluser") + self.client.login(username="normaluser", password="normaluser") + response = self.client.post("/approve/%d/" % c1.pk, + {'next': "http://elsewhere/bad"}) + self.assertEqual(response["Location"], + "http://testserver/approved/?c=%d" % c1.pk) + def testApproveSignals(self): def receive(sender, **kwargs): received_signals.append(kwargs.get('signal')) diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index bdf3c867c6..8596f8c801 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -80,13 +80,16 @@ class DefaultFiltersTests(TestCase): decimal_ctx.prec = old_prec - # This fails because of Python's float handling. Floats with many zeroes - # after the decimal point should be passed in as another type such as - # unicode or Decimal. - @unittest.expectedFailure - def test_floatformat_fail(self): + def test_floatformat_py2_fail(self): self.assertEqual(floatformat(1.00000000000000015, 16), '1.0000000000000002') + # The test above fails because of Python 2's float handling. Floats with + # many zeroes after the decimal point should be passed in as another type + # such as unicode or Decimal. + if not six.PY3: + test_floatformat_py2_fail = unittest.expectedFailure(test_floatformat_py2_fail) + + def test_addslashes(self): self.assertEqual(addslashes('"double quotes" and \'single quotes\''), '\\"double quotes\\" and \\\'single quotes\\\'') @@ -301,7 +304,16 @@ class DefaultFiltersTests(TestCase): # Check urlize trims trailing period when followed by parenthesis - see #18644 self.assertEqual(urlize('(Go to http://www.example.com/foo.)'), - '(Go to <a href="http://www.example.com/foo" rel="nofollow">http://www.example.com/foo</a>.)') + '(Go to <a href="http://www.example.com/foo" rel="nofollow">http://www.example.com/foo</a>.)') + + # Check urlize doesn't crash when square bracket is appended to url (#19070) + self.assertEqual(urlize('[see www.example.com]'), + '[see <a href="http://www.example.com" rel="nofollow">www.example.com</a>]' ) + + # Check urlize doesn't crash when square bracket is prepended to url (#19070) + self.assertEqual(urlize('see test[at[example.com'), + 'see <a href="http://test[at[example.com" rel="nofollow">test[at[example.com</a>' ) + def test_wordcount(self): self.assertEqual(wordcount(''), 0) diff --git a/tests/regressiontests/defer_regress/models.py b/tests/regressiontests/defer_regress/models.py index 444649ac92..7528e3b2c9 100644 --- a/tests/regressiontests/defer_regress/models.py +++ b/tests/regressiontests/defer_regress/models.py @@ -55,6 +55,10 @@ class Feature(models.Model): class SpecialFeature(models.Model): feature = models.ForeignKey(Feature) +class OneToOneItem(models.Model): + item = models.OneToOneField(Item, related_name="one_to_one_item") + name = models.CharField(max_length=15) + class ItemAndSimpleItem(models.Model): item = models.ForeignKey(Item) simple = models.ForeignKey(SimpleItem) diff --git a/tests/regressiontests/defer_regress/tests.py b/tests/regressiontests/defer_regress/tests.py index c77ca32135..d4d722035f 100644 --- a/tests/regressiontests/defer_regress/tests.py +++ b/tests/regressiontests/defer_regress/tests.py @@ -9,7 +9,7 @@ from django.db.models.loading import cache from django.test import TestCase from .models import (ResolveThis, Item, RelatedItem, Child, Leaf, Proxy, - SimpleItem, Feature, ItemAndSimpleItem, SpecialFeature) + SimpleItem, Feature, ItemAndSimpleItem, OneToOneItem, SpecialFeature) class DeferRegressionTest(TestCase): @@ -111,6 +111,7 @@ class DeferRegressionTest(TestCase): Item, ItemAndSimpleItem, Leaf, + OneToOneItem, Proxy, RelatedItem, ResolveThis, @@ -147,6 +148,7 @@ class DeferRegressionTest(TestCase): "Leaf_Deferred_name_value", "Leaf_Deferred_second_child_id_value", "Leaf_Deferred_value", + "OneToOneItem", "Proxy", "RelatedItem", "RelatedItem_Deferred_", @@ -182,6 +184,33 @@ class DeferRegressionTest(TestCase): self.assertEqual(1, qs.count()) self.assertEqual('Foobar', qs[0].name) + def test_reverse_one_to_one_relations(self): + # Refs #14694. Test reverse relations which are known unique (reverse + # side has o2ofield or unique FK) - the o2o case + item = Item.objects.create(name="first", value=42) + o2o = OneToOneItem.objects.create(item=item, name="second") + self.assertEqual(len(Item.objects.defer('one_to_one_item__name')), 1) + self.assertEqual(len(Item.objects.select_related('one_to_one_item')), 1) + self.assertEqual(len(Item.objects.select_related( + 'one_to_one_item').defer('one_to_one_item__name')), 1) + self.assertEqual(len(Item.objects.select_related('one_to_one_item').defer('value')), 1) + # Make sure that `only()` doesn't break when we pass in a unique relation, + # rather than a field on the relation. + self.assertEqual(len(Item.objects.only('one_to_one_item')), 1) + with self.assertNumQueries(1): + i = Item.objects.select_related('one_to_one_item')[0] + self.assertEqual(i.one_to_one_item.pk, o2o.pk) + self.assertEqual(i.one_to_one_item.name, "second") + with self.assertNumQueries(1): + i = Item.objects.select_related('one_to_one_item').defer( + 'value', 'one_to_one_item__name')[0] + self.assertEqual(i.one_to_one_item.pk, o2o.pk) + self.assertEqual(i.name, "first") + with self.assertNumQueries(1): + self.assertEqual(i.one_to_one_item.name, "second") + with self.assertNumQueries(1): + self.assertEqual(i.value, 42) + def test_defer_with_select_related(self): item1 = Item.objects.create(name="first", value=47) item2 = Item.objects.create(name="second", value=42) diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py index f94bb2f20c..c3e4a6a0b7 100644 --- a/tests/regressiontests/delete_regress/tests.py +++ b/tests/regressiontests/delete_regress/tests.py @@ -3,7 +3,8 @@ from __future__ import absolute_import import datetime from django.conf import settings -from django.db import backend, transaction, DEFAULT_DB_ALIAS, models +from django.db import transaction, DEFAULT_DB_ALIAS, models +from django.db.utils import ConnectionHandler from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith, @@ -17,17 +18,8 @@ from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith, class DeleteLockingTest(TransactionTestCase): def setUp(self): # Create a second connection to the default database - conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS] - self.conn2 = backend.DatabaseWrapper({ - 'HOST': conn_settings['HOST'], - 'NAME': conn_settings['NAME'], - 'OPTIONS': conn_settings['OPTIONS'], - 'PASSWORD': conn_settings['PASSWORD'], - 'PORT': conn_settings['PORT'], - 'USER': conn_settings['USER'], - 'TIME_ZONE': settings.TIME_ZONE, - }) - + new_connections = ConnectionHandler(settings.DATABASES) + self.conn2 = new_connections[DEFAULT_DB_ALIAS] # Put both DB connections into managed transaction mode transaction.enter_transaction_management() transaction.managed(True) diff --git a/tests/regressiontests/expressions_regress/tests.py b/tests/regressiontests/expressions_regress/tests.py index 508a497151..ddb2c83b4f 100644 --- a/tests/regressiontests/expressions_regress/tests.py +++ b/tests/regressiontests/expressions_regress/tests.py @@ -26,12 +26,13 @@ class ExpressionsRegressTests(TestCase): same object. """ self.assertQuerysetEqual( - Number.objects.all(), - [ - '<Number: -1, -1.000>', - '<Number: 42, 42.000>', - '<Number: 1337, 1337.000>' - ] + Number.objects.all(), + [ + '<Number: -1, -1.000>', + '<Number: 42, 42.000>', + '<Number: 1337, 1337.000>' + ], + ordered=False ) def test_increment_value(self): @@ -44,12 +45,13 @@ class ExpressionsRegressTests(TestCase): 2) self.assertQuerysetEqual( - Number.objects.all(), - [ - '<Number: -1, -1.000>', - '<Number: 43, 42.000>', - '<Number: 1338, 1337.000>' - ] + Number.objects.all(), + [ + '<Number: -1, -1.000>', + '<Number: 43, 42.000>', + '<Number: 1338, 1337.000>' + ], + ordered=False ) def test_filter_not_equals_other_field(self): @@ -62,11 +64,12 @@ class ExpressionsRegressTests(TestCase): .update(integer=F('integer') + 1), 2) self.assertQuerysetEqual( - Number.objects.exclude(float=F('integer')), - [ - '<Number: 43, 42.000>', - '<Number: 1338, 1337.000>' - ] + Number.objects.exclude(float=F('integer')), + [ + '<Number: 43, 42.000>', + '<Number: 1338, 1337.000>' + ], + ordered=False ) def test_complex_expressions(self): diff --git a/tests/regressiontests/extra_regress/tests.py b/tests/regressiontests/extra_regress/tests.py index f591900afe..1bc6789edd 100644 --- a/tests/regressiontests/extra_regress/tests.py +++ b/tests/regressiontests/extra_regress/tests.py @@ -58,13 +58,15 @@ class ExtraRegressTests(TestCase): ('First Revision', 'First Revision'), ('Second Revision', 'First Revision'), ], - transform=lambda r: (r.title, r.base.title) + transform=lambda r: (r.title, r.base.title), + ordered=False ) # Following queryset should return the most recent revision: self.assertQuerysetEqual(qs & qs2, [('Second Revision', 'First Revision')], - transform=lambda r: (r.title, r.base.title) + transform=lambda r: (r.title, r.base.title), + ordered=False ) def test_extra_stay_tied(self): @@ -342,5 +344,6 @@ class ExtraRegressTests(TestCase): TestObject.objects.extra( where=["first = 'a' OR second = 'a'", "third = 'a'"], ), - ['<TestObject: TestObject: a,a,a>', '<TestObject: TestObject: b,a,a>'] + ['<TestObject: TestObject: a,a,a>', '<TestObject: TestObject: b,a,a>'], + ordered=False ) diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 595b65d9f1..b6d3e1ff0b 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -24,6 +24,7 @@ from django.core.files.uploadedfile import UploadedFile from django.test import SimpleTestCase from django.utils import six from django.utils import unittest +from django.utils._os import upath from django.test.utils import override_settings from ..servers.tests import LiveServerBase @@ -104,7 +105,7 @@ class FileStorageTests(unittest.TestCase): """ storage = self.storage_class(location='') self.assertEqual(storage.base_location, '') - self.assertEqual(storage.location, os.getcwd()) + self.assertEqual(storage.location, upath(os.getcwd())) def test_file_access_options(self): """ @@ -534,7 +535,7 @@ class DimensionClosingBug(unittest.TestCase): from django.core.files import images images.open = catching_open try: - get_image_dimensions(os.path.join(os.path.dirname(__file__), "test1.png")) + get_image_dimensions(os.path.join(os.path.dirname(upath(__file__)), "test1.png")) finally: del images.open self.assertTrue(FileWrapper._closed) @@ -551,7 +552,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): """ from django.core.files.images import ImageFile - img_path = os.path.join(os.path.dirname(__file__), "test.png") + img_path = os.path.join(os.path.dirname(upath(__file__)), "test.png") image = ImageFile(open(img_path, 'rb')) image_pil = Image.open(img_path) size_1, size_2 = get_image_dimensions(image), get_image_dimensions(image) @@ -560,6 +561,13 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): class ContentFileTestCase(unittest.TestCase): + def setUp(self): + self.storage_dir = tempfile.mkdtemp() + self.storage = FileSystemStorage(self.storage_dir) + + def tearDown(self): + shutil.rmtree(self.storage_dir) + def test_content_file_default_name(self): self.assertEqual(ContentFile(b"content").name, None) @@ -576,7 +584,18 @@ class ContentFileTestCase(unittest.TestCase): retrieved content is of the same type. """ self.assertTrue(isinstance(ContentFile(b"content").read(), bytes)) - self.assertTrue(isinstance(ContentFile("español").read(), six.text_type)) + if six.PY3: + self.assertTrue(isinstance(ContentFile("español").read(), six.text_type)) + else: + self.assertTrue(isinstance(ContentFile("español").read(), bytes)) + + def test_content_saving(self): + """ + Test that ContentFile can be saved correctly with the filesystem storage, + both if it was initialized with string or unicode content""" + self.storage.save('bytes.txt', ContentFile(b"content")) + self.storage.save('unicode.txt', ContentFile("español")) + class NoNameFileTestCase(unittest.TestCase): """ diff --git a/tests/regressiontests/file_uploads/models.py b/tests/regressiontests/file_uploads/models.py index 28e71c44bc..3336cc9d90 100644 --- a/tests/regressiontests/file_uploads/models.py +++ b/tests/regressiontests/file_uploads/models.py @@ -1,12 +1,5 @@ -import tempfile -import os - -from django.core.files.storage import FileSystemStorage from django.db import models -temp_storage = FileSystemStorage(tempfile.mkdtemp()) -UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload') - class FileModel(models.Model): - testfile = models.FileField(storage=temp_storage, upload_to='test_upload') + testfile = models.FileField(upload_to='test_upload') diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py index 918f77d73c..45e7342abd 100644 --- a/tests/regressiontests/file_uploads/tests.py +++ b/tests/regressiontests/file_uploads/tests.py @@ -7,22 +7,36 @@ import hashlib import json import os import shutil +import tempfile as sys_tempfile from django.core.files import temp as tempfile from django.core.files.uploadedfile import SimpleUploadedFile from django.http.multipartparser import MultiPartParser from django.test import TestCase, client +from django.test.utils import override_settings from django.utils.encoding import force_bytes from django.utils.six import StringIO from django.utils import unittest from . import uploadhandler -from .models import FileModel, temp_storage, UPLOAD_TO +from .models import FileModel UNICODE_FILENAME = 'test-0123456789_中文_Orléans.jpg' +MEDIA_ROOT = sys_tempfile.mkdtemp() +UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload') +@override_settings(MEDIA_ROOT=MEDIA_ROOT) class FileUploadTests(TestCase): + @classmethod + def setUpClass(cls): + if not os.path.isdir(MEDIA_ROOT): + os.makedirs(MEDIA_ROOT) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(MEDIA_ROOT) + def test_simple_upload(self): with open(__file__, 'rb') as fp: post_data = { @@ -60,15 +74,14 @@ class FileUploadTests(TestCase): self.assertEqual(response.status_code, 200) - def test_base64_upload(self): - test_string = "This data will be transmitted base64-encoded." + def _test_base64_upload(self, content): payload = client.FakePayload("\r\n".join([ '--' + client.BOUNDARY, 'Content-Disposition: form-data; name="file"; filename="test.txt"', 'Content-Type: application/octet-stream', 'Content-Transfer-Encoding: base64', '',])) - payload.write(b"\r\n" + base64.b64encode(force_bytes(test_string)) + b"\r\n") + payload.write(b"\r\n" + base64.b64encode(force_bytes(content)) + b"\r\n") payload.write('--' + client.BOUNDARY + '--\r\n') r = { 'CONTENT_LENGTH': len(payload), @@ -80,10 +93,17 @@ class FileUploadTests(TestCase): response = self.client.request(**r) received = json.loads(response.content.decode('utf-8')) - self.assertEqual(received['file'], test_string) + self.assertEqual(received['file'], content) + + def test_base64_upload(self): + self._test_base64_upload("This data will be transmitted base64-encoded.") + + def test_big_base64_upload(self): + self._test_base64_upload("Big data" * 68000) # > 512Kb def test_unicode_file_name(self): - tdir = tempfile.gettempdir() + tdir = sys_tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, tdir, True) # This file contains chinese symbols and an accented char in the name. with open(os.path.join(tdir, UNICODE_FILENAME), 'w+b') as file1: @@ -96,11 +116,6 @@ class FileUploadTests(TestCase): response = self.client.post('/file_uploads/unicode_name/', post_data) - try: - os.unlink(file1.name) - except OSError: - pass - self.assertEqual(response.status_code, 200) def test_dangerous_file_names(self): @@ -347,26 +362,28 @@ class FileUploadTests(TestCase): # shouldn't differ. self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt') -class DirectoryCreationTests(unittest.TestCase): +@override_settings(MEDIA_ROOT=MEDIA_ROOT) +class DirectoryCreationTests(TestCase): """ Tests for error handling during directory creation via _save_FIELD_file (ticket #6450) """ + @classmethod + def setUpClass(cls): + if not os.path.isdir(MEDIA_ROOT): + os.makedirs(MEDIA_ROOT) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(MEDIA_ROOT) + def setUp(self): self.obj = FileModel() - if not os.path.isdir(temp_storage.location): - os.makedirs(temp_storage.location) - if os.path.isdir(UPLOAD_TO): - os.chmod(UPLOAD_TO, 0o700) - shutil.rmtree(UPLOAD_TO) - - def tearDown(self): - os.chmod(temp_storage.location, 0o700) - shutil.rmtree(temp_storage.location) def test_readonly_root(self): """Permission errors are not swallowed""" - os.chmod(temp_storage.location, 0o500) + os.chmod(MEDIA_ROOT, 0o500) + self.addCleanup(os.chmod, MEDIA_ROOT, 0o700) try: self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x')) except OSError as err: @@ -378,6 +395,7 @@ class DirectoryCreationTests(unittest.TestCase): """The correct IOError is raised when the upload directory name exists but isn't a directory""" # Create a file with the upload directory name open(UPLOAD_TO, 'wb').close() + self.addCleanup(os.remove, UPLOAD_TO) with self.assertRaises(IOError) as exc_info: self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x')) # The test needs to be done on a specific string as IOError diff --git a/tests/regressiontests/file_uploads/views.py b/tests/regressiontests/file_uploads/views.py index fcf32cecea..eb7b654c09 100644 --- a/tests/regressiontests/file_uploads/views.py +++ b/tests/regressiontests/file_uploads/views.py @@ -9,8 +9,8 @@ from django.http import HttpResponse, HttpResponseServerError from django.utils import six from django.utils.encoding import force_bytes -from .models import FileModel, UPLOAD_TO -from .tests import UNICODE_FILENAME +from .models import FileModel +from .tests import UNICODE_FILENAME, UPLOAD_TO from .uploadhandler import QuotaUploadHandler, ErroringUploadHandler diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index 55363bc5b7..988c5acd0c 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -14,6 +14,8 @@ from django.db.models import signals from django.test import (TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature) from django.test.utils import override_settings +from django.utils.encoding import force_text +from django.utils._os import upath from django.utils import six from django.utils.six import PY3, StringIO @@ -126,7 +128,7 @@ class TestFixtures(TestCase): fixture directory. """ load_absolute_path = os.path.join( - os.path.dirname(__file__), + os.path.dirname(upath(__file__)), 'fixtures', 'absolute.json' ) @@ -388,7 +390,7 @@ class TestFixtures(TestCase): commit=False, ) - _cur_dir = os.path.dirname(os.path.abspath(__file__)) + _cur_dir = os.path.dirname(os.path.abspath(upath(__file__))) @override_settings(FIXTURE_DIRS=[os.path.join(_cur_dir, 'fixtures_1'), os.path.join(_cur_dir, 'fixtures_2')]) @@ -430,7 +432,7 @@ class TestFixtures(TestCase): stdout=stdout_output, ) self.assertTrue("No xml fixture 'this_fixture_doesnt_exist' in" in - stdout_output.getvalue()) + force_text(stdout_output.getvalue())) class NaturalKeyFixtureTests(TestCase): diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index 6e9c269356..bec31d12d7 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -83,4 +83,8 @@ class Group(models.Model): class Cheese(models.Model): - name = models.CharField(max_length=100) + name = models.CharField(max_length=100) + + +class Article(models.Model): + content = models.TextField() diff --git a/tests/regressiontests/forms/templates/forms/article_form.html b/tests/regressiontests/forms/templates/forms/article_form.html new file mode 100644 index 0000000000..cde85051f6 --- /dev/null +++ b/tests/regressiontests/forms/templates/forms/article_form.html @@ -0,0 +1,8 @@ +<html> +<body> + <form method="post" action="."> + {{ form.as_p }}<br> + <input id="submit" type="submit"> + </form> +</body> +</html> diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py index 8e2150cea3..6708e54c79 100644 --- a/tests/regressiontests/forms/tests/__init__.py +++ b/tests/regressiontests/forms/tests/__init__.py @@ -18,4 +18,4 @@ from .regressions import FormsRegressionsTestCase from .util import FormsUtilTestCase from .validators import TestFieldWithValidators from .widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase, - WidgetTests, ClearableFileInputTests) + WidgetTests, LiveWidgetTests, ClearableFileInputTests) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 1027afceb1..e17d976fcf 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -36,6 +36,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.test import SimpleTestCase from django.utils import six +from django.utils._os import upath def fix_os_paths(x): @@ -928,12 +929,12 @@ class FieldsTests(SimpleTestCase): # FilePathField ############################################################### def test_filepathfield_1(self): - path = os.path.abspath(forms.__file__) + path = os.path.abspath(upath(forms.__file__)) path = os.path.dirname(path) + '/' self.assertTrue(fix_os_paths(path).endswith('/django/forms/')) def test_filepathfield_2(self): - path = forms.__file__ + path = upath(forms.__file__) path = os.path.dirname(os.path.abspath(path)) + '/' f = FilePathField(path=path) f.choices = [p for p in f.choices if p[0].endswith('.py')] @@ -954,7 +955,7 @@ class FieldsTests(SimpleTestCase): assert fix_os_paths(f.clean(path + 'fields.py')).endswith('/django/forms/fields.py') def test_filepathfield_3(self): - path = forms.__file__ + path = upath(forms.__file__) path = os.path.dirname(os.path.abspath(path)) + '/' f = FilePathField(path=path, match='^.*?\.py$') f.choices.sort() @@ -972,7 +973,7 @@ class FieldsTests(SimpleTestCase): self.assertTrue(got[0].endswith(exp[0])) def test_filepathfield_4(self): - path = os.path.abspath(forms.__file__) + path = os.path.abspath(upath(forms.__file__)) path = os.path.dirname(path) + '/' f = FilePathField(path=path, recursive=True, match='^.*?\.py$') f.choices.sort() @@ -992,7 +993,7 @@ class FieldsTests(SimpleTestCase): self.assertTrue(got[0].endswith(exp[0])) def test_filepathfield_folders(self): - path = os.path.dirname(__file__) + '/filepath_test_files/' + path = os.path.dirname(upath(__file__)) + '/filepath_test_files/' f = FilePathField(path=path, allow_folders=True, allow_files=False) f.choices.sort() expected = [ diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py index a8a28ba806..1c83ed04d4 100644 --- a/tests/regressiontests/forms/tests/forms.py +++ b/tests/regressiontests/forms/tests/forms.py @@ -269,6 +269,11 @@ class FormsTestCase(TestCase): f = SignupForm({'email': 'test@example.com', 'get_spam': 'false'}, auto_id=False) self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" />') + # A value of '0' should be interpreted as a True value (#16820) + f = SignupForm({'email': 'test@example.com', 'get_spam': '0'}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.cleaned_data.get('get_spam')) + def test_widget_output(self): # Any Field can have a Widget class passed to its constructor: class ContactForm(Form): diff --git a/tests/regressiontests/forms/tests/formsets.py b/tests/regressiontests/forms/tests/formsets.py index b3ceee551b..bf893c4c1d 100644 --- a/tests/regressiontests/forms/tests/formsets.py +++ b/tests/regressiontests/forms/tests/formsets.py @@ -856,6 +856,27 @@ class FormsFormsetTestCase(TestCase): formset = FavoriteDrinksFormSet(error_class=CustomErrorList) self.assertEqual(formset.forms[0].error_class, CustomErrorList) + def test_formset_calls_forms_is_valid(self): + # Regression tests for #18574 -- make sure formsets call + # is_valid() on each form. + + class AnotherChoice(Choice): + def is_valid(self): + self.is_valid_called = True + return super(AnotherChoice, self).is_valid() + + AnotherChoiceFormSet = formset_factory(AnotherChoice) + data = { + 'choices-TOTAL_FORMS': '1', # number of forms rendered + 'choices-INITIAL_FORMS': '0', # number of forms with initial data + 'choices-MAX_NUM_FORMS': '0', # max number of forms + 'choices-0-choice': 'Calexico', + 'choices-0-votes': '100', + } + formset = AnotherChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(formset.is_valid()) + self.assertTrue(all([form.is_valid_called for form in formset.forms])) + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered diff --git a/tests/regressiontests/forms/tests/input_formats.py b/tests/regressiontests/forms/tests/input_formats.py index d591804b9d..7a305dbc2b 100644 --- a/tests/regressiontests/forms/tests/input_formats.py +++ b/tests/regressiontests/forms/tests/input_formats.py @@ -3,13 +3,14 @@ from datetime import time, date, datetime from django import forms from django.test.utils import override_settings from django.utils.translation import activate, deactivate -from django.utils.unittest import TestCase +from django.test import SimpleTestCase @override_settings(TIME_INPUT_FORMATS=["%I:%M:%S %p", "%I:%M %p"], USE_L10N=True) -class LocalizedTimeTests(TestCase): +class LocalizedTimeTests(SimpleTestCase): def setUp(self): - activate('de') + # nl/formats.py has customized TIME_INPUT_FORMATS + activate('nl') def tearDown(self): deactivate() @@ -92,7 +93,7 @@ class LocalizedTimeTests(TestCase): result = f.clean('13.30.05') self.assertEqual(result, time(13,30,5)) - # # Check that the parsed result does a round trip to the same format + # Check that the parsed result does a round trip to the same format text = f.widget._format_value(result) self.assertEqual(text, "13:30:05") @@ -106,7 +107,7 @@ class LocalizedTimeTests(TestCase): @override_settings(TIME_INPUT_FORMATS=["%I:%M:%S %p", "%I:%M %p"]) -class CustomTimeInputFormatsTests(TestCase): +class CustomTimeInputFormatsTests(SimpleTestCase): def test_timeField(self): "TimeFields can parse dates in the default format" f = forms.TimeField() @@ -198,7 +199,7 @@ class CustomTimeInputFormatsTests(TestCase): self.assertEqual(text, "01:30:00 PM") -class SimpleTimeFormatTests(TestCase): +class SimpleTimeFormatTests(SimpleTestCase): def test_timeField(self): "TimeFields can parse dates in the default format" f = forms.TimeField() @@ -289,7 +290,7 @@ class SimpleTimeFormatTests(TestCase): @override_settings(DATE_INPUT_FORMATS=["%d/%m/%Y", "%d-%m-%Y"], USE_L10N=True) -class LocalizedDateTests(TestCase): +class LocalizedDateTests(SimpleTestCase): def setUp(self): activate('de') @@ -302,6 +303,9 @@ class LocalizedDateTests(TestCase): # Parse a date in an unaccepted format; get an error self.assertRaises(forms.ValidationError, f.clean, '21/12/2010') + # ISO formats are accepted, even if not specified in formats.py + self.assertEqual(f.clean('2010-12-21'), date(2010,12,21)) + # Parse a date in a valid format, get a parsed result result = f.clean('21.12.2010') self.assertEqual(result, date(2010,12,21)) @@ -390,7 +394,7 @@ class LocalizedDateTests(TestCase): @override_settings(DATE_INPUT_FORMATS=["%d.%m.%Y", "%d-%m-%Y"]) -class CustomDateInputFormatsTests(TestCase): +class CustomDateInputFormatsTests(SimpleTestCase): def test_dateField(self): "DateFields can parse dates in the default format" f = forms.DateField() @@ -481,7 +485,7 @@ class CustomDateInputFormatsTests(TestCase): text = f.widget._format_value(result) self.assertEqual(text, "21.12.2010") -class SimpleDateFormatTests(TestCase): +class SimpleDateFormatTests(SimpleTestCase): def test_dateField(self): "DateFields can parse dates in the default format" f = forms.DateField() @@ -572,7 +576,7 @@ class SimpleDateFormatTests(TestCase): @override_settings(DATETIME_INPUT_FORMATS=["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"], USE_L10N=True) -class LocalizedDateTimeTests(TestCase): +class LocalizedDateTimeTests(SimpleTestCase): def setUp(self): activate('de') @@ -585,6 +589,9 @@ class LocalizedDateTimeTests(TestCase): # Parse a date in an unaccepted format; get an error self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM 21/12/2010') + # ISO formats are accepted, even if not specified in formats.py + self.assertEqual(f.clean('2010-12-21 13:30:05'), datetime(2010,12,21,13,30,5)) + # Parse a date in a valid format, get a parsed result result = f.clean('21.12.2010 13:30:05') self.assertEqual(result, datetime(2010,12,21,13,30,5)) @@ -673,7 +680,7 @@ class LocalizedDateTimeTests(TestCase): @override_settings(DATETIME_INPUT_FORMATS=["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"]) -class CustomDateTimeInputFormatsTests(TestCase): +class CustomDateTimeInputFormatsTests(SimpleTestCase): def test_dateTimeField(self): "DateTimeFields can parse dates in the default format" f = forms.DateTimeField() @@ -764,7 +771,7 @@ class CustomDateTimeInputFormatsTests(TestCase): text = f.widget._format_value(result) self.assertEqual(text, "01:30:00 PM 21/12/2010") -class SimpleDateTimeFormatTests(TestCase): +class SimpleDateTimeFormatTests(SimpleTestCase): def test_dateTimeField(self): "DateTimeFields can parse dates in the default format" f = forms.DateTimeField() diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 4bdd3f76ea..31c0e65c7c 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -4,7 +4,9 @@ from __future__ import unicode_literals import copy import datetime +from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.urlresolvers import reverse from django.forms import * from django.forms.widgets import RadioFieldRenderer from django.utils import formats @@ -15,6 +17,8 @@ from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import python_2_unicode_compatible +from ..models import Article + class FormsWidgetTestCase(TestCase): # Each Widget class corresponds to an HTML form widget. A Widget knows how to @@ -225,6 +229,10 @@ class FormsWidgetTestCase(TestCase): # checkboxes). self.assertFalse(w.value_from_datadict({}, {}, 'testing')) + value = w.value_from_datadict({'testing': '0'}, {}, 'testing') + self.assertIsInstance(value, bool) + self.assertTrue(value) + self.assertFalse(w._has_changed(None, None)) self.assertFalse(w._has_changed(None, '')) self.assertFalse(w._has_changed('', None)) @@ -1091,6 +1099,22 @@ class WidgetTests(TestCase): self.assertFalse(form.is_valid()) +class LiveWidgetTests(AdminSeleniumWebDriverTestCase): + urls = 'regressiontests.forms.urls' + + def test_textarea_trailing_newlines(self): + """ + Test that a roundtrip on a ModelForm doesn't alter the TextField value + """ + article = Article.objects.create(content="\nTst\n") + self.selenium.get('%s%s' % (self.live_server_url, + reverse('article_form', args=[article.pk]))) + self.selenium.find_element_by_id('submit').submit() + article = Article.objects.get(pk=article.pk) + # Should be "\nTst\n" after #19251 is fixed + self.assertEqual(article.content, "\r\nTst\r\n") + + @python_2_unicode_compatible class FakeFieldFile(object): """ diff --git a/tests/regressiontests/forms/urls.py b/tests/regressiontests/forms/urls.py new file mode 100644 index 0000000000..b482b6c45c --- /dev/null +++ b/tests/regressiontests/forms/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, url +from django.views.generic.edit import UpdateView + +from .views import ArticleFormView + + +urlpatterns = patterns('', + url(r'^/model_form/(?P<pk>\d+)/$', ArticleFormView.as_view(), name="article_form"), +) diff --git a/tests/regressiontests/forms/views.py b/tests/regressiontests/forms/views.py new file mode 100644 index 0000000000..4bf384363c --- /dev/null +++ b/tests/regressiontests/forms/views.py @@ -0,0 +1,8 @@ +from django.views.generic.edit import UpdateView + +from .models import Article + + +class ArticleFormView(UpdateView): + model = Article + success_url = '/' diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py index 439b0d7327..c7ad7a0deb 100644 --- a/tests/regressiontests/generic_views/base.py +++ b/tests/regressiontests/generic_views/base.py @@ -216,6 +216,17 @@ class ViewTest(unittest.TestCase): response_allows = set(response['Allow'].split(', ')) self.assertEqual(set(expected_methods + ('OPTIONS',)), response_allows) + def test_args_kwargs_request_on_self(self): + """ + Test a view only has args, kwargs & request once `as_view` + has been called. + """ + bare_view = InstanceView() + view = InstanceView.as_view()(self.rf.get('/')) + for attribute in ('args', 'kwargs', 'request'): + self.assertNotIn(attribute, dir(bare_view)) + self.assertIn(attribute, dir(view)) + class TemplateViewTest(TestCase): urls = 'regressiontests.generic_views.urls' diff --git a/tests/regressiontests/generic_views/edit.py b/tests/regressiontests/generic_views/edit.py index 16f4da8efe..0f1eb3cca7 100644 --- a/tests/regressiontests/generic_views/edit.py +++ b/tests/regressiontests/generic_views/edit.py @@ -20,6 +20,15 @@ class FormMixinTests(TestCase): initial_2 = FormMixin().get_initial() self.assertNotEqual(initial_1, initial_2) + +class BasicFormTests(TestCase): + urls = 'regressiontests.generic_views.urls' + + def test_post_data(self): + res = self.client.post('/contact/', {'name': "Me", 'message': "Hello"}) + self.assertRedirects(res, 'http://testserver/list/authors/') + + class ModelFormMixinTests(TestCase): def test_get_form(self): form_class = views.AuthorGetQuerySetFormView().get_form_class() diff --git a/tests/regressiontests/generic_views/forms.py b/tests/regressiontests/generic_views/forms.py index a78242f555..e036ad8afc 100644 --- a/tests/regressiontests/generic_views/forms.py +++ b/tests/regressiontests/generic_views/forms.py @@ -11,3 +11,8 @@ class AuthorForm(forms.ModelForm): class Meta: model = Author + + +class ContactForm(forms.Form): + name = forms.CharField() + message = forms.CharField(widget=forms.Textarea) diff --git a/tests/regressiontests/generic_views/list.py b/tests/regressiontests/generic_views/list.py index 14dc1d725d..be2721d775 100644 --- a/tests/regressiontests/generic_views/list.py +++ b/tests/regressiontests/generic_views/list.py @@ -2,7 +2,9 @@ from __future__ import absolute_import from django.core.exceptions import ImproperlyConfigured from django.test import TestCase +from django.test.utils import override_settings from django.views.generic.base import View +from django.utils.encoding import force_str from .models import Author, Artist @@ -99,6 +101,16 @@ class ListViewTests(TestCase): # Custom pagination allows for 2 orphans on a page size of 5 self.assertEqual(len(res.context['object_list']), 7) + def test_paginated_custom_page_kwarg(self): + self._make_authors(100) + res = self.client.get('/list/authors/paginated/custom_page_kwarg/', {'pagina': '2'}) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, 'generic_views/author_list.html') + self.assertEqual(len(res.context['object_list']), 30) + self.assertIs(res.context['author_list'], res.context['object_list']) + self.assertEqual(res.context['author_list'][0].name, 'Author 30') + self.assertEqual(res.context['page_obj'].number, 2) + def test_paginated_custom_paginator_constructor(self): self._make_authors(7) res = self.client.get('/list/authors/paginated/custom_constructor/') @@ -106,8 +118,26 @@ class ListViewTests(TestCase): # Custom pagination allows for 2 orphans on a page size of 5 self.assertEqual(len(res.context['object_list']), 7) + def test_paginated_orphaned_queryset(self): + self._make_authors(92) + res = self.client.get('/list/authors/paginated-orphaned/') + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context['page_obj'].number, 1) + res = self.client.get( + '/list/authors/paginated-orphaned/', {'page': 'last'}) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context['page_obj'].number, 3) + res = self.client.get( + '/list/authors/paginated-orphaned/', {'page': '3'}) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context['page_obj'].number, 3) + res = self.client.get( + '/list/authors/paginated-orphaned/', {'page': '4'}) + self.assertEqual(res.status_code, 404) + def test_paginated_non_queryset(self): res = self.client.get('/list/dict/paginated/') + self.assertEqual(res.status_code, 200) self.assertEqual(len(res.context['object_list']), 1) @@ -171,8 +201,17 @@ class ListViewTests(TestCase): with self.assertNumQueries(3): self.client.get('/list/authors/notempty/paginated/') + @override_settings(DEBUG=True) + def test_paginated_list_view_returns_useful_message_on_invalid_page(self): + # test for #19240 + # tests that source exception's message is included in page + self._make_authors(1) + res = self.client.get('/list/authors/paginated/2/') + self.assertEqual(res.status_code, 404) + self.assertEqual(force_str(res.context.get('reason')), + "Invalid page (2): That page contains no results") + def _make_authors(self, n): Author.objects.all().delete() for i in range(n): Author.objects.create(name='Author %02i' % i, slug='a%s' % i) - diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py index f59389ef78..30c73d67ca 100644 --- a/tests/regressiontests/generic_views/models.py +++ b/tests/regressiontests/generic_views/models.py @@ -1,3 +1,4 @@ +from django.core.urlresolvers import reverse from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -14,9 +15,8 @@ class Artist(models.Model): def __str__(self): return self.name - @models.permalink def get_absolute_url(self): - return ('artist_detail', (), {'pk': self.id}) + return reverse('artist_detail', kwargs={'pk': self.id}) @python_2_unicode_compatible class Author(models.Model): diff --git a/tests/regressiontests/generic_views/tests.py b/tests/regressiontests/generic_views/tests.py index c985ad3309..3f80600efe 100644 --- a/tests/regressiontests/generic_views/tests.py +++ b/tests/regressiontests/generic_views/tests.py @@ -6,6 +6,6 @@ from .dates import (ArchiveIndexViewTests, YearArchiveViewTests, MonthArchiveViewTests, WeekArchiveViewTests, DayArchiveViewTests, DateDetailViewTests) from .detail import DetailViewTest -from .edit import (FormMixinTests, ModelFormMixinTests, CreateViewTests, - UpdateViewTests, DeleteViewTests) +from .edit import (FormMixinTests, BasicFormTests, ModelFormMixinTests, + CreateViewTests, UpdateViewTests, DeleteViewTests) from .list import ListViewTests diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py index c72bfecb65..34b37fa7c4 100644 --- a/tests/regressiontests/generic_views/urls.py +++ b/tests/regressiontests/generic_views/urls.py @@ -56,6 +56,10 @@ urlpatterns = patterns('', (r'^detail/nonmodel/1/$', views.NonModelDetail.as_view()), + # FormView + (r'^contact/$', + views.ContactView.as_view()), + # Create/UpdateView (r'^edit/artists/create/$', views.ArtistCreate.as_view()), @@ -133,6 +137,8 @@ urlpatterns = patterns('', views.AuthorList.as_view(paginate_by=30)), (r'^list/authors/paginated/(?P<page>\d+)/$', views.AuthorList.as_view(paginate_by=30)), + (r'^list/authors/paginated-orphaned/$', + views.AuthorList.as_view(paginate_by=30, paginate_orphans=2)), (r'^list/authors/notempty/$', views.AuthorList.as_view(allow_empty=False)), (r'^list/authors/notempty/paginated/$', @@ -149,6 +155,8 @@ urlpatterns = patterns('', views.AuthorList.as_view(queryset=None)), (r'^list/authors/paginated/custom_class/$', views.AuthorList.as_view(paginate_by=5, paginator_class=views.CustomPaginator)), + (r'^list/authors/paginated/custom_page_kwarg/$', + views.AuthorList.as_view(paginate_by=30, page_kwarg='pagina')), (r'^list/authors/paginated/custom_constructor/$', views.AuthorListCustomPaginator.as_view()), diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py index f7fcf6f509..71e78e82c7 100644 --- a/tests/regressiontests/generic_views/views.py +++ b/tests/regressiontests/generic_views/views.py @@ -2,11 +2,11 @@ from __future__ import absolute_import from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.views import generic -from .forms import AuthorForm +from .forms import AuthorForm, ContactForm from .models import Artist, Author, Book, Page, BookSigning @@ -75,6 +75,13 @@ class AuthorListCustomPaginator(AuthorList): orphans=2, allow_empty_first_page=allow_empty_first_page) + +class ContactView(generic.FormView): + form_class = ContactForm + success_url = reverse_lazy('authors_list') + template_name = 'generic_views/form.html' + + class ArtistCreate(generic.CreateView): model = Artist diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 553b6ecff4..a601e541c6 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -14,6 +14,7 @@ from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, parse_cookie) from django.test import TestCase from django.utils.encoding import smart_str +from django.utils._os import upath from django.utils import six from django.utils import unittest @@ -149,7 +150,7 @@ class QueryDictTests(unittest.TestCase): self.assertEqual(q.setdefault('foo', 'bar'), 'bar') self.assertEqual(q['foo'], 'bar') self.assertEqual(q.getlist('foo'), ['bar']) - self.assertEqual(q.urlencode(), 'foo=bar&name=john') + self.assertIn(q.urlencode(), ['foo=bar&name=john', 'name=john&foo=bar']) q.clear() self.assertEqual(len(q), 0) @@ -266,14 +267,18 @@ class HttpResponseTests(unittest.TestCase): # The response also converts unicode or bytes keys to strings, but requires # them to contain ASCII r = HttpResponse() + del r['Content-Type'] r['foo'] = 'bar' l = list(r.items()) + self.assertEqual(len(l), 1) self.assertEqual(l[0], ('foo', 'bar')) self.assertIsInstance(l[0][0], str) r = HttpResponse() + del r['Content-Type'] r[b'foo'] = 'bar' l = list(r.items()) + self.assertEqual(len(l), 1) self.assertEqual(l[0], ('foo', 'bar')) self.assertIsInstance(l[0][0], str) @@ -479,7 +484,7 @@ class StreamingHttpResponseTests(TestCase): class FileCloseTests(TestCase): def test_response(self): - filename = os.path.join(os.path.dirname(__file__), 'abc.txt') + filename = os.path.join(os.path.dirname(upath(__file__)), 'abc.txt') # file isn't closed until we close the response. file1 = open(filename) @@ -512,7 +517,7 @@ class FileCloseTests(TestCase): self.assertTrue(file2.closed) def test_streaming_response(self): - filename = os.path.join(os.path.dirname(__file__), 'abc.txt') + filename = os.path.join(os.path.dirname(upath(__file__)), 'abc.txt') # file isn't closed until we close the response. file1 = open(filename) diff --git a/tests/regressiontests/i18n/commands/compilation.py b/tests/regressiontests/i18n/commands/compilation.py index c6ab77941b..2944469110 100644 --- a/tests/regressiontests/i18n/commands/compilation.py +++ b/tests/regressiontests/i18n/commands/compilation.py @@ -4,9 +4,10 @@ from django.core.management import call_command, CommandError from django.test import TestCase from django.test.utils import override_settings from django.utils import translation, six +from django.utils._os import upath from django.utils.six import StringIO -test_dir = os.path.abspath(os.path.dirname(__file__)) +test_dir = os.path.abspath(os.path.dirname(upath(__file__))) class MessageCompilationTests(TestCase): @@ -25,9 +26,9 @@ class PoFileTests(MessageCompilationTests): def test_bom_rejection(self): os.chdir(test_dir) - with six.assertRaisesRegex(self, CommandError, - "file has a BOM \(Byte Order Mark\)"): + with self.assertRaises(CommandError) as cm: call_command('compilemessages', locale=self.LOCALE, stderr=StringIO()) + self.assertIn("file has a BOM (Byte Order Mark)", cm.exception.args[0]) self.assertFalse(os.path.exists(self.MO_FILE)) diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py index ca2c3cc026..aa5efe1967 100644 --- a/tests/regressiontests/i18n/commands/extraction.py +++ b/tests/regressiontests/i18n/commands/extraction.py @@ -1,4 +1,5 @@ # -*- encoding: utf-8 -*- +from __future__ import unicode_literals import os import re @@ -6,6 +7,8 @@ import shutil from django.core import management from django.test import TestCase +from django.utils.encoding import force_text +from django.utils._os import upath from django.utils.six import StringIO @@ -17,7 +20,7 @@ class ExtractorTests(TestCase): def setUp(self): self._cwd = os.getcwd() - self.test_dir = os.path.abspath(os.path.dirname(__file__)) + self.test_dir = os.path.abspath(os.path.dirname(upath(__file__))) def _rmrf(self, dname): if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir: @@ -55,7 +58,7 @@ class BasicExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertTrue('#. Translators: This comment should be extracted' in po_contents) self.assertTrue('This comment should not be extracted' not in po_contents) # Comments in templates @@ -83,7 +86,7 @@ class BasicExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertMsgId('Literal with a percent symbol at the end %%', po_contents) self.assertMsgId('Literal with a percent %% symbol in the middle', po_contents) self.assertMsgId('Completed 50%% of all the tasks', po_contents) @@ -99,7 +102,7 @@ class BasicExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertMsgId('I think that 100%% is more that 50%% of anything.', po_contents) self.assertMsgId('I think that 100%% is more that 50%% of %(obj)s.', po_contents) self.assertMsgId("Blocktrans extraction shouldn't double escape this: %%, a=%(a)s", po_contents) @@ -123,7 +126,7 @@ class BasicExtractorTests(ExtractorTests): stdout = StringIO() management.call_command('makemessages', locale=LOCALE, stdout=stdout) os.remove('./code_sample.py') - self.assertIn("code_sample.py:4", stdout.getvalue()) + self.assertIn("code_sample.py:4", force_text(stdout.getvalue())) def test_template_message_context_extractor(self): """ @@ -135,7 +138,7 @@ class BasicExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) # {% trans %} self.assertTrue('msgctxt "Special trans context #1"' in po_contents) self.assertTrue("Translatable literal #7a" in po_contents) @@ -161,7 +164,7 @@ class BasicExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) # {% trans %} self.assertTrue('msgctxt "Context wrapped in double quotes"' in po_contents) self.assertTrue('msgctxt "Context wrapped in single quotes"' in po_contents) @@ -216,7 +219,7 @@ class SymlinkExtractorTests(ExtractorTests): def setUp(self): self._cwd = os.getcwd() - self.test_dir = os.path.abspath(os.path.dirname(__file__)) + self.test_dir = os.path.abspath(os.path.dirname(upath(__file__))) self.symlinked_dir = os.path.join(self.test_dir, 'templates_symlinked') def tearDown(self): @@ -238,7 +241,7 @@ class SymlinkExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0, symlinks=True) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertMsgId('This literal should be included.', po_contents) self.assertTrue('templates_symlinked/test.html' in po_contents) @@ -250,7 +253,7 @@ class CopyPluralFormsExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertTrue('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents) @@ -261,7 +264,7 @@ class NoWrapExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0, no_wrap=True) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertMsgId('This literal should also be included wrapped or not wrapped depending on the use of the --no-wrap option.', po_contents) def test_no_wrap_disabled(self): @@ -269,7 +272,7 @@ class NoWrapExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0, no_wrap=False) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertMsgId('""\n"This literal should also be included wrapped or not wrapped depending on the "\n"use of the --no-wrap option."', po_contents, use_quotes=False) @@ -280,7 +283,7 @@ class NoLocationExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0, no_location=True) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertFalse('#: templates/test.html:55' in po_contents) def test_no_location_disabled(self): @@ -288,5 +291,5 @@ class NoLocationExtractorTests(ExtractorTests): management.call_command('makemessages', locale=LOCALE, verbosity=0, no_location=False) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: - po_contents = fp.read() + po_contents = force_text(fp.read()) self.assertTrue('#: templates/test.html:55' in po_contents) diff --git a/tests/regressiontests/i18n/contenttypes/tests.py b/tests/regressiontests/i18n/contenttypes/tests.py index 178232f543..5e8a9823e1 100644 --- a/tests/regressiontests/i18n/contenttypes/tests.py +++ b/tests/regressiontests/i18n/contenttypes/tests.py @@ -6,6 +6,7 @@ import os from django.contrib.contenttypes.models import ContentType from django.test import TestCase from django.test.utils import override_settings +from django.utils._os import upath from django.utils import six from django.utils import translation @@ -13,7 +14,7 @@ from django.utils import translation @override_settings( USE_I18N=True, LOCALE_PATHS=( - os.path.join(os.path.dirname(__file__), 'locale'), + os.path.join(os.path.dirname(upath(__file__)), 'locale'), ), LANGUAGE_CODE='en', LANGUAGES=( diff --git a/tests/regressiontests/i18n/patterns/tests.py b/tests/regressiontests/i18n/patterns/tests.py index 73c9f56711..358cdf65db 100644 --- a/tests/regressiontests/i18n/patterns/tests.py +++ b/tests/regressiontests/i18n/patterns/tests.py @@ -7,16 +7,17 @@ from django.core.urlresolvers import reverse, clear_url_caches from django.test import TestCase from django.test.utils import override_settings from django.template import Template, Context +from django.utils._os import upath from django.utils import translation @override_settings( USE_I18N=True, LOCALE_PATHS=( - os.path.join(os.path.dirname(__file__), 'locale'), + os.path.join(os.path.dirname(upath(__file__)), 'locale'), ), TEMPLATE_DIRS=( - os.path.join(os.path.dirname(__file__), 'templates'), + os.path.join(os.path.dirname(upath(__file__)), 'templates'), ), LANGUAGE_CODE='en', LANGUAGES=( diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 4054f85ef0..dcc288e600 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -18,6 +18,7 @@ from django.utils.formats import (get_format, date_format, time_format, number_format) from django.utils.importlib import import_module from django.utils.numberformat import format as nformat +from django.utils._os import upath from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText from django.utils import six from django.utils.six import PY3 @@ -44,7 +45,7 @@ from .patterns.tests import (URLRedirectWithoutTrailingSlashTests, URLPrefixTests, URLResponseTests, URLRedirectTests, PathUnusedTests) -here = os.path.dirname(os.path.abspath(__file__)) +here = os.path.dirname(os.path.abspath(upath(__file__))) extended_locale_paths = settings.LOCALE_PATHS + ( os.path.join(here, 'other', 'locale'), ) @@ -83,6 +84,10 @@ class TranslationTests(TestCase): s4 = ugettext_lazy('Some other string') self.assertEqual(False, s == s4) + if not six.PY3: + # On Python 2, gettext_lazy should not transform a bytestring to unicode + self.assertEqual(gettext_lazy(b"test").upper(), b"TEST") + def test_lazy_pickle(self): s1 = ugettext_lazy("test") self.assertEqual(six.text_type(s1), "test") @@ -662,8 +667,8 @@ class FormattingTests(TestCase): with self.settings(USE_L10N=True, FORMAT_MODULE_PATH='regressiontests.i18n.other.locale'): with translation.override('de', deactivate=True): - old = "%r" % get_format_modules(reverse=True) - new = "%r" % get_format_modules(reverse=True) # second try + old = str("%r") % get_format_modules(reverse=True) + new = str("%r") % get_format_modules(reverse=True) # second try self.assertEqual(new, old, 'Value returned by get_formats_modules() must be preserved between calls.') def test_localize_templatetag_and_filter(self): diff --git a/tests/modeltests/pagination/__init__.py b/tests/regressiontests/indexes/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/modeltests/pagination/__init__.py +++ b/tests/regressiontests/indexes/__init__.py diff --git a/tests/regressiontests/indexes/models.py b/tests/regressiontests/indexes/models.py new file mode 100644 index 0000000000..9758377f99 --- /dev/null +++ b/tests/regressiontests/indexes/models.py @@ -0,0 +1,11 @@ +from django.db import models + + +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateTimeField() + + class Meta: + index_together = [ + ["headline", "pub_date"], + ] diff --git a/tests/regressiontests/indexes/tests.py b/tests/regressiontests/indexes/tests.py new file mode 100644 index 0000000000..0dac881fa9 --- /dev/null +++ b/tests/regressiontests/indexes/tests.py @@ -0,0 +1,12 @@ +from django.core.management.color import no_style +from django.db import connections, DEFAULT_DB_ALIAS +from django.test import TestCase + +from .models import Article + + +class IndexesTests(TestCase): + def test_index_together(self): + connection = connections[DEFAULT_DB_ALIAS] + index_sql = connection.creation.sql_indexes_for_model(Article, no_style()) + self.assertEqual(len(index_sql), 1) diff --git a/tests/regressiontests/initial_sql_regress/sql/simple.sql b/tests/regressiontests/initial_sql_regress/sql/simple.sql index 39363baa9a..d82f8381af 100644 --- a/tests/regressiontests/initial_sql_regress/sql/simple.sql +++ b/tests/regressiontests/initial_sql_regress/sql/simple.sql @@ -2,7 +2,8 @@ INSERT INTO initial_sql_regress_simple (name) VALUES ('John'); -- another comment INSERT INTO initial_sql_regress_simple (name) VALUES ('-- Comment Man'); INSERT INTO initial_sql_regress_simple (name) VALUES ('Paul'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('Ringo'); +INSERT INTO + initial_sql_regress_simple (name) VALUES ('Ringo'); INSERT INTO initial_sql_regress_simple (name) VALUES ('George'); INSERT INTO initial_sql_regress_simple (name) VALUES ('Miles O''Brien'); INSERT INTO initial_sql_regress_simple (name) VALUES ('Semicolon;Man'); diff --git a/tests/regressiontests/initial_sql_regress/tests.py b/tests/regressiontests/initial_sql_regress/tests.py index 03a91cb807..39d8921061 100644 --- a/tests/regressiontests/initial_sql_regress/tests.py +++ b/tests/regressiontests/initial_sql_regress/tests.py @@ -1,3 +1,6 @@ +from django.core.management.color import no_style +from django.core.management.sql import custom_sql_for_model +from django.db import connections, DEFAULT_DB_ALIAS from django.test import TestCase from .models import Simple @@ -15,10 +18,6 @@ class InitialSQLTests(TestCase): self.assertEqual(Simple.objects.count(), 0) def test_custom_sql(self): - from django.core.management.sql import custom_sql_for_model - from django.core.management.color import no_style - from django.db import connections, DEFAULT_DB_ALIAS - # Simulate the custom SQL loading by syncdb connection = connections[DEFAULT_DB_ALIAS] custom_sql = custom_sql_for_model(Simple, no_style(), connection) diff --git a/tests/regressiontests/inspectdb/tests.py b/tests/regressiontests/inspectdb/tests.py index 484e7f4060..028d263337 100644 --- a/tests/regressiontests/inspectdb/tests.py +++ b/tests/regressiontests/inspectdb/tests.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django.core.management import call_command +from django.db import connection from django.test import TestCase, skipUnlessDBFeature from django.utils.six import StringIO @@ -60,14 +61,16 @@ class InspectDBTestCase(TestCase): self.assertIn("number_45extra = models.CharField", output) def test_special_column_name_introspection(self): - """Introspection of column names containing special characters, - unsuitable for Python identifiers + """ + Introspection of column names containing special characters, + unsuitable for Python identifiers """ out = StringIO() call_command('inspectdb', stdout=out) output = out.getvalue() + base_name = 'Field' if connection.vendor != 'oracle' else 'field' self.assertIn("field = models.IntegerField()", output) - self.assertIn("field_field = models.IntegerField(db_column='Field_')", output) - self.assertIn("field_field_0 = models.IntegerField(db_column='Field__')", output) + self.assertIn("field_field = models.IntegerField(db_column='%s_')" % base_name, output) + self.assertIn("field_field_0 = models.IntegerField(db_column='%s__')" % base_name, output) self.assertIn("field_field_1 = models.IntegerField(db_column='__field')", output) self.assertIn("prc_x = models.IntegerField(db_column='prc(%) x')", output) diff --git a/tests/regressiontests/introspection/models.py b/tests/regressiontests/introspection/models.py index 6e5beba61d..4de82e47e7 100644 --- a/tests/regressiontests/introspection/models.py +++ b/tests/regressiontests/introspection/models.py @@ -17,6 +17,7 @@ class Reporter(models.Model): def __str__(self): return "%s %s" % (self.first_name, self.last_name) + @python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) @@ -28,3 +29,6 @@ class Article(models.Model): class Meta: ordering = ('headline',) + index_together = [ + ["headline", "pub_date"], + ] diff --git a/tests/regressiontests/introspection/tests.py b/tests/regressiontests/introspection/tests.py index a54e0c670b..2df946d874 100644 --- a/tests/regressiontests/introspection/tests.py +++ b/tests/regressiontests/introspection/tests.py @@ -1,14 +1,19 @@ -from __future__ import absolute_import,unicode_literals +from __future__ import absolute_import, unicode_literals from functools import update_wrapper from django.db import connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature -from django.utils import six +from django.utils import six, unittest from .models import Reporter, Article -# +if connection.vendor == 'oracle': + expectedFailureOnOracle = unittest.expectedFailure +else: + expectedFailureOnOracle = lambda f: f + + # The introspection module is optional, so methods tested here might raise # NotImplementedError. This is perfectly acceptable behavior for the backend # in question, but the tests need to handle this without failing. Ideally we'd @@ -18,7 +23,7 @@ from .models import Reporter, Article # wrapper that ignores the exception. # # The metaclass is just for fun. -# + def ignore_not_implemented(func): def _inner(*args, **kwargs): @@ -29,15 +34,16 @@ def ignore_not_implemented(func): update_wrapper(_inner, func) return _inner + class IgnoreNotimplementedError(type): def __new__(cls, name, bases, attrs): - for k,v in attrs.items(): + for k, v in attrs.items(): if k.startswith('test'): attrs[k] = ignore_not_implemented(v) return type.__new__(cls, name, bases, attrs) -class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)): +class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)): def test_table_names(self): tl = connection.introspection.table_names() self.assertEqual(tl, sorted(tl)) @@ -89,7 +95,13 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase) [datatype(r[1], r) for r in desc], ['IntegerField', 'CharField', 'CharField', 'CharField', 'BigIntegerField'] ) - # Check also length of CharFields + + # The following test fails on Oracle due to #17202 (can't correctly + # inspect the length of character columns). + @expectedFailureOnOracle + def test_get_table_description_col_lengths(self): + cursor = connection.cursor() + desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table) self.assertEqual( [r[3] for r in desc if datatype(r[1], r) == 'CharField'], [30, 30, 75] @@ -152,6 +164,7 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase) self.assertNotIn('first_name', indexes) self.assertIn('id', indexes) + def datatype(dbtype, description): """Helper to convert a data type into a string.""" dt = connection.introspection.get_field_type(dbtype, description) diff --git a/tests/regressiontests/logging_tests/logconfig.py b/tests/regressiontests/logging_tests/logconfig.py index fc5ea1a0bd..b068103eb9 100644 --- a/tests/regressiontests/logging_tests/logconfig.py +++ b/tests/regressiontests/logging_tests/logconfig.py @@ -1,8 +1,15 @@ import logging from django.conf import settings +from django.core.mail.backends.base import BaseEmailBackend + class MyHandler(logging.Handler): def __init__(self): logging.Handler.__init__(self) self.config = settings.LOGGING + + +class MyEmailBackend(BaseEmailBackend): + def send_messages(self, email_messages): + pass diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py index e40800efde..07804eb398 100644 --- a/tests/regressiontests/logging_tests/tests.py +++ b/tests/regressiontests/logging_tests/tests.py @@ -2,17 +2,23 @@ from __future__ import unicode_literals import copy import logging +import sys import warnings -from django.conf import compat_patch_logging_config +from django.conf import compat_patch_logging_config, LazySettings from django.core import mail from django.test import TestCase, RequestFactory from django.test.utils import override_settings +from django.utils.encoding import force_text from django.utils.log import CallbackFilter, RequireDebugFalse from django.utils.six import StringIO +from django.utils.unittest import skipUnless from ..admin_scripts.tests import AdminScriptTestCase +from .logconfig import MyEmailBackend + +PYVERS = sys.version_info[:2] # logging config prior to using filter with mail_admins OLD_LOGGING = { @@ -131,6 +137,32 @@ class DefaultLoggingTest(TestCase): self.logger.error("Hey, this is an error.") self.assertEqual(output.getvalue(), 'Hey, this is an error.\n') +@skipUnless(PYVERS > (2,6), "warnings captured only in Python >= 2.7") +class WarningLoggerTests(TestCase): + """ + Tests that warnings output for DeprecationWarnings is enabled + and captured to the logging system + """ + def setUp(self): + self.logger = logging.getLogger('py.warnings') + self.old_stream = self.logger.handlers[0].stream + + def tearDown(self): + self.logger.handlers[0].stream = self.old_stream + + @override_settings(DEBUG=True) + def test_warnings_capture(self): + output = StringIO() + self.logger.handlers[0].stream = output + warnings.warn('Foo Deprecated', DeprecationWarning) + self.assertTrue('Foo Deprecated' in force_text(output.getvalue())) + + def test_warnings_capture_debug_false(self): + output = StringIO() + self.logger.handlers[0].stream = output + warnings.warn('Foo Deprecated', DeprecationWarning) + self.assertFalse('Foo Deprecated' in force_text(output.getvalue())) + class CallbackFilterTest(TestCase): def test_sense(self): @@ -276,6 +308,38 @@ class AdminEmailHandlerTest(TestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, expected_subject) + @override_settings( + ADMINS=(('admin', 'admin@example.com'),), + DEBUG=False, + ) + def test_uses_custom_email_backend(self): + """ + Refs #19325 + """ + message = 'All work and no play makes Jack a dull boy' + admin_email_handler = self.get_admin_email_handler(self.logger) + mail_admins_called = {'called': False} + + def my_mail_admins(*args, **kwargs): + connection = kwargs['connection'] + self.assertTrue(isinstance(connection, MyEmailBackend)) + mail_admins_called['called'] = True + + # Monkeypatches + orig_mail_admins = mail.mail_admins + orig_email_backend = admin_email_handler.email_backend + mail.mail_admins = my_mail_admins + admin_email_handler.email_backend = ( + 'regressiontests.logging_tests.logconfig.MyEmailBackend') + + try: + self.logger.error(message) + self.assertTrue(mail_admins_called['called']) + finally: + # Revert Monkeypatches + mail.mail_admins = orig_mail_admins + admin_email_handler.email_backend = orig_email_backend + class SettingsConfigTest(AdminScriptTestCase): """ @@ -302,3 +366,20 @@ class SettingsConfigTest(AdminScriptTestCase): out, err = self.run_manage(['validate']) self.assertNoOutput(err) self.assertOutput(out, "0 errors found") + + +def dictConfig(config): + dictConfig.called = True +dictConfig.called = False + + +class SettingsConfigureLogging(TestCase): + """ + Test that calling settings.configure() initializes the logging + configuration. + """ + def test_configure_initializes_logging(self): + settings = LazySettings() + settings.configure( + LOGGING_CONFIG='regressiontests.logging_tests.tests.dictConfig') + self.assertTrue(dictConfig.called) diff --git a/tests/regressiontests/m2m_regress/models.py b/tests/regressiontests/m2m_regress/models.py index 7c1108456e..1a4a6df354 100644 --- a/tests/regressiontests/m2m_regress/models.py +++ b/tests/regressiontests/m2m_regress/models.py @@ -61,3 +61,20 @@ class Worksheet(models.Model): class User(models.Model): name = models.CharField(max_length=30) friends = models.ManyToManyField(auth.User) + + +class BadModelWithSplit(models.Model): + name = models.CharField(max_length=1) + + def split(self): + raise RuntimeError('split should not be called') + + class Meta: + abstract = True + + +class RegressionModelSplit(BadModelWithSplit): + """ + Model with a split method should not cause an error in add_lazy_relation + """ + others = models.ManyToManyField('self') diff --git a/tests/regressiontests/m2m_regress/tests.py b/tests/regressiontests/m2m_regress/tests.py index 92628c1825..3dc1d2417c 100644 --- a/tests/regressiontests/m2m_regress/tests.py +++ b/tests/regressiontests/m2m_regress/tests.py @@ -5,7 +5,7 @@ from django.test import TestCase from django.utils import six from .models import (SelfRefer, Tag, TagCollection, Entry, SelfReferChild, - SelfReferChildSibling, Worksheet) + SelfReferChildSibling, Worksheet, RegressionModelSplit) class M2MRegressionTests(TestCase): @@ -74,7 +74,7 @@ class M2MRegressionTests(TestCase): c1.tags = [t1, t2] c1 = TagCollection.objects.get(name='c1') - self.assertQuerysetEqual(c1.tags.all(), ["<Tag: t1>", "<Tag: t2>"]) + self.assertQuerysetEqual(c1.tags.all(), ["<Tag: t1>", "<Tag: t2>"], ordered=False) self.assertQuerysetEqual(t1.tag_collections.all(), ["<TagCollection: c1>"]) def test_manager_class_caching(self): @@ -90,3 +90,9 @@ class M2MRegressionTests(TestCase): # Get same manager for different instances self.assertTrue(e1.topics.__class__ is e2.topics.__class__) self.assertTrue(t1.entry_set.__class__ is t2.entry_set.__class__) + + def test_m2m_abstract_split(self): + # Regression for #19236 - an abstract class with a 'split' method + # causes a TypeError in add_lazy_relation + m1 = RegressionModelSplit(name='1') + m1.save() diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py index 47c24ed5b2..91e1aa8cc1 100644 --- a/tests/regressiontests/m2m_through_regress/models.py +++ b/tests/regressiontests/m2m_through_regress/models.py @@ -62,18 +62,21 @@ class B(models.Model): # Using to_field on the through model @python_2_unicode_compatible class Car(models.Model): - make = models.CharField(max_length=20, unique=True) + make = models.CharField(max_length=20, unique=True, null=True) drivers = models.ManyToManyField('Driver', through='CarDriver') def __str__(self): - return self.make + return "%s" % self.make @python_2_unicode_compatible class Driver(models.Model): - name = models.CharField(max_length=20, unique=True) + name = models.CharField(max_length=20, unique=True, null=True) def __str__(self): - return self.name + return "%s" % self.name + + class Meta: + ordering = ('name',) @python_2_unicode_compatible class CarDriver(models.Model): diff --git a/tests/regressiontests/m2m_through_regress/tests.py b/tests/regressiontests/m2m_through_regress/tests.py index 458c194f89..17a4525442 100644 --- a/tests/regressiontests/m2m_through_regress/tests.py +++ b/tests/regressiontests/m2m_through_regress/tests.py @@ -28,7 +28,8 @@ class M2MThroughTestCase(TestCase): bob.group_set.all(), [ "<Group: Rock>", "<Group: Roll>", - ] + ], + ordered=False ) self.assertQuerysetEqual( @@ -51,7 +52,8 @@ class M2MThroughTestCase(TestCase): frank.group_set.all(), [ "<Group: Rock>", "<Group: Roll>", - ] + ], + ordered=False ) self.assertQuerysetEqual( @@ -123,18 +125,105 @@ class ToFieldThroughTests(TestCase): self.car = Car.objects.create(make="Toyota") self.driver = Driver.objects.create(name="Ryan Briscoe") CarDriver.objects.create(car=self.car, driver=self.driver) + # We are testing if wrong objects get deleted due to using wrong + # field value in m2m queries. So, it is essential that the pk + # numberings do not match. + # Create one intentionally unused driver to mix up the autonumbering + self.unused_driver = Driver.objects.create(name="Barney Gumble") + # And two intentionally unused cars. + self.unused_car1 = Car.objects.create(make="Trabant") + self.unused_car2 = Car.objects.create(make="Wartburg") def test_to_field(self): self.assertQuerysetEqual( self.car.drivers.all(), ["<Driver: Ryan Briscoe>"] - ) + ) def test_to_field_reverse(self): self.assertQuerysetEqual( self.driver.car_set.all(), ["<Car: Toyota>"] - ) + ) + + def test_to_field_clear_reverse(self): + self.driver.car_set.clear() + self.assertQuerysetEqual( + self.driver.car_set.all(),[]) + + def test_to_field_clear(self): + self.car.drivers.clear() + self.assertQuerysetEqual( + self.car.drivers.all(),[]) + + # Low level tests for _add_items and _remove_items. We test these methods + # because .add/.remove aren't available for m2m fields with through, but + # through is the only way to set to_field currently. We do want to make + # sure these methods are ready if the ability to use .add or .remove with + # to_field relations is added some day. + def test_add(self): + self.assertQuerysetEqual( + self.car.drivers.all(), + ["<Driver: Ryan Briscoe>"] + ) + # Yikes - barney is going to drive... + self.car.drivers._add_items('car', 'driver', self.unused_driver) + self.assertQuerysetEqual( + self.car.drivers.all(), + ["<Driver: Barney Gumble>", "<Driver: Ryan Briscoe>"] + ) + + def test_add_null(self): + nullcar = Car.objects.create(make=None) + with self.assertRaises(ValueError): + nullcar.drivers._add_items('car', 'driver', self.unused_driver) + + def test_add_related_null(self): + nulldriver = Driver.objects.create(name=None) + with self.assertRaises(ValueError): + self.car.drivers._add_items('car', 'driver', nulldriver) + + def test_add_reverse(self): + car2 = Car.objects.create(make="Honda") + self.assertQuerysetEqual( + self.driver.car_set.all(), + ["<Car: Toyota>"] + ) + self.driver.car_set._add_items('driver', 'car', car2) + self.assertQuerysetEqual( + self.driver.car_set.all(), + ["<Car: Toyota>", "<Car: Honda>"], + ordered=False + ) + + def test_add_null_reverse(self): + nullcar = Car.objects.create(make=None) + with self.assertRaises(ValueError): + self.driver.car_set._add_items('driver', 'car', nullcar) + + def test_add_null_reverse_related(self): + nulldriver = Driver.objects.create(name=None) + with self.assertRaises(ValueError): + nulldriver.car_set._add_items('driver', 'car', self.car) + + def test_remove(self): + self.assertQuerysetEqual( + self.car.drivers.all(), + ["<Driver: Ryan Briscoe>"] + ) + self.car.drivers._remove_items('car', 'driver', self.driver) + self.assertQuerysetEqual( + self.car.drivers.all(),[]) + + def test_remove_reverse(self): + self.assertQuerysetEqual( + self.driver.car_set.all(), + ["<Car: Toyota>"] + ) + self.driver.car_set._remove_items('driver', 'car', self.car) + self.assertQuerysetEqual( + self.driver.car_set.all(),[]) + class ThroughLoadDataTestCase(TestCase): fixtures = ["m2m_through"] diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 33898cc1d5..b798cb21aa 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -17,6 +17,7 @@ from django.core.mail.backends import console, dummy, locmem, filebased, smtp from django.core.mail.message import BadHeaderError from django.test import TestCase from django.test.utils import override_settings +from django.utils.encoding import force_str, force_text from django.utils.six import PY3, StringIO from django.utils.translation import ugettext_lazy @@ -357,6 +358,14 @@ class BaseEmailBackendTests(object): self.assertEqual(message["from"], "from@example.com") self.assertEqual(message.get_all("to"), ["to@example.com"]) + def test_send_unicode(self): + email = EmailMessage('Chère maman', 'Je t\'aime très fort', 'from@example.com', ['to@example.com']) + num_sent = mail.get_connection().send_messages([email]) + self.assertEqual(num_sent, 1) + message = self.get_the_message() + self.assertEqual(message["subject"], '=?utf-8?q?Ch=C3=A8re_maman?=') + self.assertEqual(force_text(message.get_payload()), 'Je t\'aime très fort') + def test_send_many(self): email1 = EmailMessage('Subject', 'Content1', 'from@example.com', ['to@example.com']) email2 = EmailMessage('Subject', 'Content2', 'from@example.com', ['to@example.com']) @@ -526,8 +535,8 @@ class FileBackendTests(BaseEmailBackendTests, TestCase): messages = [] for filename in os.listdir(self.tmp_dir): with open(os.path.join(self.tmp_dir, filename), 'r') as fp: - session = fp.read().split('\n' + ('-' * 79) + '\n') - messages.extend(email.message_from_string(str(m)) for m in session if m) + session = force_text(fp.read()).split('\n' + ('-' * 79) + '\n') + messages.extend(email.message_from_string(force_str(m)) for m in session if m) return messages def test_file_sessions(self): @@ -579,8 +588,8 @@ class ConsoleBackendTests(BaseEmailBackendTests, TestCase): self.stream = sys.stdout = StringIO() def get_mailbox_content(self): - messages = self.stream.getvalue().split('\n' + ('-' * 79) + '\n') - return [email.message_from_string(str(m)) for m in messages if m] + messages = force_text(self.stream.getvalue()).split('\n' + ('-' * 79) + '\n') + return [email.message_from_string(force_str(m)) for m in messages if m] def test_console_stream_kwarg(self): """ diff --git a/tests/regressiontests/managers_regress/tests.py b/tests/regressiontests/managers_regress/tests.py index f3721a4c01..45059be4e5 100644 --- a/tests/regressiontests/managers_regress/tests.py +++ b/tests/regressiontests/managers_regress/tests.py @@ -61,7 +61,8 @@ class ManagersRegressionTests(TestCase): self.assertQuerysetEqual(Child4.manager1.all(), [ "<Child4: d1>", "<Child4: f1>" - ] + ], + ordered=False ) self.assertQuerysetEqual(Child5._default_manager.all(), ["<Child5: fred>"]) self.assertQuerysetEqual(Child6._default_manager.all(), ["<Child6: f1>"]) diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index de901f4a80..b8cffd9c92 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -294,6 +294,15 @@ class CommonMiddlewareTest(TestCase): CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 0) + # Other tests + + def test_non_ascii_query_string_does_not_crash(self): + """Regression test for #15152""" + request = self._get_request('slash') + request.META['QUERY_STRING'] = 'drink=café' + response = CommonMiddleware().process_request(request) + self.assertEqual(response.status_code, 301) + class ConditionalGetMiddlewareTest(TestCase): urls = 'regressiontests.middleware.cond_get_urls' diff --git a/tests/regressiontests/model_fields/imagefield.py b/tests/regressiontests/model_fields/imagefield.py index 7446f222ff..df0215db3d 100644 --- a/tests/regressiontests/model_fields/imagefield.py +++ b/tests/regressiontests/model_fields/imagefield.py @@ -6,6 +6,7 @@ import shutil from django.core.files import File from django.core.files.images import ImageFile from django.test import TestCase +from django.utils._os import upath from django.utils.unittest import skipIf from .models import Image @@ -43,10 +44,10 @@ class ImageFieldTestMixin(object): shutil.rmtree(temp_storage_dir) os.mkdir(temp_storage_dir) - file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png") + file_path1 = os.path.join(os.path.dirname(upath(__file__)), "4x8.png") self.file1 = self.File(open(file_path1, 'rb')) - file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png") + file_path2 = os.path.join(os.path.dirname(upath(__file__)), "8x4.png") self.file2 = self.File(open(file_path2, 'rb')) def tearDown(self): diff --git a/tests/regressiontests/model_forms_regress/models.py b/tests/regressiontests/model_forms_regress/models.py index f6e08d24dc..2c2fd39158 100644 --- a/tests/regressiontests/model_forms_regress/models.py +++ b/tests/regressiontests/model_forms_regress/models.py @@ -5,6 +5,7 @@ import os from django.core.exceptions import ValidationError from django.db import models from django.utils.encoding import python_2_unicode_compatible +from django.utils._os import upath class Person(models.Model): @@ -19,7 +20,7 @@ class Triple(models.Model): unique_together = (('left', 'middle'), ('middle', 'right')) class FilePathModel(models.Model): - path = models.FilePathField(path=os.path.dirname(__file__), match=".*\.py$", blank=True) + path = models.FilePathField(path=os.path.dirname(upath(__file__)), match=".*\.py$", blank=True) @python_2_unicode_compatible class Publication(models.Model): diff --git a/tests/regressiontests/model_formsets_regress/tests.py b/tests/regressiontests/model_formsets_regress/tests.py index 1fbdb9744f..8cadcfc409 100644 --- a/tests/regressiontests/model_formsets_regress/tests.py +++ b/tests/regressiontests/model_formsets_regress/tests.py @@ -351,7 +351,7 @@ class FormfieldShouldDeleteFormTests(TestCase): def should_delete(self): """ delete form if odd PK """ - return self.instance.id % 2 != 0 + return self.instance.pk % 2 != 0 NormalFormset = modelformset_factory(User, form=CustomDeleteUserForm, can_delete=True) DeleteFormset = modelformset_factory(User, form=CustomDeleteUserForm, formset=BaseCustomDeleteModelFormSet) @@ -392,7 +392,7 @@ class FormfieldShouldDeleteFormTests(TestCase): data = dict(self.data) data['form-INITIAL_FORMS'] = 4 data.update(dict( - ('form-%d-id' % i, user.id) + ('form-%d-id' % i, user.pk) for i,user in enumerate(User.objects.all()) )) formset = self.NormalFormset(data, queryset=User.objects.all()) @@ -409,7 +409,7 @@ class FormfieldShouldDeleteFormTests(TestCase): data = dict(self.data) data['form-INITIAL_FORMS'] = 4 data.update(dict( - ('form-%d-id' % i, user.id) + ('form-%d-id' % i, user.pk) for i,user in enumerate(User.objects.all()) )) data.update(self.delete_all_ids) @@ -428,7 +428,7 @@ class FormfieldShouldDeleteFormTests(TestCase): data = dict(self.data) data['form-INITIAL_FORMS'] = 4 data.update(dict( - ('form-%d-id' % i, user.id) + ('form-%d-id' % i, user.pk) for i,user in enumerate(User.objects.all()) )) data.update(self.delete_all_ids) @@ -440,5 +440,5 @@ class FormfieldShouldDeleteFormTests(TestCase): self.assertEqual(len(User.objects.all()), 2) # verify no "odd" PKs left - odd_ids = [user.id for user in User.objects.all() if user.id % 2] + odd_ids = [user.pk for user in User.objects.all() if user.pk % 2] self.assertEqual(len(odd_ids), 0) diff --git a/tests/regressiontests/model_regress/tests.py b/tests/regressiontests/model_regress/tests.py index 6a45a83052..c90fe98658 100644 --- a/tests/regressiontests/model_regress/tests.py +++ b/tests/regressiontests/model_regress/tests.py @@ -71,7 +71,8 @@ class ModelTests(TestCase): datetime.date(1999, 12, 31), datetime.date(1998, 12, 31), ], - attrgetter("when") + attrgetter("when"), + ordered=False ) self.assertQuerysetEqual( Party.objects.filter(when__year=1998), [ @@ -85,14 +86,16 @@ class ModelTests(TestCase): datetime.date(1999, 12, 31), datetime.date(1998, 12, 31), ], - attrgetter("when") + attrgetter("when"), + ordered=False ) self.assertQuerysetEqual( Party.objects.filter(when__month="12"), [ datetime.date(1999, 12, 31), datetime.date(1998, 12, 31), ], - attrgetter("when") + attrgetter("when"), + ordered=False ) self.assertQuerysetEqual( Party.objects.filter(when__year="1998"), [ diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py index 79fe6beebd..a903ea11a0 100644 --- a/tests/regressiontests/multiple_database/tests.py +++ b/tests/regressiontests/multiple_database/tests.py @@ -16,16 +16,6 @@ from django.utils.six import StringIO from .models import Book, Person, Pet, Review, UserProfile -def copy_content_types_from_default_to_other(): - # On post_syncdb, content types are created in the 'default' database. - # However, tests of generic foreign keys require them in 'other' too. - # The problem is masked on backends that defer constraints checks: at the - # end of each test, there's a rollback, and constraints are never checked. - # It only appears on MySQL + InnoDB. - for ct in ContentType.objects.using('default').all(): - ct.save(using='other') - - class QueryTestCase(TestCase): multi_db = True @@ -705,8 +695,6 @@ class QueryTestCase(TestCase): def test_generic_key_separation(self): "Generic fields are constrained to a single database" - copy_content_types_from_default_to_other() - # Create a book and author on the default database pro = Book.objects.create(title="Pro Django", published=datetime.date(2008, 12, 16)) @@ -734,8 +722,6 @@ class QueryTestCase(TestCase): def test_generic_key_reverse_operations(self): "Generic reverse manipulations are all constrained to a single DB" - copy_content_types_from_default_to_other() - dive = Book.objects.using('other').create(title="Dive into Python", published=datetime.date(2009, 5, 4)) @@ -780,8 +766,6 @@ class QueryTestCase(TestCase): def test_generic_key_cross_database_protection(self): "Operations that involve sharing generic key objects across databases raise an error" - copy_content_types_from_default_to_other() - # Create a book and author on the default database pro = Book.objects.create(title="Pro Django", published=datetime.date(2008, 12, 16)) @@ -833,8 +817,6 @@ class QueryTestCase(TestCase): def test_generic_key_deletion(self): "Cascaded deletions of Generic Key relations issue queries on the right database" - copy_content_types_from_default_to_other() - dive = Book.objects.using('other').create(title="Dive into Python", published=datetime.date(2009, 5, 4)) review = Review.objects.using('other').create(source="Python Weekly", content_object=dive) @@ -1402,8 +1384,6 @@ class RouterTestCase(TestCase): def test_generic_key_cross_database_protection(self): "Generic Key operations can span databases if they share a source" - copy_content_types_from_default_to_other() - # Create a book and author on the default database pro = Book.objects.using('default' ).create(title="Pro Django", published=datetime.date(2008, 12, 16)) @@ -1515,8 +1495,6 @@ class RouterTestCase(TestCase): def test_generic_key_managers(self): "Generic key relations are represented by managers, and can be controlled like managers" - copy_content_types_from_default_to_other() - pro = Book.objects.using('other').create(title="Pro Django", published=datetime.date(2008, 12, 16)) @@ -1922,3 +1900,39 @@ class RouterModelArgumentTestCase(TestCase): pet = Pet.objects.create(owner=person, name='Wart') # test related FK collection person.delete() + + +class SyncOnlyDefaultDatabaseRouter(object): + def allow_syncdb(self, db, model): + return db == DEFAULT_DB_ALIAS + + +class SyncDBTestCase(TestCase): + multi_db = True + + def test_syncdb_to_other_database(self): + """Regression test for #16039: syncdb with --database option.""" + cts = ContentType.objects.using('other').filter(app_label='multiple_database') + + count = cts.count() + self.assertGreater(count, 0) + + cts.delete() + management.call_command('syncdb', verbosity=0, interactive=False, + load_initial_data=False, database='other') + self.assertEqual(cts.count(), count) + + def test_syncdb_to_other_database_with_router(self): + """Regression test for #16039: syncdb with --database option.""" + cts = ContentType.objects.using('other').filter(app_label='multiple_database') + + cts.delete() + try: + old_routers = router.routers + router.routers = [SyncOnlyDefaultDatabaseRouter()] + management.call_command('syncdb', verbosity=0, interactive=False, + load_initial_data=False, database='other') + finally: + router.routers = old_routers + + self.assertEqual(cts.count(), 0) diff --git a/tests/regressiontests/pagination_regress/__init__.py b/tests/regressiontests/pagination/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/regressiontests/pagination_regress/__init__.py +++ b/tests/regressiontests/pagination/__init__.py diff --git a/tests/regressiontests/pagination/custom.py b/tests/regressiontests/pagination/custom.py new file mode 100644 index 0000000000..47a932c7e3 --- /dev/null +++ b/tests/regressiontests/pagination/custom.py @@ -0,0 +1,20 @@ +from django.core.paginator import Paginator, Page + + +class ValidAdjacentNumsPage(Page): + + def next_page_number(self): + if not self.has_next(): + return None + return super(ValidAdjacentNumsPage, self).next_page_number() + + def previous_page_number(self): + if not self.has_previous(): + return None + return super(ValidAdjacentNumsPage, self).previous_page_number() + + +class ValidAdjacentNumsPaginator(Paginator): + + def _get_page(self, *args, **kwargs): + return ValidAdjacentNumsPage(*args, **kwargs) diff --git a/tests/modeltests/pagination/models.py b/tests/regressiontests/pagination/models.py index 779d3029ba..9dc8d4b776 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/regressiontests/pagination/models.py @@ -1,11 +1,3 @@ -""" -30. Object pagination - -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. -""" - from django.db import models from django.utils.encoding import python_2_unicode_compatible diff --git a/tests/regressiontests/pagination_regress/tests.py b/tests/regressiontests/pagination/tests.py index e98352e006..dea5756672 100644 --- a/tests/regressiontests/pagination_regress/tests.py +++ b/tests/regressiontests/pagination/tests.py @@ -1,9 +1,18 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.utils.unittest import TestCase +from datetime import datetime -class PaginatorTests(TestCase): +from django.core.paginator import (Paginator, EmptyPage, InvalidPage, + PageNotAnInteger) +from django.test import TestCase +from django.utils import six +from django.utils import unittest + +from .models import Article +from .custom import ValidAdjacentNumsPaginator + + +class PaginationTests(unittest.TestCase): """ Tests for the Paginator and Page classes. """ @@ -31,15 +40,6 @@ class PaginatorTests(TestCase): "For '%s', expected %s but got %s. Paginator parameters were: %s" % (name, expected, got, params)) - def test_invalid_page_number(self): - """ - Tests that invalid page numbers result in the correct exception being - raised. - """ - paginator = Paginator([1, 2, 3], 2) - self.assertRaises(PageNotAnInteger, paginator.validate_number, None) - self.assertRaises(PageNotAnInteger, paginator.validate_number, 'x') - def test_paginator(self): """ Tests the paginator attributes using varying inputs. @@ -107,6 +107,38 @@ class PaginatorTests(TestCase): for params, output in tests: self.check_paginator(params, output) + def test_invalid_page_number(self): + """ + Tests that invalid page numbers result in the correct exception being + raised. + """ + paginator = Paginator([1, 2, 3], 2) + self.assertRaises(InvalidPage, paginator.page, 3) + self.assertRaises(PageNotAnInteger, paginator.validate_number, None) + self.assertRaises(PageNotAnInteger, paginator.validate_number, 'x') + # With no content and allow_empty_first_page=True, 1 is a valid page number + paginator = Paginator([], 2) + self.assertEqual(paginator.validate_number(1), 1) + + def test_paginate_misc_classes(self): + class CountContainer(object): + def count(self): + return 42 + # Paginator can be passed other objects with a count() method. + paginator = Paginator(CountContainer(), 10) + self.assertEqual(42, paginator.count) + self.assertEqual(5, paginator.num_pages) + self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range)) + + # Paginator can be passed other objects that implement __len__. + class LenContainer(object): + def __len__(self): + return 42 + paginator = Paginator(LenContainer(), 10) + self.assertEqual(42, paginator.count) + self.assertEqual(5, paginator.num_pages) + self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range)) + def check_indexes(self, params, page_num, indexes): """ Helper method that instantiates a Paginator object from the passed @@ -168,6 +200,7 @@ class PaginatorTests(TestCase): 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) @@ -184,3 +217,91 @@ class PaginatorTests(TestCase): self.assertFalse('a' in page2) self.assertEqual(''.join(page2), 'fghijk') self.assertEqual(''.join(reversed(page2)), 'kjihgf') + + def test_get_page_hook(self): + """ + Tests that a Paginator subclass can use the ``_get_page`` hook to + return an alternative to the standard Page class. + """ + eleven = 'abcdefghijk' + paginator = ValidAdjacentNumsPaginator(eleven, per_page=6) + page1 = paginator.page(1) + page2 = paginator.page(2) + self.assertIsNone(page1.previous_page_number()) + self.assertEqual(page1.next_page_number(), 2) + self.assertEqual(page2.previous_page_number(), 1) + self.assertIsNone(page2.next_page_number()) + + +class ModelPaginationTests(TestCase): + """ + Test pagination with Django model instances + """ + def setUp(self): + # Prepare a list of objects for pagination. + for x in range(1, 10): + a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) + a.save() + + def test_first_page(self): + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(1) + self.assertEqual("<Page 1 of 2>", six.text_type(p)) + self.assertQuerysetEqual(p.object_list, [ + "<Article: Article 1>", + "<Article: Article 2>", + "<Article: Article 3>", + "<Article: Article 4>", + "<Article: Article 5>" + ], + ordered=False + ) + self.assertTrue(p.has_next()) + self.assertFalse(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(2, p.next_page_number()) + self.assertRaises(InvalidPage, p.previous_page_number) + self.assertEqual(1, p.start_index()) + self.assertEqual(5, p.end_index()) + + def test_last_page(self): + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(2) + self.assertEqual("<Page 2 of 2>", six.text_type(p)) + self.assertQuerysetEqual(p.object_list, [ + "<Article: Article 6>", + "<Article: Article 7>", + "<Article: Article 8>", + "<Article: Article 9>" + ], + ordered=False + ) + self.assertFalse(p.has_next()) + self.assertTrue(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertRaises(InvalidPage, p.next_page_number) + self.assertEqual(1, p.previous_page_number()) + self.assertEqual(6, p.start_index()) + self.assertEqual(9, p.end_index()) + + def test_page_getitem(self): + """ + Tests proper behaviour of a paginator page __getitem__ (queryset + evaluation, slicing, exception raised). + """ + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(1) + + # Make sure object_list queryset is not evaluated by an invalid __getitem__ call. + # (this happens from the template engine when using eg: {% page_obj.has_previous %}) + self.assertIsNone(p.object_list._result_cache) + self.assertRaises(TypeError, lambda: p['has_previous']) + self.assertIsNone(p.object_list._result_cache) + + # Make sure slicing the Page object with numbers and slice objects work. + self.assertEqual(p[0], Article.objects.get(headline='Article 1')) + self.assertQuerysetEqual(p[slice(2)], [ + "<Article: Article 1>", + "<Article: Article 2>", + ] + ) diff --git a/tests/regressiontests/pagination_regress/models.py b/tests/regressiontests/pagination_regress/models.py deleted file mode 100644 index cde172db68..0000000000 --- a/tests/regressiontests/pagination_regress/models.py +++ /dev/null @@ -1 +0,0 @@ -# Models file for tests to run. diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index f0178a0256..73b9762150 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -283,6 +283,7 @@ class SingleObject(models.Model): class RelatedObject(models.Model): single = models.ForeignKey(SingleObject, null=True) + f = models.IntegerField(null=True) class Meta: ordering = ['single'] @@ -311,7 +312,7 @@ class Food(models.Model): @python_2_unicode_compatible class Eaten(models.Model): - food = models.ForeignKey(Food, to_field="name") + food = models.ForeignKey(Food, to_field="name", null=True) meal = models.CharField(max_length=20) def __str__(self): @@ -400,3 +401,23 @@ class ModelA(models.Model): name = models.TextField() b = models.ForeignKey(ModelB, null=True) d = models.ForeignKey(ModelD) + +@python_2_unicode_compatible +class Job(models.Model): + name = models.CharField(max_length=20, unique=True) + + def __str__(self): + return self.name + +class JobResponsibilities(models.Model): + job = models.ForeignKey(Job, to_field='name') + responsibility = models.ForeignKey('Responsibility', to_field='description') + +@python_2_unicode_compatible +class Responsibility(models.Model): + description = models.CharField(max_length=20, unique=True) + jobs = models.ManyToManyField(Job, through=JobResponsibilities, + related_name='responsibilities') + + def __str__(self): + return self.description diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index c2a4ad1caf..75e27769b4 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -23,7 +23,8 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, SpecialCategory, OneToOneCategory, NullableName, ProxyCategory, - SingleObject, RelatedObject, ModelA, ModelD) + SingleObject, RelatedObject, ModelA, ModelD, Responsibility, Job, + JobResponsibilities) class BaseQuerysetTest(TestCase): @@ -243,7 +244,10 @@ class Queries1Tests(BaseQuerysetTest): q1 = Item.objects.order_by('name') q2 = Item.objects.filter(id=self.i1.id) list(q2) - self.assertEqual(len((q1 & q2).order_by('name').query.tables), 1) + combined_query = (q1 & q2).order_by('name').query + self.assertEqual(len([ + t for t in combined_query.tables if combined_query.alias_refcount[t] + ]), 1) def test_order_by_join_unref(self): """ @@ -841,11 +845,14 @@ class Queries1Tests(BaseQuerysetTest): """ original_ordering = Tag._meta.ordering Tag._meta.ordering = None - self.assertQuerysetEqual( - Tag.objects.all(), - ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'], - ) - Tag._meta.ordering = original_ordering + try: + self.assertQuerysetEqual( + Tag.objects.all(), + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'], + ordered=False + ) + finally: + Tag._meta.ordering = original_ordering def test_exclude(self): self.assertQuerysetEqual( @@ -880,6 +887,225 @@ class Queries1Tests(BaseQuerysetTest): Item.objects.filter(Q(tags__name__in=['t4', 't3'])), [repr(i) for i in Item.objects.filter(~~Q(tags__name__in=['t4', 't3']))]) + def test_ticket_10790_1(self): + # Querying direct fields with isnull should trim the left outer join. + # It also should not create INNER JOIN. + q = Tag.objects.filter(parent__isnull=True) + + self.assertQuerysetEqual(q, ['<Tag: t1>']) + self.assertTrue('JOIN' not in str(q.query)) + + q = Tag.objects.filter(parent__isnull=False) + + self.assertQuerysetEqual( + q, + ['<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'], + ) + self.assertTrue('JOIN' not in str(q.query)) + + q = Tag.objects.exclude(parent__isnull=True) + self.assertQuerysetEqual( + q, + ['<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'], + ) + self.assertTrue('JOIN' not in str(q.query)) + + q = Tag.objects.exclude(parent__isnull=False) + self.assertQuerysetEqual(q, ['<Tag: t1>']) + self.assertTrue('JOIN' not in str(q.query)) + + q = Tag.objects.exclude(parent__parent__isnull=False) + + self.assertQuerysetEqual( + q, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'], + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1) + self.assertTrue('INNER JOIN' not in str(q.query)) + + def test_ticket_10790_2(self): + # Querying across several tables should strip only the last outer join, + # while preserving the preceeding inner joins. + q = Tag.objects.filter(parent__parent__isnull=False) + + self.assertQuerysetEqual( + q, + ['<Tag: t4>', '<Tag: t5>'], + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 1) + + # Querying without isnull should not convert anything to left outer join. + q = Tag.objects.filter(parent__parent=self.t1) + self.assertQuerysetEqual( + q, + ['<Tag: t4>', '<Tag: t5>'], + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 1) + + def test_ticket_10790_3(self): + # Querying via indirect fields should populate the left outer join + q = NamedCategory.objects.filter(tag__isnull=True) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1) + # join to dumbcategory ptr_id + self.assertTrue(str(q.query).count('INNER JOIN') == 1) + self.assertQuerysetEqual(q, []) + + # Querying across several tables should strip only the last join, while + # preserving the preceding left outer joins. + q = NamedCategory.objects.filter(tag__parent__isnull=True) + self.assertTrue(str(q.query).count('INNER JOIN') == 1) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1) + self.assertQuerysetEqual( q, ['<NamedCategory: NamedCategory object>']) + + def test_ticket_10790_4(self): + # Querying across m2m field should not strip the m2m table from join. + q = Author.objects.filter(item__tags__isnull=True) + self.assertQuerysetEqual( + q, + ['<Author: a2>', '<Author: a3>'], + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 2) + self.assertTrue('INNER JOIN' not in str(q.query)) + + q = Author.objects.filter(item__tags__parent__isnull=True) + self.assertQuerysetEqual( + q, + ['<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a3>'], + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 3) + self.assertTrue('INNER JOIN' not in str(q.query)) + + def test_ticket_10790_5(self): + # Querying with isnull=False across m2m field should not create outer joins + q = Author.objects.filter(item__tags__isnull=False) + self.assertQuerysetEqual( + q, + ['<Author: a1>', '<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a4>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 2) + + q = Author.objects.filter(item__tags__parent__isnull=False) + self.assertQuerysetEqual( + q, + ['<Author: a1>', '<Author: a2>', '<Author: a4>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 3) + + q = Author.objects.filter(item__tags__parent__parent__isnull=False) + self.assertQuerysetEqual( + q, + ['<Author: a4>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 4) + + def test_ticket_10790_6(self): + # Querying with isnull=True across m2m field should not create inner joins + # and strip last outer join + q = Author.objects.filter(item__tags__parent__parent__isnull=True) + self.assertQuerysetEqual( + q, + ['<Author: a1>', '<Author: a1>', '<Author: a2>', '<Author: a2>', + '<Author: a2>', '<Author: a3>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 4) + self.assertTrue(str(q.query).count('INNER JOIN') == 0) + + q = Author.objects.filter(item__tags__parent__isnull=True) + self.assertQuerysetEqual( + q, + ['<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a3>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 3) + self.assertTrue(str(q.query).count('INNER JOIN') == 0) + + def test_ticket_10790_7(self): + # Reverse querying with isnull should not strip the join + q = Author.objects.filter(item__isnull=True) + self.assertQuerysetEqual( + q, + ['<Author: a3>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1) + self.assertTrue(str(q.query).count('INNER JOIN') == 0) + + q = Author.objects.filter(item__isnull=False) + self.assertQuerysetEqual( + q, + ['<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a4>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 1) + + def test_ticket_10790_8(self): + # Querying with combined q-objects should also strip the left outer join + q = Tag.objects.filter(Q(parent__isnull=True) | Q(parent=self.t1)) + self.assertQuerysetEqual( + q, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'] + ) + self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q.query).count('INNER JOIN') == 0) + + def test_ticket_10790_combine(self): + # Combining queries should not re-populate the left outer join + q1 = Tag.objects.filter(parent__isnull=True) + q2 = Tag.objects.filter(parent__isnull=False) + + q3 = q1 | q2 + self.assertQuerysetEqual( + q3, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'], + ) + self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q3.query).count('INNER JOIN') == 0) + + q3 = q1 & q2 + self.assertQuerysetEqual(q3, []) + self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q3.query).count('INNER JOIN') == 0) + + q2 = Tag.objects.filter(parent=self.t1) + q3 = q1 | q2 + self.assertQuerysetEqual( + q3, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'] + ) + self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q3.query).count('INNER JOIN') == 0) + + q3 = q2 | q1 + self.assertQuerysetEqual( + q3, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'] + ) + self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0) + self.assertTrue(str(q3.query).count('INNER JOIN') == 0) + + q1 = Tag.objects.filter(parent__isnull=True) + q2 = Tag.objects.filter(parent__parent__isnull=True) + + q3 = q1 | q2 + self.assertQuerysetEqual( + q3, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'] + ) + self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 1) + self.assertTrue(str(q3.query).count('INNER JOIN') == 0) + + q3 = q2 | q1 + self.assertQuerysetEqual( + q3, + ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'] + ) + self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 1) + self.assertTrue(str(q3.query).count('INNER JOIN') == 0) + + class Queries2Tests(TestCase): def setUp(self): Number.objects.create(num=4) @@ -925,15 +1151,18 @@ class Queries2Tests(TestCase): self.assertQuerysetEqual(Number.objects.filter(num__gt=12.1), []) self.assertQuerysetEqual( Number.objects.filter(num__lt=12), - ['<Number: 4>', '<Number: 8>'] + ['<Number: 4>', '<Number: 8>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__lt=12.0), - ['<Number: 4>', '<Number: 8>'] + ['<Number: 4>', '<Number: 8>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__lt=12.1), - ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ['<Number: 4>', '<Number: 8>', '<Number: 12>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__gte=11.9), @@ -951,23 +1180,28 @@ class Queries2Tests(TestCase): self.assertQuerysetEqual(Number.objects.filter(num__gte=12.9), []) self.assertQuerysetEqual( Number.objects.filter(num__lte=11.9), - ['<Number: 4>', '<Number: 8>'] + ['<Number: 4>', '<Number: 8>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__lte=12), - ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ['<Number: 4>', '<Number: 8>', '<Number: 12>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__lte=12.0), - ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ['<Number: 4>', '<Number: 8>', '<Number: 12>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__lte=12.1), - ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ['<Number: 4>', '<Number: 8>', '<Number: 12>'], + ordered=False ) self.assertQuerysetEqual( Number.objects.filter(num__lte=12.9), - ['<Number: 4>', '<Number: 8>', '<Number: 12>'] + ['<Number: 4>', '<Number: 8>', '<Number: 12>'], + ordered=False ) def test_ticket7411(self): @@ -1026,14 +1260,18 @@ class Queries4Tests(BaseQuerysetTest): Item.objects.create(name='i2', created=datetime.datetime.now(), note=n1, creator=self.a3) def test_ticket14876(self): + # Note: when combining the query we need to have information available + # about the join type of the trimmed "creator__isnull" join. If we + # don't have that information, then the join is created as INNER JOIN + # and results will be incorrect. q1 = Report.objects.filter(Q(creator__isnull=True) | Q(creator__extra__info='e1')) q2 = Report.objects.filter(Q(creator__isnull=True)) | Report.objects.filter(Q(creator__extra__info='e1')) - self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"]) + self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"], ordered=False) self.assertEqual(str(q1.query), str(q2.query)) q1 = Report.objects.filter(Q(creator__extra__info='e1') | Q(creator__isnull=True)) q2 = Report.objects.filter(Q(creator__extra__info='e1')) | Report.objects.filter(Q(creator__isnull=True)) - self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"]) + self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"], ordered=False) self.assertEqual(str(q1.query), str(q2.query)) q1 = Item.objects.filter(Q(creator=self.a1) | Q(creator__report__name='r1')).order_by() @@ -1046,6 +1284,18 @@ class Queries4Tests(BaseQuerysetTest): self.assertQuerysetEqual(q1, ["<Item: i1>"]) self.assertEqual(str(q1.query), str(q2.query)) + def test_combine_join_reuse(self): + # Test that we correctly recreate joins having identical connections + # in the rhs query, in case the query is ORed together. Related to + # ticket #18748 + Report.objects.create(name='r4', creator=self.a1) + q1 = Author.objects.filter(report__name='r5') + q2 = Author.objects.filter(report__name='r4').filter(report__name='r1') + combined = q1|q2 + self.assertEquals(str(combined.query).count('JOIN'), 2) + self.assertEquals(len(combined), 1) + self.assertEquals(combined[0].name, 'a1') + def test_ticket7095(self): # Updates that are filtered on the model being updated are somewhat # tricky in MySQL. This exercises that case. @@ -1382,17 +1632,19 @@ class NullableRelOrderingTests(TestCase): # the join type of already existing joins. Plaything.objects.create(name="p1") s = SingleObject.objects.create(name='s') - r = RelatedObject.objects.create(single=s) + r = RelatedObject.objects.create(single=s, f=1) Plaything.objects.create(name="p2", others=r) qs = Plaything.objects.all().filter(others__isnull=False).order_by('pk') + self.assertTrue('JOIN' not in str(qs.query)) + qs = Plaything.objects.all().filter(others__f__isnull=False).order_by('pk') self.assertTrue('INNER' in str(qs.query)) qs = qs.order_by('others__single__name') # The ordering by others__single__pk will add one new join (to single) # and that join must be LEFT join. The already existing join to related # objects must be kept INNER. So, we have both a INNER and a LEFT join # in the query. - self.assertTrue('LEFT' in str(qs.query)) - self.assertTrue('INNER' in str(qs.query)) + self.assertEquals(str(qs.query).count('LEFT'), 1) + self.assertEquals(str(qs.query).count('INNER'), 1) self.assertQuerysetEqual( qs, ['<Plaything: p2>'] @@ -1443,6 +1695,7 @@ class Queries6Tests(TestCase): # This next test used to cause really weird PostgreSQL behavior, but it was # only apparent much later when the full test suite ran. + # - Yeah, it leaves global ITER_CHUNK_SIZE to 2 instead of 100... #@unittest.expectedFailure def test_slicing_and_cache_interaction(self): # We can do slicing beyond what is currently in the result cache, @@ -1970,6 +2223,29 @@ class DefaultValuesInsertTest(TestCase): except TypeError: self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)") +class ExcludeTest(TestCase): + def setUp(self): + f1 = Food.objects.create(name='apples') + Food.objects.create(name='oranges') + Eaten.objects.create(food=f1, meal='dinner') + j1 = Job.objects.create(name='Manager') + r1 = Responsibility.objects.create(description='Playing golf') + j2 = Job.objects.create(name='Programmer') + r2 = Responsibility.objects.create(description='Programming') + JobResponsibilities.objects.create(job=j1, responsibility=r1) + JobResponsibilities.objects.create(job=j2, responsibility=r2) + + def test_to_field(self): + self.assertQuerysetEqual( + Food.objects.exclude(eaten__meal='dinner'), + ['<Food: oranges>']) + self.assertQuerysetEqual( + Job.objects.exclude(responsibilities__description='Playing golf'), + ['<Job: Programmer>']) + self.assertQuerysetEqual( + Responsibility.objects.exclude(jobs__name='Manager'), + ['<Responsibility: Programming>']) + class NullInExcludeTest(TestCase): def setUp(self): NullableName.objects.create(name='i1') @@ -2132,3 +2408,13 @@ class NullJoinPromotionOrTest(TestCase): # so we can use INNER JOIN for it. However, we can NOT use INNER JOIN # for the b->c join, as a->b is nullable. self.assertEqual(str(qset.query).count('INNER JOIN'), 1) + +class ReverseJoinTrimmingTest(TestCase): + def test_reverse_trimming(self): + # Check that we don't accidentally trim reverse joins - we can't know + # if there is anything on the other side of the join, so trimming + # reverse joins can't be done, ever. + t = Tag.objects.create() + qs = Tag.objects.filter(annotation__tag=t.pk) + self.assertIn('INNER JOIN', str(qs.query)) + self.assertEquals(list(qs), []) diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index 6522620d5f..adf824dff7 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -1,3 +1,4 @@ +# -*- encoding: utf-8 -*- from __future__ import unicode_literals import time @@ -11,7 +12,7 @@ from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_r from django.test.client import FakePayload from django.test.utils import override_settings, str_prefix from django.utils import unittest -from django.utils.http import cookie_date +from django.utils.http import cookie_date, urlencode from django.utils.timezone import utc @@ -115,13 +116,15 @@ class RequestsTests(unittest.TestCase): '12.34.56.78:443', '[2001:19f0:feee::dead:beef:cafe]', '[2001:19f0:feee::dead:beef:cafe]:8080', + 'xn--4ca9at.com', # Punnycode for öäü.com ] poisoned_hosts = [ 'example.com@evil.tld', 'example.com:dr.frankenstein@evil.tld', - 'example.com:someone@somestie.com:80', - 'example.com:80/badpath' + 'example.com:dr.frankenstein@evil.tld:80', + 'example.com:80/badpath', + 'example.com: recovermypassword.com', ] for host in legit_hosts: @@ -185,13 +188,15 @@ class RequestsTests(unittest.TestCase): '12.34.56.78:443', '[2001:19f0:feee::dead:beef:cafe]', '[2001:19f0:feee::dead:beef:cafe]:8080', + 'xn--4ca9at.com', # Punnycode for öäü.com ] poisoned_hosts = [ 'example.com@evil.tld', 'example.com:dr.frankenstein@evil.tld', 'example.com:dr.frankenstein@evil.tld:80', - 'example.com:80/badpath' + 'example.com:80/badpath', + 'example.com: recovermypassword.com', ] for host in legit_hosts: @@ -352,6 +357,30 @@ class RequestsTests(unittest.TestCase): self.assertRaises(Exception, lambda: request.body) self.assertEqual(request.POST, {}) + def test_non_ascii_POST(self): + payload = FakePayload(urlencode({'key': 'España'})) + request = WSGIRequest({ + 'REQUEST_METHOD': 'POST', + 'CONTENT_LENGTH': len(payload), + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', + 'wsgi.input': payload, + }) + self.assertEqual(request.POST, {'key': ['España']}) + + def test_alternate_charset_POST(self): + """ + Test a POST with non-utf-8 payload encoding. + """ + from django.utils.http import urllib_parse + payload = FakePayload(urllib_parse.urlencode({'key': 'España'.encode('latin-1')})) + request = WSGIRequest({ + 'REQUEST_METHOD': 'POST', + 'CONTENT_LENGTH': len(payload), + 'CONTENT_TYPE': 'application/x-www-form-urlencoded; charset=iso-8859-1', + 'wsgi.input': payload, + }) + self.assertEqual(request.POST, {'key': ['España']}) + def test_body_after_POST_multipart(self): """ Reading body after parsing multipart is not allowed diff --git a/tests/regressiontests/select_related_onetoone/models.py b/tests/regressiontests/select_related_onetoone/models.py index 3284defb11..d32faafbb9 100644 --- a/tests/regressiontests/select_related_onetoone/models.py +++ b/tests/regressiontests/select_related_onetoone/models.py @@ -51,6 +51,7 @@ class StatDetails(models.Model): class AdvancedUserStat(UserStat): karma = models.IntegerField() + class Image(models.Model): name = models.CharField(max_length=100) @@ -58,3 +59,44 @@ class Image(models.Model): class Product(models.Model): name = models.CharField(max_length=100) image = models.OneToOneField(Image, null=True) + + +@python_2_unicode_compatible +class Parent1(models.Model): + name1 = models.CharField(max_length=50) + + def __str__(self): + return self.name1 + + +@python_2_unicode_compatible +class Parent2(models.Model): + # Avoid having two "id" fields in the Child1 subclass + id2 = models.AutoField(primary_key=True) + name2 = models.CharField(max_length=50) + + def __str__(self): + return self.name2 + + +@python_2_unicode_compatible +class Child1(Parent1, Parent2): + value = models.IntegerField() + + def __str__(self): + return self.name1 + + +@python_2_unicode_compatible +class Child2(Parent1): + parent2 = models.OneToOneField(Parent2) + value = models.IntegerField() + + def __str__(self): + return self.name1 + +class Child3(Child2): + value3 = models.IntegerField() + +class Child4(Child1): + value4 = models.IntegerField() diff --git a/tests/regressiontests/select_related_onetoone/tests.py b/tests/regressiontests/select_related_onetoone/tests.py index 1373f04717..d4a1275e49 100644 --- a/tests/regressiontests/select_related_onetoone/tests.py +++ b/tests/regressiontests/select_related_onetoone/tests.py @@ -1,9 +1,11 @@ from __future__ import absolute_import from django.test import TestCase +from django.utils import unittest from .models import (User, UserProfile, UserStat, UserStatResult, StatDetails, - AdvancedUserStat, Image, Product) + AdvancedUserStat, Image, Product, Parent1, Parent2, Child1, Child2, Child3, + Child4) class ReverseSelectRelatedTestCase(TestCase): @@ -21,6 +23,14 @@ class ReverseSelectRelatedTestCase(TestCase): advstat = AdvancedUserStat.objects.create(user=user2, posts=200, karma=5, results=results2) StatDetails.objects.create(base_stats=advstat, comments=250) + p1 = Parent1(name1="Only Parent1") + p1.save() + c1 = Child1(name1="Child1 Parent1", name2="Child1 Parent2", value=1) + c1.save() + p2 = Parent2(name2="Child2 Parent2") + p2.save() + c2 = Child2(name1="Child2 Parent1", parent2=p2, value=2) + c2.save() def test_basic(self): with self.assertNumQueries(1): @@ -108,3 +118,93 @@ class ReverseSelectRelatedTestCase(TestCase): image = Image.objects.select_related('product').get() with self.assertRaises(Product.DoesNotExist): image.product + + def test_parent_only(self): + with self.assertNumQueries(1): + p = Parent1.objects.select_related('child1').get(name1="Only Parent1") + with self.assertNumQueries(0): + with self.assertRaises(Child1.DoesNotExist): + p.child1 + + def test_multiple_subclass(self): + with self.assertNumQueries(1): + p = Parent1.objects.select_related('child1').get(name1="Child1 Parent1") + self.assertEqual(p.child1.name2, 'Child1 Parent2') + + def test_onetoone_with_subclass(self): + with self.assertNumQueries(1): + p = Parent2.objects.select_related('child2').get(name2="Child2 Parent2") + self.assertEqual(p.child2.name1, 'Child2 Parent1') + + def test_onetoone_with_two_subclasses(self): + with self.assertNumQueries(1): + p = Parent2.objects.select_related('child2', "child2__child3").get(name2="Child2 Parent2") + self.assertEqual(p.child2.name1, 'Child2 Parent1') + with self.assertRaises(Child3.DoesNotExist): + p.child2.child3 + p3 = Parent2(name2="Child3 Parent2") + p3.save() + c2 = Child3(name1="Child3 Parent1", parent2=p3, value=2, value3=3) + c2.save() + with self.assertNumQueries(1): + p = Parent2.objects.select_related('child2', "child2__child3").get(name2="Child3 Parent2") + self.assertEqual(p.child2.name1, 'Child3 Parent1') + self.assertEqual(p.child2.child3.value3, 3) + self.assertEqual(p.child2.child3.value, p.child2.value) + self.assertEqual(p.child2.name1, p.child2.child3.name1) + + def test_multiinheritance_two_subclasses(self): + with self.assertNumQueries(1): + p = Parent1.objects.select_related('child1', 'child1__child4').get(name1="Child1 Parent1") + self.assertEqual(p.child1.name2, 'Child1 Parent2') + self.assertEqual(p.child1.name1, p.name1) + with self.assertRaises(Child4.DoesNotExist): + p.child1.child4 + Child4(name1='n1', name2='n2', value=1, value4=4).save() + with self.assertNumQueries(1): + p = Parent2.objects.select_related('child1', 'child1__child4').get(name2="n2") + self.assertEqual(p.name2, 'n2') + self.assertEqual(p.child1.name1, 'n1') + self.assertEqual(p.child1.name2, p.name2) + self.assertEqual(p.child1.value, 1) + self.assertEqual(p.child1.child4.name1, p.child1.name1) + self.assertEqual(p.child1.child4.name2, p.child1.name2) + self.assertEqual(p.child1.child4.value, p.child1.value) + self.assertEqual(p.child1.child4.value4, 4) + + @unittest.expectedFailure + def test_inheritance_deferred(self): + c = Child4.objects.create(name1='n1', name2='n2', value=1, value4=4) + with self.assertNumQueries(1): + p = Parent2.objects.select_related('child1').only( + 'id2', 'child1__value').get(name2="n2") + self.assertEqual(p.id2, c.id2) + self.assertEqual(p.child1.value, 1) + p = Parent2.objects.select_related('child1').only( + 'id2', 'child1__value').get(name2="n2") + with self.assertNumQueries(1): + self.assertEquals(p.name2, 'n2') + p = Parent2.objects.select_related('child1').only( + 'id2', 'child1__value').get(name2="n2") + with self.assertNumQueries(1): + self.assertEquals(p.child1.name2, 'n2') + + @unittest.expectedFailure + def test_inheritance_deferred2(self): + c = Child4.objects.create(name1='n1', name2='n2', value=1, value4=4) + qs = Parent2.objects.select_related('child1', 'child4').only( + 'id2', 'child1__value', 'child1__child4__value4') + with self.assertNumQueries(1): + p = qs.get(name2="n2") + self.assertEqual(p.id2, c.id2) + self.assertEqual(p.child1.value, 1) + self.assertEqual(p.child1.child4.value4, 4) + self.assertEqual(p.child1.child4.id2, c.id2) + p = qs.get(name2="n2") + with self.assertNumQueries(1): + self.assertEquals(p.child1.name2, 'n2') + p = qs.get(name2="n2") + with self.assertNumQueries(1): + self.assertEquals(p.child1.name1, 'n1') + with self.assertNumQueries(1): + self.assertEquals(p.child1.child4.name1, 'n1') diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py index 52223e72d8..af261f9cbc 100644 --- a/tests/regressiontests/serializers_regress/models.py +++ b/tests/regressiontests/serializers_regress/models.py @@ -8,7 +8,6 @@ This class sets up a model for each model field type from django.db import models from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType -from django.contrib.localflavor.us.models import USStateField, PhoneNumberField # The following classes are for testing basic data # marshalling, including NULL values, where allowed. @@ -58,9 +57,6 @@ class GenericIPAddressData(models.Model): class NullBooleanData(models.Model): data = models.NullBooleanField(null=True) -class PhoneData(models.Model): - data = PhoneNumberField(null=True) - class PositiveIntegerData(models.Model): data = models.PositiveIntegerField(null=True) @@ -79,9 +75,6 @@ class TextData(models.Model): class TimeData(models.Model): data = models.TimeField(null=True) -class USStateData(models.Model): - data = USStateField(null=True) - class Tag(models.Model): """A tag on an item.""" data = models.SlugField() @@ -212,9 +205,6 @@ class GenericIPAddressPKData(models.Model): # class NullBooleanPKData(models.Model): # data = models.NullBooleanField(primary_key=True) -class PhonePKData(models.Model): - data = PhoneNumberField(primary_key=True) - class PositiveIntegerPKData(models.Model): data = models.PositiveIntegerField(primary_key=True) @@ -233,9 +223,6 @@ class SmallPKData(models.Model): # class TimePKData(models.Model): # data = models.TimeField(primary_key=True) -class USStatePKData(models.Model): - data = USStateField(primary_key=True) - class ComplexModel(models.Model): field1 = models.CharField(max_length=10) field2 = models.CharField(max_length=10) diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index f4b3adaa5e..2583a1ea04 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -28,14 +28,14 @@ from django.utils.unittest import skipUnless from .models import (BooleanData, CharData, DateData, DateTimeData, EmailData, FileData, FilePathData, DecimalData, FloatData, IntegerData, IPAddressData, - GenericIPAddressData, NullBooleanData, PhoneData, PositiveIntegerData, + GenericIPAddressData, NullBooleanData, PositiveIntegerData, PositiveSmallIntegerData, SlugData, SmallData, TextData, TimeData, - USStateData, GenericData, Anchor, UniqueAnchor, FKData, M2MData, O2OData, + GenericData, Anchor, UniqueAnchor, FKData, M2MData, O2OData, FKSelfData, M2MSelfData, FKDataToField, FKDataToO2O, M2MIntermediateData, Intermediate, BooleanPKData, CharPKData, EmailPKData, FilePathPKData, DecimalPKData, FloatPKData, IntegerPKData, IPAddressPKData, - GenericIPAddressPKData, PhonePKData, PositiveIntegerPKData, - PositiveSmallIntegerPKData, SlugPKData, SmallPKData, USStatePKData, + GenericIPAddressPKData, PositiveIntegerPKData, + PositiveSmallIntegerPKData, SlugPKData, SmallPKData, AutoNowDateTimeData, ModifyingSaveData, InheritAbstractModel, BaseModel, ExplicitInheritBaseModel, InheritBaseModel, ProxyBaseModel, ProxyProxyBaseModel, BigIntegerData, LengthModel, Tag, ComplexModel, @@ -218,8 +218,6 @@ test_data = [ (data_obj, 100, NullBooleanData, True), (data_obj, 101, NullBooleanData, False), (data_obj, 102, NullBooleanData, None), - (data_obj, 110, PhoneData, "212-634-5789"), - (data_obj, 111, PhoneData, None), (data_obj, 120, PositiveIntegerData, 123456789), (data_obj, 121, PositiveIntegerData, None), (data_obj, 130, PositiveSmallIntegerData, 12), @@ -239,9 +237,6 @@ The end."""), (data_obj, 162, TextData, None), (data_obj, 170, TimeData, datetime.time(10,42,37)), (data_obj, 171, TimeData, None), - (data_obj, 180, USStateData, "MA"), - (data_obj, 181, USStateData, None), - (data_obj, 182, USStateData, ""), (generic_obj, 200, GenericData, ['Generic Object 1', 'tag1', 'tag2']), (generic_obj, 201, GenericData, ['Generic Object 2', 'tag2', 'tag3']), @@ -320,7 +315,6 @@ The end."""), (pk_obj, 695, GenericIPAddressPKData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"), # (pk_obj, 700, NullBooleanPKData, True), # (pk_obj, 701, NullBooleanPKData, False), - (pk_obj, 710, PhonePKData, "212-634-5789"), (pk_obj, 720, PositiveIntegerPKData, 123456789), (pk_obj, 730, PositiveSmallIntegerPKData, 12), (pk_obj, 740, SlugPKData, "this-is-a-slug"), @@ -332,7 +326,6 @@ The end."""), # Several of them. # The end."""), # (pk_obj, 770, TimePKData, datetime.time(10,42,37)), - (pk_obj, 780, USStatePKData, "MA"), # (pk_obj, 790, XMLPKData, "<foo></foo>"), (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)), diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py index f54e34ce28..1a7552ed11 100644 --- a/tests/regressiontests/servers/tests.py +++ b/tests/regressiontests/servers/tests.py @@ -15,11 +15,12 @@ from django.test import LiveServerTestCase from django.core.servers.basehttp import WSGIServerException from django.test.utils import override_settings from django.utils.http import urlencode +from django.utils._os import upath from .models import Person -TEST_ROOT = os.path.dirname(__file__) +TEST_ROOT = os.path.dirname(upath(__file__)) TEST_SETTINGS = { 'MEDIA_URL': '/media/', 'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'), diff --git a/tests/regressiontests/settings_tests/tests.py b/tests/regressiontests/settings_tests/tests.py index aaf8bcffcf..68f620b5c9 100644 --- a/tests/regressiontests/settings_tests/tests.py +++ b/tests/regressiontests/settings_tests/tests.py @@ -4,8 +4,9 @@ import warnings from django.conf import settings, global_settings from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest -from django.test import TransactionTestCase, TestCase, signals +from django.test import SimpleTestCase, TransactionTestCase, TestCase, signals from django.test.utils import override_settings +from django.utils import unittest, six @override_settings(TEST='override') @@ -67,11 +68,6 @@ class ClassDecoratedTestCase(ClassDecoratedTestCaseSuper): self.fail() -class SettingGetter(object): - def __init__(self): - self.test = getattr(settings, 'TEST', 'undefined') - - class SettingsTests(TestCase): def setUp(self): self.testvalue = None @@ -122,10 +118,20 @@ class SettingsTests(TestCase): self.assertRaises(AttributeError, getattr, settings, 'TEST') def test_class_decorator(self): - self.assertEqual(SettingGetter().test, 'undefined') - DecoratedSettingGetter = override_settings(TEST='override')(SettingGetter) - self.assertEqual(DecoratedSettingGetter().test, 'override') - self.assertRaises(AttributeError, getattr, settings, 'TEST') + # SimpleTestCase can be decorated by override_settings, but not ut.TestCase + class SimpleTestCaseSubclass(SimpleTestCase): + pass + + class UnittestTestCaseSubclass(unittest.TestCase): + pass + + decorated = override_settings(TEST='override')(SimpleTestCaseSubclass) + self.assertIsInstance(decorated, type) + self.assertTrue(issubclass(decorated, SimpleTestCase)) + + with six.assertRaisesRegex(self, Exception, + "Only subclasses of Django SimpleTestCase*"): + decorated = override_settings(TEST='override')(UnittestTestCaseSubclass) def test_signal_callback_context_manager(self): self.assertRaises(AttributeError, getattr, settings, 'TEST') diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py index 7ecbccc448..90c8621d0b 100644 --- a/tests/regressiontests/staticfiles_tests/tests.py +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -12,18 +12,17 @@ from django.template import loader, Context from django.conf import settings from django.core.cache.backends.base import BaseCache from django.core.exceptions import ImproperlyConfigured -from django.core.files.storage import default_storage from django.core.management import call_command from django.test import TestCase from django.test.utils import override_settings -from django.utils.encoding import smart_text +from django.utils.encoding import force_text from django.utils.functional import empty -from django.utils._os import rmtree_errorhandler +from django.utils._os import rmtree_errorhandler, upath from django.utils import six from django.contrib.staticfiles import finders, storage -TEST_ROOT = os.path.dirname(__file__) +TEST_ROOT = os.path.dirname(upath(__file__)) TEST_SETTINGS = { 'DEBUG': True, 'MEDIA_URL': '/media/', @@ -48,10 +47,9 @@ class BaseStaticFilesTestCase(object): Test case with a couple utility assertions. """ def setUp(self): - # Clear the cached default_storage out, this is because when it first - # gets accessed (by some other test), it evaluates settings.MEDIA_ROOT, + # Clear the cached staticfiles_storage out, this is because when it first + # gets accessed (by some other test), it evaluates settings.STATIC_ROOT, # since we're planning on changing that we need to clear out the cache. - default_storage._wrapped = empty storage.staticfiles_storage._wrapped = empty # Clear the cached staticfile finders, so they are reinitialized every # run and pick up changes in settings.STATICFILES_DIRS. @@ -60,9 +58,9 @@ class BaseStaticFilesTestCase(object): testfiles_path = os.path.join(TEST_ROOT, 'apps', 'test', 'static', 'test') # To make sure SVN doesn't hangs itself with the non-ASCII characters # during checkout, we actually create one file dynamically. - self._nonascii_filepath = os.path.join(testfiles_path, 'fi\u015fier.txt') + self._nonascii_filepath = os.path.join(testfiles_path, '\u2297.txt') with codecs.open(self._nonascii_filepath, 'w', 'utf-8') as f: - f.write("fi\u015fier in the app dir") + f.write("\u2297 in the app dir") # And also create the stupid hidden file to dwarf the setup.py's # package data handling. self._hidden_filepath = os.path.join(testfiles_path, '.hidden') @@ -79,7 +77,7 @@ class BaseStaticFilesTestCase(object): os.unlink(self._backup_filepath) def assertFileContains(self, filepath, text): - self.assertIn(text, self._get_file(smart_text(filepath)), + self.assertIn(text, self._get_file(force_text(filepath)), "'%s' not in '%s'" % (text, filepath)) def assertFileNotFound(self, filepath): @@ -179,7 +177,7 @@ class TestDefaults(object): """ Can find a file with non-ASCII character in an app static/ directory. """ - self.assertFileContains('test/fişier.txt', 'fişier in the app dir') + self.assertFileContains('test/⊗.txt', '⊗ in the app dir') def test_camelcase_filenames(self): """ @@ -197,7 +195,7 @@ class TestFindStatic(CollectionTestCase, TestDefaults): call_command('findstatic', filepath, all=False, verbosity=0, stdout=out) out.seek(0) lines = [l.strip() for l in out.readlines()] - with codecs.open(smart_text(lines[1].strip()), "r", "utf-8") as f: + with codecs.open(force_text(lines[1].strip()), "r", "utf-8") as f: return f.read() def test_all_files(self): @@ -209,8 +207,8 @@ class TestFindStatic(CollectionTestCase, TestDefaults): out.seek(0) lines = [l.strip() for l in out.readlines()] self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line - self.assertIn('project', lines[1]) - self.assertIn('apps', lines[2]) + self.assertIn('project', force_text(lines[1])) + self.assertIn('apps', force_text(lines[2])) class TestCollection(CollectionTestCase, TestDefaults): @@ -709,9 +707,6 @@ class TestMiscFinder(TestCase): """ A few misc finder tests. """ - def setUp(self): - default_storage._wrapped = empty - def test_get_finder(self): self.assertIsInstance(finders.get_finder( 'django.contrib.staticfiles.finders.FileSystemFinder'), diff --git a/tests/regressiontests/swappable_models/__init__.py b/tests/regressiontests/swappable_models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/swappable_models/__init__.py diff --git a/tests/regressiontests/swappable_models/models.py b/tests/regressiontests/swappable_models/models.py new file mode 100644 index 0000000000..92692d4396 --- /dev/null +++ b/tests/regressiontests/swappable_models/models.py @@ -0,0 +1,15 @@ +from django.db import models + + +class Article(models.Model): + title = models.CharField(max_length=100) + publication_date = models.DateField() + + class Meta: + swappable = 'TEST_ARTICLE_MODEL' + + +class AlternateArticle(models.Model): + title = models.CharField(max_length=100) + publication_date = models.DateField() + byline = models.CharField(max_length=100) diff --git a/tests/regressiontests/swappable_models/tests.py b/tests/regressiontests/swappable_models/tests.py new file mode 100644 index 0000000000..75644e32aa --- /dev/null +++ b/tests/regressiontests/swappable_models/tests.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.six import StringIO + +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.core import management +from django.db.models.loading import cache +from django.test import TestCase +from django.test.utils import override_settings + + +class SwappableModelTests(TestCase): + def setUp(self): + # This test modifies the installed apps, so we need to make sure + # we're not dealing with a cached app list. + cache._get_models_cache.clear() + + def tearDown(self): + # By fiddling with swappable models, we alter the installed models + # cache, so flush it to make sure there are no side effects. + cache._get_models_cache.clear() + + @override_settings(TEST_ARTICLE_MODEL='swappable_models.AlternateArticle') + def test_generated_data(self): + "Permissions and content types are not created for a swapped model" + + # Delete all permissions and content_types + Permission.objects.filter(content_type__app_label='swappable_models').delete() + ContentType.objects.filter(app_label='swappable_models').delete() + + # Re-run syncdb. This will re-build the permissions and content types. + new_io = StringIO() + management.call_command('syncdb', load_initial_data=False, interactive=False, stdout=new_io) + + # Check that content types and permissions exist for the swapped model, + # but not for the swappable model. + apps_models = [(p.content_type.app_label, p.content_type.model) + for p in Permission.objects.all()] + self.assertIn(('swappable_models', 'alternatearticle'), apps_models) + self.assertNotIn(('swappable_models', 'article'), apps_models) + + apps_models = [(ct.app_label, ct.model) + for ct in ContentType.objects.all()] + self.assertIn(('swappable_models', 'alternatearticle'), apps_models) + self.assertNotIn(('swappable_models', 'article'), apps_models) diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py index 8b4aebbf22..7ba1681fd5 100644 --- a/tests/regressiontests/templates/filters.py +++ b/tests/regressiontests/templates/filters.py @@ -8,7 +8,7 @@ consistent. """ from __future__ import unicode_literals -from datetime import date, datetime, timedelta +from datetime import date, datetime, time, timedelta from django.test.utils import str_prefix from django.utils.tzinfo import LocalTimezone, FixedOffset @@ -356,6 +356,9 @@ def get_filter_tests(): # Timezone name 'date06': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12, tzinfo=FixedOffset(30))}, '+0030'), 'date07': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12)}, ''), + # Ticket 19370: Make sure |date doesn't blow up on a midnight time object + 'date08': (r'{{ t|date:"H:i" }}', {'t': time(0, 1)}, '00:01'), + 'date09': (r'{{ t|date:"H:i" }}', {'t': time(0, 0)}, '00:00'), # Tests for #11687 and #16676 'add01': (r'{{ i|add:"5" }}', {'i': 2000}, '2005'), diff --git a/tests/regressiontests/templates/loaders.py b/tests/regressiontests/templates/loaders.py index 7fbb0841f9..b77965203f 100644 --- a/tests/regressiontests/templates/loaders.py +++ b/tests/regressiontests/templates/loaders.py @@ -18,6 +18,7 @@ from django.template import TemplateDoesNotExist, Context from django.template.loaders.eggs import Loader as EggLoader from django.template import loader from django.utils import unittest, six +from django.utils._os import upath from django.utils.six import StringIO @@ -111,9 +112,9 @@ class CachedLoader(unittest.TestCase): def test_templatedir_caching(self): "Check that the template directories form part of the template cache key. Refs #13573" # Retrive a template specifying a template directory to check - t1, name = loader.find_template('test.html', (os.path.join(os.path.dirname(__file__), 'templates', 'first'),)) + t1, name = loader.find_template('test.html', (os.path.join(os.path.dirname(upath(__file__)), 'templates', 'first'),)) # Now retrieve the same template name, but from a different directory - t2, name = loader.find_template('test.html', (os.path.join(os.path.dirname(__file__), 'templates', 'second'),)) + t2, name = loader.find_template('test.html', (os.path.join(os.path.dirname(upath(__file__)), 'templates', 'second'),)) # The two templates should not have the same content self.assertNotEqual(t1.render(Context({})), t2.render(Context({}))) @@ -123,7 +124,7 @@ class RenderToStringTest(unittest.TestCase): def setUp(self): self._old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS settings.TEMPLATE_DIRS = ( - os.path.join(os.path.dirname(__file__), 'templates'), + os.path.join(os.path.dirname(upath(__file__)), 'templates'), ) def tearDown(self): diff --git a/tests/regressiontests/templates/response.py b/tests/regressiontests/templates/response.py index 93919b95cd..c4da50af6b 100644 --- a/tests/regressiontests/templates/response.py +++ b/tests/regressiontests/templates/response.py @@ -11,6 +11,7 @@ from django.template import Template, Context from django.template.response import (TemplateResponse, SimpleTemplateResponse, ContentNotRenderedError) from django.test.utils import override_settings +from django.utils._os import upath def test_processor(request): return {'processors': 'yes'} @@ -189,9 +190,24 @@ class SimpleTemplateResponseTest(TestCase): unpickled_response = pickle.loads(pickled_response) repickled_response = pickle.dumps(unpickled_response) + def test_pickling_cookie(self): + response = SimpleTemplateResponse('first/test.html', { + 'value': 123, + 'fn': datetime.now, + }) + + response.cookies['key'] = 'value' + + response.render() + pickled_response = pickle.dumps(response, pickle.HIGHEST_PROTOCOL) + unpickled_response = pickle.loads(pickled_response) + + self.assertEqual(unpickled_response.cookies['key'].value, 'value') + + @override_settings( TEMPLATE_CONTEXT_PROCESSORS=[test_processor_name], - TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__),'templates')), + TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates')), ) class TemplateResponseTest(TestCase): diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index c95b5bfa3c..d21434a12e 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -19,16 +19,18 @@ except ImportError: # Python 2 from urlparse import urljoin from django import template -from django.template import base as template_base, RequestContext, Template, Context +from django.template import (base as template_base, Context, RequestContext, + Template) from django.core import urlresolvers from django.template import loader from django.template.loaders import app_directories, filesystem, cached -from django.test import RequestFactory +from django.test import RequestFactory, TestCase from django.test.utils import (setup_test_template_loader, restore_template_loaders, override_settings) from django.utils import unittest from django.utils.encoding import python_2_unicode_compatible from django.utils.formats import date_format +from django.utils._os import upath from django.utils.translation import activate, deactivate, ugettext as _ from django.utils.safestring import mark_safe from django.utils import six @@ -156,7 +158,7 @@ class UTF8Class: return 'ŠĐĆŽćžšđ' @override_settings(MEDIA_URL="/media/", STATIC_URL="/static/") -class Templates(unittest.TestCase): +class Templates(TestCase): def test_loaders_security(self): ad_loader = app_directories.Loader() @@ -364,6 +366,20 @@ class Templates(unittest.TestCase): with self.assertRaises(urlresolvers.NoReverseMatch): t.render(c) + def test_url_explicit_exception_for_old_syntax_at_run_time(self): + # Regression test for #19280 + t = Template('{% url path.to.view %}') # not quoted = old syntax + c = Context() + with self.assertRaisesRegexp(urlresolvers.NoReverseMatch, + "The syntax changed in Django 1.5, see the docs."): + t.render(c) + + def test_url_explicit_exception_for_old_syntax_at_compile_time(self): + # Regression test for #19392 + with self.assertRaisesRegexp(template.TemplateSyntaxError, + "The syntax of 'url' changed in Django 1.5, see the docs."): + t = Template('{% url my-view %}') # not a variable = old syntax + @override_settings(DEBUG=True, TEMPLATE_DEBUG=True) def test_no_wrapped_exception(self): """ @@ -414,7 +430,7 @@ class Templates(unittest.TestCase): # Set ALLOWED_INCLUDE_ROOTS so that ssi works. old_allowed_include_roots = settings.ALLOWED_INCLUDE_ROOTS settings.ALLOWED_INCLUDE_ROOTS = ( - os.path.dirname(os.path.abspath(__file__)), + os.path.dirname(os.path.abspath(upath(__file__))), ) # Warm the URL reversing cache. This ensures we don't pay the cost @@ -505,7 +521,7 @@ class Templates(unittest.TestCase): def get_template_tests(self): # SYNTAX -- # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class) - basedir = os.path.dirname(os.path.abspath(__file__)) + basedir = os.path.dirname(os.path.abspath(upath(__file__))) tests = { ### BASIC SYNTAX ################################################ @@ -1305,7 +1321,7 @@ class Templates(unittest.TestCase): # retrieving language information 'i18n28_2': ('{% load i18n %}{% get_language_info for "de" as l %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}', {}, 'de: German/Deutsch bidi=False'), 'i18n29': ('{% load i18n %}{% get_language_info for LANGUAGE_CODE as l %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}', {'LANGUAGE_CODE': 'fi'}, 'fi: Finnish/suomi bidi=False'), - 'i18n30': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': ['it', 'no']}, 'it: Italian/italiano bidi=False; no: Norwegian/Norsk bidi=False; '), + 'i18n30': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': ['it', 'no']}, 'it: Italian/italiano bidi=False; no: Norwegian/norsk bidi=False; '), 'i18n31': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': (('sl', 'Slovenian'), ('fa', 'Persian'))}, 'sl: Slovenian/Sloven\u0161\u010dina bidi=False; fa: Persian/\u0641\u0627\u0631\u0633\u06cc bidi=True; '), 'i18n32': ('{% load i18n %}{{ "hu"|language_name }} {{ "hu"|language_name_local }} {{ "hu"|language_bidi }}', {}, 'Hungarian Magyar False'), 'i18n33': ('{% load i18n %}{{ langcode|language_name }} {{ langcode|language_name_local }} {{ langcode|language_bidi }}', {'langcode': 'nl'}, 'Dutch Nederlands False'), @@ -1640,7 +1656,7 @@ class TemplateTagLoading(unittest.TestCase): def setUp(self): self.old_path = sys.path[:] self.old_apps = settings.INSTALLED_APPS - self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__)) self.old_tag_modules = template_base.templatetags_modules template_base.templatetags_modules = [] diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py index f424321663..5ba5d3c4b3 100644 --- a/tests/regressiontests/test_client_regress/tests.py +++ b/tests/regressiontests/test_client_regress/tests.py @@ -16,12 +16,13 @@ from django.test import Client, TestCase from django.test.client import encode_file, RequestFactory from django.test.utils import ContextList, override_settings, str_prefix from django.template.response import SimpleTemplateResponse +from django.utils._os import upath from django.utils.translation import ugettext_lazy from django.http import HttpResponse @override_settings( - TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) + TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),) ) class AssertContainsTests(TestCase): def test_contains(self): @@ -629,7 +630,7 @@ class TemplateExceptionTests(TestCase): template_loader.reset() @override_settings( - TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'bad_templates'),) + TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'bad_templates'),) ) def test_bad_404_template(self): "Errors found when rendering 404 error templates are re-raised" diff --git a/tests/regressiontests/test_utils/models.py b/tests/regressiontests/test_utils/models.py index 4da7a07bbf..85a1031c02 100644 --- a/tests/regressiontests/test_utils/models.py +++ b/tests/regressiontests/test_utils/models.py @@ -1,5 +1,9 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible - +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=100) + + def __str__(self): + return self.name diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index 95913b5aab..d5d49b2104 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -54,6 +54,46 @@ class AssertNumQueriesTests(TestCase): self.assertNumQueries(2, test_func) +class AssertQuerysetEqualTests(TestCase): + def setUp(self): + self.p1 = Person.objects.create(name='p1') + self.p2 = Person.objects.create(name='p2') + + def test_ordered(self): + self.assertQuerysetEqual( + Person.objects.all().order_by('name'), + [repr(self.p1), repr(self.p2)] + ) + + def test_unordered(self): + self.assertQuerysetEqual( + Person.objects.all().order_by('name'), + [repr(self.p2), repr(self.p1)], + ordered=False + ) + + def test_transform(self): + self.assertQuerysetEqual( + Person.objects.all().order_by('name'), + [self.p1.pk, self.p2.pk], + transform=lambda x: x.pk + ) + + def test_undefined_order(self): + # Using an unordered queryset with more than one ordered value + # is an error. + with self.assertRaises(ValueError): + self.assertQuerysetEqual( + Person.objects.all(), + [repr(self.p1), repr(self.p2)] + ) + # No error for one value. + self.assertQuerysetEqual( + Person.objects.filter(name='p1'), + [repr(self.p1)] + ) + + class AssertNumQueriesContextManagerTests(TestCase): urls = 'regressiontests.test_utils.urls' diff --git a/tests/regressiontests/transactions_regress/tests.py b/tests/regressiontests/transactions_regress/tests.py index 472e2aafd9..5d1ab2c6f6 100644 --- a/tests/regressiontests/transactions_regress/tests.py +++ b/tests/regressiontests/transactions_regress/tests.py @@ -24,17 +24,15 @@ class TestTransactionClosing(TransactionTestCase): def raw_sql(): "Write a record using raw sql under a commit_on_success decorator" cursor = connection.cursor() - cursor.execute("INSERT into transactions_regress_mod (id,fld) values (17,18)") + cursor.execute("INSERT into transactions_regress_mod (fld) values (18)") raw_sql() # Rollback so that if the decorator didn't commit, the record is unwritten transaction.rollback() - try: - # Check that the record is in the DB - obj = Mod.objects.get(pk=17) - self.assertEqual(obj.fld, 18) - except Mod.DoesNotExist: - self.fail("transaction with raw sql not committed") + self.assertEqual(Mod.objects.count(), 1) + # Check that the record is in the DB + obj = Mod.objects.all()[0] + self.assertEqual(obj.fld, 18) def test_commit_manually_enforced(self): """ @@ -115,19 +113,16 @@ class TestTransactionClosing(TransactionTestCase): be committed. """ cursor = connection.cursor() - cursor.execute("INSERT into transactions_regress_mod (id,fld) values (1,2)") + cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") transaction.rollback() - cursor.execute("INSERT into transactions_regress_mod (id,fld) values (1,2)") + cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") reuse_cursor_ref() # Rollback so that if the decorator didn't commit, the record is unwritten transaction.rollback() - try: - # Check that the record is in the DB - obj = Mod.objects.get(pk=1) - self.assertEqual(obj.fld, 2) - except Mod.DoesNotExist: - self.fail("After ending a transaction, cursor use no longer sets dirty") + self.assertEqual(Mod.objects.count(), 1) + obj = Mod.objects.all()[0] + self.assertEqual(obj.fld, 2) def test_failing_query_transaction_closed(self): """ @@ -145,7 +140,7 @@ class TestTransactionClosing(TransactionTestCase): "Create a user in a transaction" user = User.objects.create_user(username='system', password='iamr00t', email='root@SITENAME.com') # Redundant, just makes sure the user id was read back from DB - Mod.objects.create(fld=user.id) + Mod.objects.create(fld=user.pk) # Create a user create_system_user() diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 234897d267..eb3afe8201 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -171,6 +171,18 @@ class URLPatternReverse(TestCase): # Reversing None should raise an error, not return the last un-named view. self.assertRaises(NoReverseMatch, reverse, None) + def test_prefix_braces(self): + self.assertEqual('/%7B%7Binvalid%7D%7D/includes/non_path_include/', + reverse('non_path_include', prefix='/{{invalid}}/')) + + def test_prefix_parenthesis(self): + self.assertEqual('/bogus%29/includes/non_path_include/', + reverse('non_path_include', prefix='/bogus)/')) + + def test_prefix_format_char(self): + self.assertEqual('/bump%2520map/includes/non_path_include/', + reverse('non_path_include', prefix='/bump%20map/')) + class ResolverTests(unittest.TestCase): def test_resolver_repr(self): """ @@ -225,7 +237,7 @@ class ResolverTests(unittest.TestCase): self.assertEqual(len(e.args[0]['tried']), len(url_types_names), 'Wrong number of tried URLs returned. Expected %s, got %s.' % (len(url_types_names), len(e.args[0]['tried']))) for tried, expected in zip(e.args[0]['tried'], url_types_names): for t, e in zip(tried, expected): - self.assertTrue(isinstance(t, e['type']), '%s is not an instance of %s' % (t, e['type'])) + self.assertTrue(isinstance(t, e['type']), str('%s is not an instance of %s') % (t, e['type'])) if 'name' in e: if not e['name']: self.assertTrue(t.name is None, 'Expected no URL name but found %s.' % t.name) diff --git a/tests/regressiontests/utils/archive.py b/tests/regressiontests/utils/archive.py index 5575f340f6..8861b4a577 100644 --- a/tests/regressiontests/utils/archive.py +++ b/tests/regressiontests/utils/archive.py @@ -4,9 +4,10 @@ import tempfile from django.utils import unittest from django.utils.archive import Archive, extract +from django.utils._os import upath -TEST_DIR = os.path.join(os.path.dirname(__file__), 'archives') +TEST_DIR = os.path.join(os.path.dirname(upath(__file__)), 'archives') class ArchiveTester(object): diff --git a/tests/regressiontests/utils/feedgenerator.py b/tests/regressiontests/utils/feedgenerator.py index b646a5997d..bcd53bb2a0 100644 --- a/tests/regressiontests/utils/feedgenerator.py +++ b/tests/regressiontests/utils/feedgenerator.py @@ -104,10 +104,14 @@ class FeedgeneratorTest(unittest.TestCase): feed = feedgenerator.Rss201rev2Feed('title', '/link/', 'descr') self.assertEqual(feed.feed['feed_url'], None) feed_content = feed.writeString('utf-8') - self.assertNotIn('<atom:link href=', feed_content) + self.assertNotIn('<atom:link', feed_content) + self.assertNotIn('href="/feed/"', feed_content) + self.assertNotIn('rel="self"', feed_content) def test_feed_with_feed_url_gets_rendered_with_atom_link(self): feed = feedgenerator.Rss201rev2Feed('title', '/link/', 'descr', feed_url='/feed/') self.assertEqual(feed.feed['feed_url'], '/feed/') feed_content = feed.writeString('utf-8') - self.assertIn('<atom:link href="/feed/" rel="self"></atom:link>', feed_content) + self.assertIn('<atom:link', feed_content) + self.assertIn('href="/feed/"', feed_content) + self.assertIn('rel="self"', feed_content) diff --git a/tests/regressiontests/utils/html.py b/tests/regressiontests/utils/html.py index 6a93dff85e..a0226c4765 100644 --- a/tests/regressiontests/utils/html.py +++ b/tests/regressiontests/utils/html.py @@ -65,6 +65,9 @@ class TestUtilsHtml(unittest.TestCase): ('<f', '<f'), ('</fe', '</fe'), ('<x>b<y>', 'b'), + ('a<p onclick="alert(\'<test>\')">b</p>c', 'abc'), + ('a<p a >b</p>c', 'abc'), + ('d<a:b c:d>e</p>f', 'def'), ) for value, output in items: self.check_output(f, value, output) diff --git a/tests/regressiontests/utils/module_loading.py b/tests/regressiontests/utils/module_loading.py index dffb51966c..3fc92b0862 100644 --- a/tests/regressiontests/utils/module_loading.py +++ b/tests/regressiontests/utils/module_loading.py @@ -6,6 +6,7 @@ from zipimport import zipimporter from django.utils import unittest from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule +from django.utils._os import upath class DefaultLoader(unittest.TestCase): @@ -50,7 +51,7 @@ class DefaultLoader(unittest.TestCase): class EggLoader(unittest.TestCase): def setUp(self): self.old_path = sys.path[:] - self.egg_dir = '%s/eggs' % os.path.dirname(__file__) + self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__)) def tearDown(self): sys.path = self.old_path diff --git a/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mo b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mo Binary files differnew file mode 100644 index 0000000000..34ba691029 --- /dev/null +++ b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mo diff --git a/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000..88cc35e88f --- /dev/null +++ b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po @@ -0,0 +1,40 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: django tests\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-02-14 17:33+0100\n" +"PO-Revision-Date: 2011-01-21 21:37-0300\n" +"Last-Translator: Jannis Leidel <jannis@leidel.info>\n" +"Language-Team: de <de@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: models.py:7 +msgctxt "month name" +msgid "May" +msgstr "Mai" + +#: models.py:9 +msgctxt "verb" +msgid "May" +msgstr "Kann" + +#: models.py:11 +msgid "%s item" +msgid_plural "%s items" +msgstr[0] "%s Element" +msgstr[1] "%s Elemente" + +#: models.py:11 +msgctxt "search" +msgid "%s result" +msgid_plural "%s results" +msgstr[0] "%s Resultat" +msgstr[1] "%s Resultate" diff --git a/tests/regressiontests/views/templates/jsi18n.html b/tests/regressiontests/views/templates/jsi18n.html new file mode 100644 index 0000000000..d6093c8ef4 --- /dev/null +++ b/tests/regressiontests/views/templates/jsi18n.html @@ -0,0 +1,44 @@ +<html> +<head> + <script type="text/javascript" src="/jsi18n_admin/"></script> +</head> + +<body> + <p id="gettext"> + <script type="text/javascript"> + document.write(gettext("Remove")); + </script> + </p> + + <p id="ngettext_sing"> + <script type="text/javascript"> + document.write(interpolate(ngettext("%s item", "%s items", 1), [1])); + </script> + </p> + + <p id="ngettext_plur"> + <script type="text/javascript"> + document.write(interpolate(ngettext("%s item", "%s items", 455), [455])); + </script> + </p> + + <p id="pgettext"> + <script type="text/javascript"> + document.write(pgettext("verb", "May")); + </script> + </p> + + <p id="npgettext_sing"> + <script type="text/javascript"> + document.write(interpolate(npgettext("search", "%s result", "%s results", 1), [1])); + </script> + </p> + + <p id="npgettext_plur"> + <script type="text/javascript"> + document.write(interpolate(npgettext("search", "%s result", "%s results", 455), [455])); + </script> + </p> + +</body> +</html> diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py index 12d0c59014..17f3f4562d 100644 --- a/tests/regressiontests/views/tests/__init__.py +++ b/tests/regressiontests/views/tests/__init__.py @@ -4,7 +4,7 @@ from .debug import (DebugViewTests, ExceptionReporterTests, ExceptionReporterTests, PlainTextReportTests, ExceptionReporterFilterTests, AjaxResponseExceptionReporterFilter) from .defaults import DefaultsTests -from .i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage +from .i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage, JavascriptI18nTests from .shortcuts import ShortcutTests from .specials import URLHandling from .static import StaticHelperTest, StaticUtilsTests, StaticTests diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index e616d184b8..4fdaad5010 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -14,6 +14,7 @@ from django.core.urlresolvers import reverse from django.test import TestCase, RequestFactory from django.test.utils import (override_settings, setup_test_template_loader, restore_template_loaders) +from django.utils.encoding import force_text from django.views.debug import ExceptionReporter from .. import BrokenException, except_args @@ -306,15 +307,16 @@ class ExceptionReportTestMixin(object): self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] # Frames vars are never shown in plain text email reports. - self.assertNotIn('cooked_eggs', email.body) - self.assertNotIn('scrambled', email.body) - self.assertNotIn('sauce', email.body) - self.assertNotIn('worcestershire', email.body) + body = force_text(email.body) + self.assertNotIn('cooked_eggs', body) + self.assertNotIn('scrambled', body) + self.assertNotIn('sauce', body) + self.assertNotIn('worcestershire', body) if check_for_POST_params: for k, v in self.breakfast_data.items(): # All POST parameters are shown. - self.assertIn(k, email.body) - self.assertIn(v, email.body) + self.assertIn(k, body) + self.assertIn(v, body) def verify_safe_email(self, view, check_for_POST_params=True): """ @@ -327,20 +329,21 @@ class ExceptionReportTestMixin(object): self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] # Frames vars are never shown in plain text email reports. - self.assertNotIn('cooked_eggs', email.body) - self.assertNotIn('scrambled', email.body) - self.assertNotIn('sauce', email.body) - self.assertNotIn('worcestershire', email.body) + body = force_text(email.body) + self.assertNotIn('cooked_eggs', body) + self.assertNotIn('scrambled', body) + self.assertNotIn('sauce', body) + self.assertNotIn('worcestershire', body) if check_for_POST_params: for k, v in self.breakfast_data.items(): # All POST parameters' names are shown. - self.assertIn(k, email.body) + self.assertIn(k, body) # Non-sensitive POST parameters' values are shown. - self.assertIn('baked-beans-value', email.body) - self.assertIn('hash-brown-value', email.body) + self.assertIn('baked-beans-value', body) + self.assertIn('hash-brown-value', body) # Sensitive POST parameters' values are not shown. - self.assertNotIn('sausage-value', email.body) - self.assertNotIn('bacon-value', email.body) + self.assertNotIn('sausage-value', body) + self.assertNotIn('bacon-value', body) def verify_paranoid_email(self, view): """ @@ -353,15 +356,16 @@ class ExceptionReportTestMixin(object): self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] # Frames vars are never shown in plain text email reports. - self.assertNotIn('cooked_eggs', email.body) - self.assertNotIn('scrambled', email.body) - self.assertNotIn('sauce', email.body) - self.assertNotIn('worcestershire', email.body) + body = force_text(email.body) + self.assertNotIn('cooked_eggs', body) + self.assertNotIn('scrambled', body) + self.assertNotIn('sauce', body) + self.assertNotIn('worcestershire', body) for k, v in self.breakfast_data.items(): # All POST parameters' names are shown. - self.assertIn(k, email.body) + self.assertIn(k, body) # No POST parameters' values are shown. - self.assertNotIn(v, email.body) + self.assertNotIn(v, body) class ExceptionReporterFilterTests(TestCase, ExceptionReportTestMixin): diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py index 601df6d512..b1dc8808a1 100644 --- a/tests/regressiontests/views/tests/i18n.py +++ b/tests/regressiontests/views/tests/i18n.py @@ -6,11 +6,18 @@ from os import path from django.conf import settings from django.core.urlresolvers import reverse -from django.test import TestCase -from django.utils import six -from django.utils.translation import override, get_language +from django.test import LiveServerTestCase, TestCase +from django.test.utils import override_settings +from django.utils import six, unittest +from django.utils._os import upath +from django.utils.translation import override from django.utils.text import javascript_quote +try: + from selenium.webdriver.firefox import webdriver as firefox +except ImportError: + firefox = None + from ..urls import locale_dir @@ -18,13 +25,28 @@ class I18NTests(TestCase): """ Tests django views in django/views/i18n.py """ def test_setlang(self): - """The set_language view can be used to change the session language""" + """ + The set_language view can be used to change the session language. + + The user is redirected to the 'next' argument if provided. + """ for lang_code, lang_name in settings.LANGUAGES: post_data = dict(language=lang_code, next='/views/') response = self.client.post('/views/i18n/setlang/', data=post_data) self.assertRedirects(response, 'http://testserver/views/') self.assertEqual(self.client.session['django_language'], lang_code) + def test_setlang_unsafe_next(self): + """ + The set_language view only redirects to the 'next' argument if it is + "safe". + """ + lang_code, lang_name = settings.LANGUAGES[0] + post_data = dict(language=lang_code, next='//unsafe/redirection/') + response = self.client.post('/views/i18n/setlang/', data=post_data) + self.assertEqual(response['Location'], 'http://testserver/') + self.assertEqual(self.client.session['django_language'], lang_code) + def test_setlang_reversal(self): self.assertEqual(reverse('set_language'), '/views/i18n/setlang/') @@ -146,9 +168,49 @@ class JsI18NTestsMultiPackage(TestCase): def testI18NWithLocalePaths(self): extended_locale_paths = settings.LOCALE_PATHS + ( path.join(path.dirname( - path.dirname(path.abspath(__file__))), 'app3', 'locale'),) + path.dirname(path.abspath(upath(__file__)))), 'app3', 'locale'),) with self.settings(LANGUAGE_CODE='es-ar', LOCALE_PATHS=extended_locale_paths): with override('es-ar'): response = self.client.get('/views/jsi18n/') self.assertContains(response, javascript_quote('este texto de app3 debe ser traducido')) + + +@unittest.skipUnless(firefox, 'Selenium not installed') +class JavascriptI18nTests(LiveServerTestCase): + urls = 'regressiontests.views.urls' + + @classmethod + def setUpClass(cls): + cls.selenium = firefox.WebDriver() + super(JavascriptI18nTests, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + cls.selenium.quit() + super(JavascriptI18nTests, cls).tearDownClass() + + @override_settings(LANGUAGE_CODE='de') + def test_javascript_gettext(self): + extended_apps = list(settings.INSTALLED_APPS) + ['regressiontests.views'] + with self.settings(INSTALLED_APPS=extended_apps): + self.selenium.get('%s%s' % (self.live_server_url, '/jsi18n_template/')) + + elem = self.selenium.find_element_by_id("gettext") + self.assertEqual(elem.text, "Entfernen") + elem = self.selenium.find_element_by_id("ngettext_sing") + self.assertEqual(elem.text, "1 Element") + elem = self.selenium.find_element_by_id("ngettext_plur") + self.assertEqual(elem.text, "455 Elemente") + elem = self.selenium.find_element_by_id("pgettext") + self.assertEqual(elem.text, "Kann") + elem = self.selenium.find_element_by_id("npgettext_sing") + self.assertEqual(elem.text, "1 Resultat") + elem = self.selenium.find_element_by_id("npgettext_plur") + self.assertEqual(elem.text, "455 Resultate") + + def test_escaping(self): + extended_apps = list(settings.INSTALLED_APPS) + ['regressiontests.views'] + with self.settings(INSTALLED_APPS=extended_apps): + response = self.client.get('%s%s' % (self.live_server_url, '/jsi18n_admin/')) + self.assertContains(response, '\\x04') diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py index 90d2382f71..2c06557ae9 100644 --- a/tests/regressiontests/views/urls.py +++ b/tests/regressiontests/views/urls.py @@ -4,11 +4,12 @@ from __future__ import absolute_import from os import path from django.conf.urls import patterns, url, include +from django.utils._os import upath from . import views -base_dir = path.dirname(path.abspath(__file__)) +base_dir = path.dirname(path.abspath(upath(__file__))) media_dir = path.join(base_dir, 'media') locale_dir = path.join(base_dir, 'locale') @@ -32,6 +33,11 @@ js_info_dict_multi_packages2 = { 'packages': ('regressiontests.views.app3', 'regressiontests.views.app4'), } +js_info_dict_admin = { + 'domain': 'djangojs', + 'packages': ('django.contrib.admin', 'regressiontests.views'), +} + urlpatterns = patterns('', (r'^$', views.index_page), @@ -51,6 +57,8 @@ urlpatterns = patterns('', (r'^jsi18n_english_translation/$', 'django.views.i18n.javascript_catalog', js_info_dict_english_translation), (r'^jsi18n_multi_packages1/$', 'django.views.i18n.javascript_catalog', js_info_dict_multi_packages1), (r'^jsi18n_multi_packages2/$', 'django.views.i18n.javascript_catalog', js_info_dict_multi_packages2), + (r'^jsi18n_admin/$', 'django.views.i18n.javascript_catalog', js_info_dict_admin), + (r'^jsi18n_template/$', views.jsi18n), # Static views (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}), diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py index 2836d1bdde..ed9d61144a 100644 --- a/tests/regressiontests/views/views.py +++ b/tests/regressiontests/views/views.py @@ -51,6 +51,9 @@ def template_exception(request, n): return render_to_response('debug/template_exception.html', {'arg': except_args[int(n)]}) +def jsi18n(request): + return render_to_response('jsi18n.html') + # Some views to exercise the shortcuts def render_to_response_view(request): diff --git a/tests/runtests.py b/tests/runtests.py index a81fee6858..8c56e273b5 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -7,6 +7,7 @@ import tempfile import warnings from django import contrib +from django.utils._os import upath from django.utils import six # databrowse is deprecated, but we still want to run its tests @@ -19,8 +20,8 @@ REGRESSION_TESTS_DIR_NAME = 'regressiontests' TEST_TEMPLATE_DIR = 'templates' -RUNTESTS_DIR = os.path.dirname(__file__) -CONTRIB_DIR = os.path.dirname(contrib.__file__) +RUNTESTS_DIR = os.path.dirname(upath(__file__)) +CONTRIB_DIR = os.path.dirname(upath(contrib.__file__)) MODEL_TEST_DIR = os.path.join(RUNTESTS_DIR, MODEL_TESTS_DIR_NAME) REGRESSION_TEST_DIR = os.path.join(RUNTESTS_DIR, REGRESSION_TESTS_DIR_NAME) TEMP_DIR = tempfile.mkdtemp(prefix='django_') @@ -192,7 +193,7 @@ def bisect_tests(bisection_label, options, test_labels): pass subprocess_args = [ - sys.executable, __file__, '--settings=%s' % options.settings] + sys.executable, upath(__file__), '--settings=%s' % options.settings] if options.failfast: subprocess_args.append('--failfast') if options.verbosity: @@ -253,7 +254,7 @@ def paired_tests(paired_test, options, test_labels): pass subprocess_args = [ - sys.executable, __file__, '--settings=%s' % options.settings] + sys.executable, upath(__file__), '--settings=%s' % options.settings] if options.failfast: subprocess_args.append('--failfast') if options.verbosity: @@ -277,7 +278,7 @@ if __name__ == "__main__": usage = "%prog [options] [module module module ...]" parser = OptionParser(usage=usage) parser.add_option( - '-v','--verbosity', action='store', dest='verbosity', default='1', + '-v', '--verbosity', action='store', dest='verbosity', default='1', type='choice', choices=['0', '1', '2', '3'], help='Verbosity level; 0=minimal output, 1=normal output, 2=all ' 'output') |
