From 172b50b72224dea985cd665accab2ff3110f6674 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Tue, 23 Apr 2013 21:17:04 +0300 Subject: [PATCH 001/249] Clearer explanation when exception has no message "No exception supplied" is misleading; actually there is an exception, but there's no message string. --- django/views/debug.py | 4 ++-- tests/view_tests/tests/test_debug.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django/views/debug.py b/django/views/debug.py index 9b95b524d2..3a840588e3 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -584,7 +584,7 @@ TECHNICAL_500_TEMPLATE = """

{% if exception_type %}{{ exception_type }}{% else %}Report{% endif %}{% if request %} at {{ request.path_info|escape }}{% endif %}

-
{% if exception_value %}{{ exception_value|force_escape }}{% else %}No exception supplied{% endif %}
+
{% if exception_value %}{{ exception_value|force_escape }}{% else %}No exception message supplied{% endif %}
{% if request %} @@ -927,7 +927,7 @@ Exception Value: {{ exception_value|force_escape }} """ TECHNICAL_500_TEXT_TEMPLATE = """{% load firstof from future %}{% firstof exception_type 'Report' %}{% if request %} at {{ request.path_info }}{% endif %} -{% firstof exception_value 'No exception supplied' %} +{% firstof exception_value 'No exception message supplied' %} {% if request %} Request Method: {{ request.META.REQUEST_METHOD }} Request URL: {{ request.build_absolute_uri }}{% endif %} diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index b44cd88abe..dd0d0dc9a1 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -128,7 +128,7 @@ class ExceptionReporterTests(TestCase): reporter = ExceptionReporter(request, None, None, None) html = reporter.get_traceback_html() self.assertIn('

Report at /test_view/

', html) - self.assertIn('
No exception supplied
', html) + self.assertIn('
No exception message supplied
', html) self.assertIn('', html) self.assertIn('', html) self.assertNotIn('', html) From f043cfe3e2c1097804f486ecf05d18f054eb64fb Mon Sep 17 00:00:00 2001 From: Michael Manfre Date: Wed, 24 Apr 2013 14:26:13 -0300 Subject: [PATCH 002/249] Fixed documentation of disable_constraint_checking The docstring and base implementation of disable_constraint_checking do not indicate that a return value is expected for proper behavior. --- django/db/backends/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 9acef4ad19..4abc08c5a1 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -390,9 +390,10 @@ class BaseDatabaseWrapper(object): def disable_constraint_checking(self): """ Backends can implement as needed to temporarily disable foreign key - constraint checking. + constraint checking. Should return True if the constraints were + disabled and will need to be reenabled. """ - pass + return False def enable_constraint_checking(self): """ From c278e56bafc8b2740f01e53fc5ce3650806c04ad Mon Sep 17 00:00:00 2001 From: Eric Urban Date: Fri, 17 May 2013 19:49:33 -0400 Subject: [PATCH 003/249] Corrected documentation on the constructor arguments of MultiPartParser --- django/http/multipartparser.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index 0e999f2ded..26e10da1a2 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -48,9 +48,9 @@ class MultiPartParser(object): The standard ``META`` dictionary in Django request objects. :input_data: The raw post data, as a file-like object. - :upload_handler: - An UploadHandler instance that performs operations on the uploaded - data. + :upload_handlers: + A list of UploadHandler instances that perform operations on the uploaded + data. :encoding: The encoding with which to treat the incoming data. """ @@ -113,14 +113,15 @@ class MultiPartParser(object): if self._content_length == 0: return QueryDict('', encoding=self._encoding), MultiValueDict() - # See if the handler will want to take care of the parsing. - # This allows overriding everything if somebody wants it. + # See if any of the handlers take care of the parsing. + # This allows overriding everything if need be. for handler in handlers: result = handler.handle_raw_input(self._input_data, self._meta, self._content_length, self._boundary, encoding) + #Check to see if it was handled if result is not None: return result[0], result[1] From ee11d325a426480a8549069b2d003bb8eec107f8 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 10:29:01 +0200 Subject: [PATCH 004/249] Reorganize committers list chronologically. This completes the removal of the distinction between core devs and specialists. Patch by Simon Meers. --- AUTHORS | 13 ++- docs/internals/committers.txt | 187 +++++++++++++++++----------------- 2 files changed, 101 insertions(+), 99 deletions(-) diff --git a/AUTHORS b/AUTHORS index 973e32d05a..3544e3b81a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,18 +12,25 @@ The PRIMARY AUTHORS are (and/or have been): * Luke Plant * Russell Keith-Magee * Robert Wittams + * James Bennett * Gary Wilson + * Matt Boersma + * Ian Kelly + * Joseph Kocherhans * Brian Rosner * Justin Bronn * Karen Tracey * Jannis Leidel * James Tauber * Alex Gaynor + * Simon Meers * Andrew Godwin * Carl Meyer * Ramiro Morales + * Gabriel Hurley * Chris Beaven * Honza Král + * Tim Graham * Idan Gazit * Paul McMillan * Julien Phalip @@ -36,6 +43,7 @@ The PRIMARY AUTHORS are (and/or have been): * Preston Holmes * Simon Charette * Donald Stufft + * Daniel Lindsley * Marc Tamlyn More information on the main contributors to Django can be found in @@ -91,7 +99,6 @@ answer newbie questions, and generally made Django that much better: Shannon -jj Behrens Esdras Beleza Chris Bennett - James Bennett Danilo Bargen Shai Berger berto @@ -104,7 +111,6 @@ answer newbie questions, and generally made Django that much better: Simon Blanchard Craig Blaszczyk David Blewett - Matt Boersma Artem Gnilov Matías Bordese Nate Bragg @@ -278,7 +284,6 @@ answer newbie questions, and generally made Django that much better: Rob Hudson Jason Huggins Jeff Hui - Gabriel Hurley Hyun Mi Ae Ibon Tom Insam @@ -327,7 +332,6 @@ answer newbie questions, and generally made Django that much better: Meir Kriheli Bruce Kroeze krzysiek.pawlik@silvermedia.pl - Joseph Kocherhans konrad@gwu.edu knox David Krauth @@ -360,7 +364,6 @@ answer newbie questions, and generally made Django that much better: limodou Philip Lindborg Simon Litchfield - Daniel Lindsley Trey Long Laurent Luce Martin Mahner diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 6b9c7df14a..131f9c0f26 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -88,6 +88,23 @@ Malcolm Tredinnick *Malcolm passed away on March 17, 2013.* +`Luke Plant`_ + At University Luke studied physics and Materials Science and also + met `Michael Meeks`_ who introduced him to Linux and Open Source, + re-igniting an interest in programming. Since then he has + contributed to a number of Open Source projects and worked + professionally as a developer. + + Luke has contributed many excellent improvements to Django, + including database-level improvements, the CSRF middleware and + many unit tests. + + Luke currently works for a church in Bradford, UK, and part-time + as a freelance developer. + +.. _luke plant: http://lukeplant.me.uk/ +.. _michael meeks: http://en.wikipedia.org/wiki/Michael_Meeks_(software) + `Russell Keith-Magee`_ Russell studied physics as an undergraduate, and studied neural networks for his PhD. His first job was with a startup in the defense industry developing @@ -102,6 +119,42 @@ Malcolm Tredinnick .. _russell keith-magee: http://cecinestpasun.com/ +`James Bennett`_ + James is Django's release manager, and also contributes to the + documentation and provide the occasional bugfix. + + James came to Web development from philosophy when he discovered + that programmers get to argue just as much while collecting much + better pay. He lives in Lawrence, Kansas and previously worked at + World Online; currently, he's part of the Web development team at + Mozilla. + + He `keeps a blog`_, and enjoys fine port and talking to his car. + +.. _james bennett: http://b-list.org/ +.. _keeps a blog: `james bennett`_ + +`Gary Wilson`_ + Gary starting contributing patches to Django in 2006 while developing Web + applications for `The University of Texas`_ (UT). Since, he has made + contributions to the email and forms systems, as well as many other + improvements and code cleanups throughout the code base. + + Gary is currently a developer and software engineering graduate student at + UT, where his dedication to spreading the ways of Python and Django never + ceases. + + Gary lives in Austin, Texas, USA. + +.. _Gary Wilson: http://thegarywilson.com/ +.. _The University of Texas: http://www.utexas.edu/ + +Matt Boersma + Matt is responsible for Django's Oracle support. + +Ian Kelly + Ian is also responsible for Django's support for Oracle. + Joseph Kocherhans Joseph was the director of lead development at EveryBlock and previously developed at the Lawrence Journal-World. He is treasurer of the `Django @@ -119,23 +172,6 @@ Joseph Kocherhans .. _django software foundation: https://www.djangoproject.com/foundation/ .. _charango: http://en.wikipedia.org/wiki/Charango -`Luke Plant`_ - At University Luke studied physics and Materials Science and also - met `Michael Meeks`_ who introduced him to Linux and Open Source, - re-igniting an interest in programming. Since then he has - contributed to a number of Open Source projects and worked - professionally as a developer. - - Luke has contributed many excellent improvements to Django, - including database-level improvements, the CSRF middleware and - many unit tests. - - Luke currently works for a church in Bradford, UK, and part-time - as a freelance developer. - -.. _luke plant: http://lukeplant.me.uk/ -.. _michael meeks: http://en.wikipedia.org/wiki/Michael_Meeks_(software) - `Brian Rosner`_ Brian is currently the tech lead at Eldarion_ managing and developing Django / Pinax_ based Web sites. He enjoys learning more about programming @@ -153,21 +189,6 @@ Joseph Kocherhans .. _django dose: http://djangodose.com/ .. _pinax: http://pinaxproject.com/ -`Gary Wilson`_ - Gary starting contributing patches to Django in 2006 while developing Web - applications for `The University of Texas`_ (UT). Since, he has made - contributions to the email and forms systems, as well as many other - improvements and code cleanups throughout the code base. - - Gary is currently a developer and software engineering graduate student at - UT, where his dedication to spreading the ways of Python and Django never - ceases. - - Gary lives in Austin, Texas, USA. - -.. _Gary Wilson: http://thegarywilson.com/ -.. _The University of Texas: http://www.utexas.edu/ - Justin Bronn Justin Bronn is a computer scientist and attorney specializing in legal topics related to intellectual property and spatial law. @@ -232,6 +253,15 @@ Karen Tracey .. _Alex Gaynor: http://alexgaynor.net .. _Rdio: http://rdio.com +`Simon Meers`_ + Simon discovered Django 0.96 during his Computer Science PhD research and + has been developing with it full-time ever since. His core code + contributions are mostly in Django's admin application. + + Simon works as a freelance developer based in Wollongong, Australia. + +.. _Simon Meers: http://simonmeers.com/ + `Andrew Godwin`_ Andrew is a freelance Python developer and tinkerer, and has been developing against Django since 2007. He graduated from Oxford University @@ -265,6 +295,18 @@ Ramiro Morales Ramiro lives in Córdoba, Argentina. +`Gabriel Hurley`_ + Gabriel has been working with Django since 2008, shortly after the 1.0 + release. Convinced by his business partner that Python and Django were the + right direction for the company, he couldn't have been more happy with the + decision. His contributions range across many areas in Django, but years of + copy-editing and an eye for detail lead him to be particularly at home + while working on Django's documentation. + + Gabriel works as a web developer in Berkeley, CA, USA. + +.. _gabriel hurley: http://strikeawe.com/ + `Chris Beaven`_ Chris has been submitting patches and suggesting crazy ideas for Django since early 2006. An advocate for community involvement and a long-term @@ -290,6 +332,13 @@ Honza Král .. _Whiskey Media: http://www.whiskeymedia.com/ +Tim Graham + When exploring Web frameworks for an independent study project in the fall + of 2008, Tim discovered Django and was lured to it by the documentation. + He enjoys contributing to the docs because they're awesome. + + Tim works as a software engineer and lives in Philadelphia, PA, USA. + `Idan Gazit`_ As a self-professed design geek, Idan was initially attracted to Django sometime between magic-removal and queryset-refactor. Formally trained @@ -439,6 +488,18 @@ Jeremy Dunck .. _Ultimate Frisbee: http://www.montrealultimate.ca .. _Reptiletech: http://www.reptiletech.com +Donald Stufft + Donald found Python and Django in 2007 while trying to find a language, + and web framework that he really enjoyed using after many years of PHP. He + fell in love with the beauty of Python and the way Django made tasks simple + and easy. His contributions to Django focus primarily on ensuring that it + is and remains a secure web framework. + + Donald currently works at `Nebula Inc`_ as a Software Engineer for their + security team and lives in the Greater Philadelphia Area. + +.. _Nebula Inc: https://www.nebula.com/ + `Daniel Lindsley`_ Pythonista since 2003, Djangonaut since 2006. Daniel started with Django just after the v0.90 release (back when ``Manipulators`` looked good) & fell @@ -453,56 +514,6 @@ Jeremy Dunck .. _`Daniel Lindsley`: http://toastdriven.com/ .. _`Amazon Web Services`: https://aws.amazon.com/ -`James Bennett`_ - James is Django's release manager, and also contributes to the - documentation and provide the occasional bugfix. - - James came to Web development from philosophy when he discovered - that programmers get to argue just as much while collecting much - better pay. He lives in Lawrence, Kansas and previously worked at - World Online; currently, he's part of the Web development team at - Mozilla. - - He `keeps a blog`_, and enjoys fine port and talking to his car. - -.. _james bennett: http://b-list.org/ -.. _keeps a blog: `james bennett`_ - -Ian Kelly - Ian is responsible for Django's support for Oracle. - -Matt Boersma - Matt is also responsible for Django's Oracle support. - -`Simon Meers`_ - Simon discovered Django 0.96 during his Computer Science PhD research and - has been developing with it full-time ever since. His core code - contributions are mostly in Django's admin application. He is also helping - to improve Django's documentation. - - Simon works as a freelance developer based in Wollongong, Australia. - -.. _simon meers: http://simonmeers.com/ - -`Gabriel Hurley`_ - Gabriel has been working with Django since 2008, shortly after the 1.0 - release. Convinced by his business partner that Python and Django were the - right direction for the company, he couldn't have been more happy with the - decision. His contributions range across many areas in Django, but years of - copy-editing and an eye for detail lead him to be particularly at home - while working on Django's documentation. - - Gabriel works as a web developer in Berkeley, CA, USA. - -.. _gabriel hurley: http://strikeawe.com/ - -Tim Graham - When exploring Web frameworks for an independent study project in the fall - of 2008, Tim discovered Django and was lured to it by the documentation. - He enjoys contributing to the docs because they're awesome. - - Tim works as a software engineer and lives in Philadelphia, PA, USA. - Marc Tamlyn Marc started life on the web using Django 1.2 back in 2010, and has never looked back. He was involved with rewriting the class based view @@ -515,18 +526,6 @@ Marc Tamlyn .. _CCBV: http://ccbv.co.uk/ .. _Incuna Ltd: http://incuna.com/ -Donald Stufft - Donald found Python and Django in 2007 while trying to find a language, - and web framework that he really enjoyed using after many years of PHP. He - fell in love with the beauty of Python and the way Django made tasks simple - and easy. His contributions to Django focus primarily on ensuring that it - is and remains a secure web framework. - - Donald currently works at `Nebula Inc`_ as a Software Engineer for their - security team and lives in the Greater Philadelphia Area. - -.. _Nebula Inc: https://www.nebula.com/ - Developers Emeritus =================== From 9012a9e2000020493b881a5b79cc801c62180796 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Thu, 16 May 2013 18:29:18 +0200 Subject: [PATCH 005/249] Fixed #20422 -- Applied makemessage's --ignore patterns to full path Fix makemessage's --ignore patterns being applied to the full path instead of the file name. Thanks to nnseva for the report and the original patch. --- django/core/management/commands/makemessages.py | 7 +++---- tests/i18n/commands/extraction.py | 9 +++++++-- tests/i18n/commands/templates/xxx_ignored.html | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 tests/i18n/commands/templates/xxx_ignored.html diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index bc171176c2..e693e17400 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -309,10 +309,9 @@ class Command(NoArgsCommand): """ Check if the given path should be ignored or not. """ - for pattern in ignore_patterns: - if fnmatch.fnmatchcase(path, pattern): - return True - return False + filename = os.path.basename(path) + ignore = lambda pattern: fnmatch.fnmatchcase(filename, pattern) + return any(ignore(pattern) for pattern in ignore_patterns) dir_suffix = '%s*' % os.sep norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in self.ignore_patterns] diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index 7c482e58fb..8696ae453b 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -279,17 +279,22 @@ class IgnoredExtractorTests(ExtractorTests): def test_ignore_option(self): os.chdir(self.test_dir) - pattern1 = os.path.join('ignore_dir', '*') + ignore_patterns = [ + os.path.join('ignore_dir', '*'), + 'xxx_*', + ] stdout = StringIO() management.call_command('makemessages', locale=LOCALE, verbosity=2, - ignore_patterns=[pattern1], stdout=stdout) + ignore_patterns=ignore_patterns, stdout=stdout) data = stdout.getvalue() self.assertTrue("ignoring directory ignore_dir" in data) + self.assertTrue("ignoring file xxx_ignored.html" in data) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: po_contents = fp.read() self.assertMsgId('This literal should be included.', po_contents) self.assertNotMsgId('This should be ignored.', po_contents) + self.assertNotMsgId('This should be ignored too.', po_contents) class SymlinkExtractorTests(ExtractorTests): diff --git a/tests/i18n/commands/templates/xxx_ignored.html b/tests/i18n/commands/templates/xxx_ignored.html new file mode 100644 index 0000000000..a41cbe202a --- /dev/null +++ b/tests/i18n/commands/templates/xxx_ignored.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% trans "This should be ignored too." %} From 051cb1f4c60ac8e7087d92ef34ed41e6684d8b9b Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sat, 18 May 2013 12:32:47 +0200 Subject: [PATCH 006/249] Fixed #20411 -- Don't let invalid referers blow up CSRF same origin checks. Thanks to edevil for the report and saz for the patch. --- django/utils/http.py | 5 ++++- tests/csrf_tests/tests.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/django/utils/http.py b/django/utils/http.py index 15fac6bfca..9897df4fb0 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -226,7 +226,10 @@ def same_origin(url1, url2): Checks if two URLs are 'same-origin' """ p1, p2 = urllib_parse.urlparse(url1), urllib_parse.urlparse(url2) - return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port) + try: + return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port) + except ValueError: + return False def is_safe_url(url, host=None): """ diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 5300b2172a..b9e8cb5f75 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -283,6 +283,19 @@ class CsrfViewMiddlewareTest(TestCase): self.assertNotEqual(None, req2) self.assertEqual(403, req2.status_code) + @override_settings(ALLOWED_HOSTS=['www.example.com']) + def test_https_malformed_referer(self): + """ + Test that a POST HTTPS request with a bad referer is rejected + """ + req = self._get_POST_request_with_token() + req._is_secure_override = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'http://http://www.example.com/' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertNotEqual(None, req2) + self.assertEqual(403, req2.status_code) + @override_settings(ALLOWED_HOSTS=['www.example.com']) def test_https_good_referer(self): """ From 8fd44b2551b9cca765b216a31306f9c6935f1492 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 18 May 2013 12:37:22 +0200 Subject: [PATCH 007/249] Fixed #20356 -- Prevented crash when HTTP_REFERER contains non-ascii Thanks srusskih for the report and Aymeric Augustin for the review. --- django/middleware/common.py | 3 ++- tests/middleware/tests.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/django/middleware/common.py b/django/middleware/common.py index 92f8cb3992..250737970d 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.mail import mail_managers from django.core import urlresolvers from django import http +from django.utils.encoding import force_text from django.utils.http import urlquote from django.utils import six @@ -140,7 +141,7 @@ class BrokenLinkEmailsMiddleware(object): if response.status_code == 404 and not settings.DEBUG: domain = request.get_host() path = request.get_full_path() - referer = request.META.get('HTTP_REFERER', '') + referer = force_text(request.META.get('HTTP_REFERER', ''), errors='replace') is_internal = self.is_internal_request(domain, referer) is_not_search_engine = '?' not in referer is_ignorable = self.is_ignorable_404(path) diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index f2f7f4df66..e526da4772 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -22,7 +22,7 @@ from django.test.utils import override_settings from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange -from django.utils.unittest import expectedFailure +from django.utils.unittest import expectedFailure, skipIf from transactions.tests import IgnorePendingDeprecationWarningsMixin @@ -320,6 +320,14 @@ class BrokenLinkEmailsMiddlewareTest(TestCase): BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) self.assertEqual(len(mail.outbox), 0) + @skipIf(six.PY3, "HTTP_REFERER is str type on Python 3") + def test_404_error_nonascii_referrer(self): + # Such referer strings should not happen, but anyway, if it happens, + # let's not crash + self.req.META['HTTP_REFERER'] = b'http://testserver/c/\xd0\xbb\xd0\xb8/' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) + class ConditionalGetMiddlewareTest(TestCase): urls = 'middleware.cond_get_urls' From 0b0741602b18928a418ba4661dc24b880daa5253 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sat, 18 May 2013 12:32:48 +0200 Subject: [PATCH 008/249] Fixed #20294 -- Documented context processors in TemplateResponseMixin. --- docs/ref/class-based-views/mixins-simple.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 6796675529..377c85cc3b 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -60,6 +60,17 @@ TemplateResponseMixin altered later (e.g. in :ref:`template response middleware `). + .. admonition:: Context processors + + ``TemplateResponse`` uses :class:`~django.template.RequestContext` + which means that callables defined in + :setting:`TEMPLATE_CONTEXT_PROCESSORS` may overwrite template + variables defined in your views. For example, if you subclass + :class:`DetailView ` and + set ``context_object_name`` to ``user``, the + ``django.contrib.auth.context_processors.auth`` context processor + will happily overwrite your variable with current user. + If you need custom template loading or custom context object instantiation, create a ``TemplateResponse`` subclass and assign it to ``response_class``. From 7b85ef9dfb83bd2f2cde46b9836b9fd12a033b26 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 12:45:06 +0200 Subject: [PATCH 009/249] Fixed #20408 -- Clarified that values_list() doesn't return a list. Thanks marktranchant, bmispelon, and alextreme. --- docs/ref/models/querysets.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index ffada19082..14123cd79a 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -544,6 +544,11 @@ It is an error to pass in ``flat`` when there is more than one field. If you don't pass any values to ``values_list()``, it will return all the fields in the model, in the order they were declared. +Note that this method returns a ``ValuesListQuerySet``. This class behaves +like a list. Most of the time this is enough, but if you require an actual +Python list object, you can simply call ``list()`` on it, which will evaluate +the queryset. + dates ~~~~~ From 566e284c565a9ea95d81756c6b1f94dfa63fc61b Mon Sep 17 00:00:00 2001 From: Senko Rasic Date: Sat, 18 May 2013 12:26:38 +0200 Subject: [PATCH 010/249] Added test for multipart, non form-data POST. Closes #9054. The bug itself is no longer present. --- tests/requests/tests.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/requests/tests.py b/tests/requests/tests.py index daf426ea47..56d58c4c75 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -503,9 +503,9 @@ class RequestsTests(SimpleTestCase): }) self.assertEqual(request.POST, {'key': ['España']}) - def test_body_after_POST_multipart(self): + def test_body_after_POST_multipart_form_data(self): """ - Reading body after parsing multipart is not allowed + Reading body after parsing multipart/form-data is not allowed """ # Because multipart is used for large amounts fo data i.e. file uploads, # we don't want the data held in memory twice, and we don't want to @@ -524,6 +524,29 @@ class RequestsTests(SimpleTestCase): self.assertEqual(request.POST, {'name': ['value']}) self.assertRaises(Exception, lambda: request.body) + def test_body_after_POST_multipart_related(self): + """ + Reading body after parsing multipart that isn't form-data is allowed + """ + # Ticket #9054 + # There are cases in which the multipart data is related instead of + # being a binary upload, in which case it should still be accessible + # via body. + payload_data = "\r\n".join([ + '--boundary', + 'Content-ID: id; name="name"', + '', + 'value', + '--boundary--' + '']) + payload = FakePayload(payload_data) + request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'multipart/related; boundary=boundary', + 'CONTENT_LENGTH': len(payload), + 'wsgi.input': payload}) + self.assertEqual(request.POST, {}) + self.assertEqual(request.body, payload_data) + def test_POST_multipart_with_content_length_zero(self): """ Multipart POST requests with Content-Length >= 0 are valid and need to be handled. From 029c690b1484c6c16e9f9974fbec6bcc2b72dd77 Mon Sep 17 00:00:00 2001 From: Jacob Burch Date: Sat, 18 May 2013 12:55:13 +0200 Subject: [PATCH 011/249] #20432: Fix for GroupAdmin test --- django/contrib/auth/tests/test_management.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index 04fd4941ab..fee0a29e7b 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -7,6 +7,7 @@ from django.contrib.auth.management.commands import changepassword from django.contrib.auth.models import User from django.contrib.auth.tests.test_custom_user import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.contenttypes.models import ContentType from django.core.management import call_command from django.core.management.base import CommandError from django.core.management.validation import get_validation_errors @@ -195,6 +196,7 @@ class PermissionDuplicationTestCase(TestCase): def tearDown(self): models.Permission._meta.permissions = self._original_permissions + ContentType.objects.clear_cache() def test_duplicated_permissions(self): """ From 340115200f459d02e9a3e61c9704653215e185d5 Mon Sep 17 00:00:00 2001 From: Jacob Burch Date: Sat, 18 May 2013 12:55:13 +0200 Subject: [PATCH 012/249] Fixed #20432 -- Test failure in admin_views. The failure was triggered by a cache leak. --- django/contrib/auth/tests/test_management.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index 04fd4941ab..fee0a29e7b 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -7,6 +7,7 @@ from django.contrib.auth.management.commands import changepassword from django.contrib.auth.models import User from django.contrib.auth.tests.test_custom_user import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.contenttypes.models import ContentType from django.core.management import call_command from django.core.management.base import CommandError from django.core.management.validation import get_validation_errors @@ -195,6 +196,7 @@ class PermissionDuplicationTestCase(TestCase): def tearDown(self): models.Permission._meta.permissions = self._original_permissions + ContentType.objects.clear_cache() def test_duplicated_permissions(self): """ From 215647c0f7614abe4fe0cd76bc70dd7b02f829b4 Mon Sep 17 00:00:00 2001 From: Shai Berger Date: Thu, 16 May 2013 18:30:52 +0300 Subject: [PATCH 013/249] Fixed #20386 - Introspection problem on Oracle Made introspection always return a unicode as column name on Oracle. Thanks aaugustin for review and suggestion to use force_text(). --- django/db/backends/oracle/introspection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index ff56dca5c2..608901f081 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -1,4 +1,5 @@ from django.db.backends import BaseDatabaseIntrospection, FieldInfo +from django.utils.encoding import force_text import cx_Oracle import re @@ -48,7 +49,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): cursor.execute("SELECT * FROM %s WHERE ROWNUM < 2" % self.connection.ops.quote_name(table_name)) description = [] for desc in cursor.description: - description.append(FieldInfo(*((desc[0].lower(),) + desc[1:]))) + name = force_text(desc[0]) # cx_Oracle always returns a 'str' on both Python 2 and 3 + description.append(FieldInfo(*(name.lower(),) + desc[1:])) return description def table_name_converter(self, name): From 5e208d579d64cd62c9e1e57b2b96c4469d8f1faf Mon Sep 17 00:00:00 2001 From: zyegfryed Date: Sat, 18 May 2013 13:20:52 +0200 Subject: [PATCH 014/249] Fixed #20433: Extract catalog compilation code from javascript_catalog view. --- django/views/i18n.py | 68 ++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/django/views/i18n.py b/django/views/i18n.py index 37ec10b552..71ac005855 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -184,38 +184,8 @@ def render_javascript_catalog(catalog=None, plural=None): return http.HttpResponse(template.render(context), 'text/javascript') -def null_javascript_catalog(request, domain=None, packages=None): - """ - Returns "identity" versions of the JavaScript i18n functions -- i.e., - versions that don't actually do anything. - """ - return render_javascript_catalog() - - -def javascript_catalog(request, domain='djangojs', packages=None): - """ - Returns the selected language catalog as a javascript library. - - Receives the list of packages to check for translations in the - packages parameter either from an infodict or as a +-delimited - string from the request. Default is 'django.conf'. - - Additionally you can override the gettext domain for this view, - but usually you don't want to do that, as JavaScript messages - go to the djangojs domain. But this might be needed if you - deliver your JavaScript source from Django templates. - """ +def get_javascript_catalog(locale, domain, packages): default_locale = to_locale(settings.LANGUAGE_CODE) - locale = to_locale(get_language()) - - if request.GET and 'language' in request.GET: - if check_for_language(request.GET['language']): - locale = to_locale(request.GET['language']) - - if packages is None: - packages = ['django.conf'] - if isinstance(packages, six.string_types): - packages = packages.split('+') packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS] t = {} paths = [] @@ -296,4 +266,40 @@ def javascript_catalog(request, domain='djangojs', packages=None): for k, v in pdict.items(): catalog[k] = [v.get(i, '') for i in range(maxcnts[msgid] + 1)] + return catalog, plural + + +def null_javascript_catalog(request, domain=None, packages=None): + """ + Returns "identity" versions of the JavaScript i18n functions -- i.e., + versions that don't actually do anything. + """ + return render_javascript_catalog() + + +def javascript_catalog(request, domain='djangojs', packages=None): + """ + Returns the selected language catalog as a javascript library. + + Receives the list of packages to check for translations in the + packages parameter either from an infodict or as a +-delimited + string from the request. Default is 'django.conf'. + + Additionally you can override the gettext domain for this view, + but usually you don't want to do that, as JavaScript messages + go to the djangojs domain. But this might be needed if you + deliver your JavaScript source from Django templates. + """ + locale = to_locale(get_language()) + + if request.GET and 'language' in request.GET: + if check_for_language(request.GET['language']): + locale = to_locale(request.GET['language']) + + if packages is None: + packages = ['django.conf'] + if isinstance(packages, six.string_types): + packages = packages.split('+') + + catalog, plural = get_javascript_catalog(locale, domain, packages) return render_javascript_catalog(catalog, plural) From 4bed64c4171194385f5995e657885a3ef35ea49b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 13:34:08 +0200 Subject: [PATCH 015/249] Made test introduced in 566e284c pass on Python 3. --- tests/requests/tests.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/requests/tests.py b/tests/requests/tests.py index 56d58c4c75..676cd05679 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -532,13 +532,13 @@ class RequestsTests(SimpleTestCase): # There are cases in which the multipart data is related instead of # being a binary upload, in which case it should still be accessible # via body. - payload_data = "\r\n".join([ - '--boundary', - 'Content-ID: id; name="name"', - '', - 'value', - '--boundary--' - '']) + payload_data = b"\r\n".join([ + b'--boundary', + b'Content-ID: id; name="name"', + b'', + b'value', + b'--boundary--' + b'']) payload = FakePayload(payload_data) request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/related; boundary=boundary', From 493aca453aa94f160764d9a89c8043f7c9a67a78 Mon Sep 17 00:00:00 2001 From: Senko Rasic Date: Sat, 18 May 2013 13:44:27 +0200 Subject: [PATCH 016/249] Fixed #11160 - Ensure full_clean is called from non_form_errors Updated FormSet.non_form_errors() to ensure full_clean() has been called before returning the errors. --- django/forms/formsets.py | 11 +++++++---- tests/forms_tests/tests/test_formsets.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index d421770093..3ec94d20ec 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -250,9 +250,9 @@ class BaseFormSet(object): form -- i.e., from formset.clean(). Returns an empty ErrorList if there are none. """ - if self._non_form_errors is not None: - return self._non_form_errors - return self.error_class() + if self._non_form_errors is None: + self.full_clean() + return self._non_form_errors @property def errors(self): @@ -291,9 +291,12 @@ class BaseFormSet(object): def full_clean(self): """ - Cleans all of self.data and populates self._errors. + Cleans all of self.data and populates self._errors and + self._non_form_errors. """ self._errors = [] + self._non_form_errors = self.error_class() + if not self.is_bound: # Stop further processing. return for i in range(0, self.total_form_count()): diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 4ac3c5ecf1..31adb921ba 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -972,6 +972,20 @@ class FormsFormsetTestCase(TestCase): finally: formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM + def test_non_form_errors_run_full_clean(self): + # Regression test for #11160 + # If non_form_errors() is called without calling is_valid() first, + # it should ensure that full_clean() is called. + class BaseCustomFormSet(BaseFormSet): + def clean(self): + raise ValidationError("This is a non-form error") + + ChoiceFormSet = formset_factory(Choice, formset=BaseCustomFormSet) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertTrue(isinstance(formset.non_form_errors(), ErrorList)) + self.assertEqual(list(formset.non_form_errors()), + ['This is a non-form error']) + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered From b11b036145d6a1e9d2192e332c8a221a595be1b6 Mon Sep 17 00:00:00 2001 From: Senko Rasic Date: Sat, 18 May 2013 13:49:13 +0200 Subject: [PATCH 017/249] Added myself to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 3544e3b81a..e7046f4386 100644 --- a/AUTHORS +++ b/AUTHORS @@ -468,6 +468,7 @@ answer newbie questions, and generally made Django that much better: Luciano Ramalho Amit Ramon Philippe Raoult + Senko Rašić Massimiliano Ravelli Brian Ray Lee Reilly From be826aafacb9fe266209604116efe128a0b4cdc0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 13:51:09 +0200 Subject: [PATCH 018/249] Fixed #20402: removed as-limit from uWSGI example. It can fail in hard-to-diagnose ways. --- docs/howto/deployment/wsgi/uwsgi.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/howto/deployment/wsgi/uwsgi.txt b/docs/howto/deployment/wsgi/uwsgi.txt index 5b40d5f2f7..22f39342d6 100644 --- a/docs/howto/deployment/wsgi/uwsgi.txt +++ b/docs/howto/deployment/wsgi/uwsgi.txt @@ -62,7 +62,6 @@ Here's an example command to start a uWSGI server:: --processes=5 \ # number of worker processes --uid=1000 --gid=2000 \ # if root, uwsgi can drop privileges --harakiri=20 \ # respawn processes taking more than 20 seconds - --limit-as=128 \ # limit the project to 128 MB --max-requests=5000 \ # respawn processes after serving 5000 requests --vacuum \ # clear environment on exit --home=/path/to/virtual/env \ # optional path to a virtualenv From 1c921cfac34daa9c18e4e235e20fa7beb8929f1c Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Wed, 10 Apr 2013 11:27:28 +0100 Subject: [PATCH 019/249] Fixed #20235 -- Use self.object_list if object_list not present in get_context_data kwargs. This is so MultipleObjectMixin can be used in the same way as SingleObjectMixin. --- AUTHORS | 1 + django/views/generic/list.py | 4 ++-- tests/generic_views/test_base.py | 20 ++++++++++++++++++++ tests/generic_views/views.py | 11 +++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 3544e3b81a..70f8bb22d9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -274,6 +274,7 @@ answer newbie questions, and generally made Django that much better: Eric Holscher Ian Holsman Kieran Holland + Markus Holtermann Sung-Jin Hong Leo "hylje" Honkanen Matt Hoskins diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 08c4bbcda0..1aff3454f4 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -105,7 +105,7 @@ class MultipleObjectMixin(ContextMixin): """ Get the context for this view. """ - queryset = kwargs.pop('object_list') + queryset = kwargs.pop('object_list', self.object_list) page_size = self.get_paginate_by(queryset) context_object_name = self.get_context_object_name(queryset) if page_size: @@ -149,7 +149,7 @@ class BaseListView(MultipleObjectMixin, View): if is_empty: raise Http404(_("Empty list and '%(class_name)s.allow_empty' is False.") % {'class_name': self.__class__.__name__}) - context = self.get_context_data(object_list=self.object_list) + context = self.get_context_data() return self.render_to_response(context) diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 0e84e17132..2eadee2b42 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -411,3 +411,23 @@ class GetContextDataTest(unittest.TestCase): # test that kwarg overrides values assigned higher up context = test_view.get_context_data(test_name='test_value') self.assertEqual(context['test_name'], 'test_value') + + +class UseMultipleObjectMixinTest(unittest.TestCase): + rf = RequestFactory() + + def test_use_queryset_from_view(self): + test_view = views.CustomMultipleObjectMixinView() + test_view.get(self.rf.get('/')) + # Don't pass queryset as argument + context = test_view.get_context_data() + self.assertEqual(context['object_list'], test_view.queryset) + + def test_overwrite_queryset(self): + test_view = views.CustomMultipleObjectMixinView() + test_view.get(self.rf.get('/')) + queryset = [{'name': 'Lennon'}, {'name': 'Ono'}] + self.assertNotEqual(test_view.queryset, queryset) + # Overwrite the view's queryset with queryset from kwarg + context = test_view.get_context_data(object_list=queryset) + self.assertEqual(context['object_list'], queryset) diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index aa8777e8c6..4dda3fe0e0 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -201,6 +201,17 @@ class BookDetailGetObjectCustomQueryset(BookDetail): return super(BookDetailGetObjectCustomQueryset,self).get_object( queryset=Book.objects.filter(pk=2)) + +class CustomMultipleObjectMixinView(generic.list.MultipleObjectMixin, generic.View): + queryset = [ + {'name': 'John'}, + {'name': 'Yoko'}, + ] + + def get(self, request): + self.object_list = self.get_queryset() + + class CustomContextView(generic.detail.SingleObjectMixin, generic.View): model = Book object = Book(name='dummy') From 756b81dbd1a947351670b66c7e91116abe6aa5c2 Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 14:13:00 +0200 Subject: [PATCH 020/249] Fixed #13546 -- Easier handling of localize field options in ModelForm --- django/forms/models.py | 37 +++++++++++++++++---------- docs/ref/forms/models.txt | 16 +++++++----- docs/releases/1.6.txt | 4 +++ docs/topics/forms/modelforms.txt | 36 ++++++++++++++++++++++++++ tests/model_forms_regress/tests.py | 35 +++++++++++++++++++++++++ tests/model_formsets_regress/tests.py | 7 ++++- 6 files changed, 114 insertions(+), 21 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index af5cda8faf..6b61560cca 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -136,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None): data[f.name] = f.value_from_object(instance) return data -def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None): +def fields_for_model(model, fields=None, exclude=None, widgets=None, localized_fields=None, formfield_callback=None): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -162,10 +162,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c continue if exclude and f.name in exclude: continue + + kwargs = {} if widgets and f.name in widgets: - kwargs = {'widget': widgets[f.name]} - else: - kwargs = {} + kwargs['widget'] = widgets[f.name] + if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields): + kwargs['localize'] = True if formfield_callback is None: formfield = f.formfield(**kwargs) @@ -192,6 +194,7 @@ class ModelFormOptions(object): self.fields = getattr(options, 'fields', None) self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) + self.localized_fields = getattr(options, 'localized_fields', None) class ModelFormMetaclass(type): @@ -215,7 +218,7 @@ class ModelFormMetaclass(type): # We check if a string was passed to `fields` or `exclude`, # which is likely to be a mistake where the user typed ('foo') instead # of ('foo',) - for opt in ['fields', 'exclude']: + for opt in ['fields', 'exclude', 'localized_fields']: value = getattr(opts, opt) if isinstance(value, six.string_types) and value != ALL_FIELDS: msg = ("%(model)s.Meta.%(opt)s cannot be a string. " @@ -242,8 +245,9 @@ class ModelFormMetaclass(type): # fields from the model" opts.fields = None - fields = fields_for_model(opts.model, opts.fields, - opts.exclude, opts.widgets, formfield_callback) + fields = fields_for_model(opts.model, opts.fields, opts.exclude, + opts.widgets, opts.localized_fields, formfield_callback) + # make sure opts.fields doesn't specify an invalid field none_model_fields = [k for k, v in six.iteritems(fields) if not v] missing_fields = set(none_model_fields) - \ @@ -409,7 +413,7 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): pass def modelform_factory(model, form=ModelForm, fields=None, exclude=None, - formfield_callback=None, widgets=None): + localized_fields=None, widgets=None, formfield_callback=None): """ Returns a ModelForm containing form fields for the given model. @@ -423,6 +427,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, ``widgets`` is a dictionary of model field names mapped to a widget. + ``localized_fields`` is a list of names of fields which should be localized. + ``formfield_callback`` is a callable that takes a model field and returns a form field. """ @@ -438,6 +444,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, attrs['exclude'] = exclude if widgets is not None: attrs['widgets'] = widgets + if localized_fields is not None: + attrs['localized_fields'] = localized_fields # If parent form class already has an inner Meta, the Meta we're # creating needs to inherit from the parent's inner meta. @@ -726,8 +734,8 @@ class BaseModelFormSet(BaseFormSet): def modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, - can_order=False, max_num=None, fields=None, - exclude=None, widgets=None, validate_max=False): + can_order=False, max_num=None, fields=None, exclude=None, + widgets=None, validate_max=False, localized_fields=None): """ Returns a FormSet class for the given Django model class. """ @@ -748,7 +756,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, form = modelform_factory(model, form=form, fields=fields, exclude=exclude, formfield_callback=formfield_callback, - widgets=widgets) + widgets=widgets, localized_fields=localized_fields) FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, can_order=can_order, can_delete=can_delete, validate_max=validate_max) @@ -885,9 +893,9 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): def inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, - fields=None, exclude=None, - extra=3, can_order=False, can_delete=True, max_num=None, - formfield_callback=None, widgets=None, validate_max=False): + fields=None, exclude=None, extra=3, can_order=False, + can_delete=True, max_num=None, formfield_callback=None, + widgets=None, validate_max=False, localized_fields=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -910,6 +918,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'max_num': max_num, 'widgets': widgets, 'validate_max': validate_max, + 'localized_fields': localized_fields, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 9b3480758a..54d34160a5 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -5,7 +5,7 @@ Model Form Functions .. module:: django.forms.models :synopsis: Django's functions for building model forms and formsets. -.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) +.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None) Returns a :class:`~django.forms.ModelForm` class for the given ``model``. You can optionally pass a ``form`` argument to use as a starting point for @@ -20,6 +20,8 @@ Model Form Functions ``widgets`` is a dictionary of model field names mapped to a widget. + ``localized_fields`` is a list of names of fields which should be localized. + ``formfield_callback`` is a callable that takes a model field and returns a form field. @@ -33,12 +35,14 @@ Model Form Functions information. Omitting any definition of the fields to use will result in all fields being used, but this behaviour is deprecated. -.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False) + The ``localized_fields`` parameter was added. + +.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None) Returns a ``FormSet`` class for the given ``model`` class. Arguments ``model``, ``form``, ``fields``, ``exclude``, - ``formfield_callback`` and ``widgets`` are all passed through to + ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, @@ -50,9 +54,9 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets`` and the ``validate_max`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. -.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False) +.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None) Returns an ``InlineFormSet`` using :func:`modelformset_factory` with defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and @@ -65,4 +69,4 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets`` and the ``validate_max`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 98889254cd..c9b3715c45 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -234,6 +234,10 @@ Minor features .. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`PIL`: https://pypi.python.org/pypi/PIL +* :doc:`ModelForm ` accepts a new + Meta option: ``localized_fields``. Fields included in this list will be localized + (by setting ``localize`` on the form field). + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index e58dade736..3cd8c69ab5 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -474,6 +474,24 @@ parameter when declaring the form field:: See the :doc:`form field documentation ` for more information on fields and their arguments. + +Enabling localization of fields +------------------------------- + +.. versionadded:: 1.6 + +By default, the fields in a ``ModelForm`` will not localize their data. To +enable localization for fields, you can use the ``localized_fields`` +attribute on the ``Meta`` class. + + >>> class AuthorForm(ModelForm): + ... class Meta: + ... model = Author + ... localized_fields = ('birth_date',) + +If ``localized_fields`` is set to the special value ``'__all__'``, all fields +will be localized. + .. _overriding-modelform-clean-method: Overriding the clean() method @@ -570,6 +588,10 @@ keyword arguments, or the corresponding attributes on the ``ModelForm`` inner ``Meta`` class. Please see the ``ModelForm`` :ref:`modelforms-selecting-fields` documentation. +... or enable localization for specific fields:: + + >>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",)) + .. _model-formsets: Model formsets @@ -663,6 +685,20 @@ class of a ``ModelForm`` works:: >>> AuthorFormSet = modelformset_factory( ... Author, widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20}) +Enabling localization for fields with ``localized_fields`` +---------------------------------------------------------- + +.. versionadded:: 1.6 + +Using the ``localized_fields`` parameter, you can enable localization for +fields in the form. + + >>> AuthorFormSet = modelformset_factory( + ... Author, localized_fields=('value',)) + +If ``localized_fields`` is set to the special value ``'__all__'``, all fields +will be localized. + Providing initial values ------------------------ diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 0e033e033f..80900a59e0 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -92,6 +92,41 @@ class OverrideCleanTests(TestCase): self.assertEqual(form.instance.left, 1) + +class PartiallyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = ('left', 'right',) + + +class FullyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "__all__" + +class LocalizedModelFormTest(TestCase): + def test_model_form_applies_localize_to_some_fields(self): + f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertFalse(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_applies_localize_to_all_fields(self): + f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertTrue(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_refuses_arbitrary_string(self): + with self.assertRaises(TypeError): + class BrokenLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "foo" + + # Regression test for #12960. # Make sure the cleaned_data returned from ModelForm.clean() is applied to the # model instance. diff --git a/tests/model_formsets_regress/tests.py b/tests/model_formsets_regress/tests.py index 38ebd9d24b..c8fb1e76c1 100644 --- a/tests/model_formsets_regress/tests.py +++ b/tests/model_formsets_regress/tests.py @@ -273,6 +273,7 @@ class UserSiteForm(forms.ModelForm): 'id': CustomWidget, 'data': CustomWidget, } + localized_fields = ('data',) class Callback(object): @@ -297,19 +298,23 @@ class FormfieldCallbackTests(TestCase): form = Formset().forms[0] self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def test_modelformset_factory_default(self): Formset = modelformset_factory(UserSite, form=UserSiteForm) form = Formset().forms[0] self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def assertCallbackCalled(self, callback): id_field, user_field, data_field = UserSite._meta.fields expected_log = [ (id_field, {'widget': CustomWidget}), (user_field, {}), - (data_field, {'widget': CustomWidget}), + (data_field, {'widget': CustomWidget, 'localize': True}), ] self.assertEqual(callback.log, expected_log) From d77082b43877b4df0f29fa977b73b7b4feab84d4 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sat, 18 May 2013 14:22:40 +0200 Subject: [PATCH 021/249] Added example of using sitemaps with static views. References #16829. --- docs/ref/contrib/sitemaps.txt | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index d37ee83378..56a15cb9e0 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -280,6 +280,46 @@ Here's an example of a :doc:`URLconf ` using both:: .. _URLconf: ../url_dispatch/ +Sitemap for static views +======================== + +Often you want the search engine crawlers to index views which are neither +object detail pages nor flatpages. The solution is to explicitly list URL +names for these views in ``items`` and call +:func:`~django.core.urlresolvers.reverse` in the ``location`` method of +the sitemap. For example:: + + # sitemaps.py + from django.contrib import sitemaps + from django.core.urlresolvers import reverse + + class StaticViewSitemap(sitemaps.Sitemap): + priority = 0.5 + changefreq = 'daily' + + def items(self): + return ['main', 'about', 'license'] + + def location(self, item): + return reverse(item) + + # urls.py + from django.conf.urls import patterns, url + from .sitemaps import StaticViewSitemap + + sitemaps = { + 'static': StaticViewSitemap, + } + + urlpatterns = patterns('', + url(r'^$', 'views.main', name='main'), + url(r'^about/$', 'views.about', name='about'), + url(r'^license/$', 'views.license', name='license'), + # ... + url(r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}) + ) + + Creating a sitemap index ======================== From 90f1170bb92cfd588762039fc00477bfcdae11b3 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 18 May 2013 14:24:27 +0200 Subject: [PATCH 022/249] Fixed #20269 -- Adapted PostGIS template create script for CentOS/RHEL Thanks Stephane Benchimol for the report and the initial script and mfandreas for the patch. --- .../contrib/gis/install/create_template_postgis-1.5.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh index 081b5f2656..67c82a8b25 100755 --- a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh +++ b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh @@ -1,9 +1,15 @@ #!/usr/bin/env bash -POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 +if [[ `uname -r | grep el6` ]]; then + POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis + POSTGIS_SQL_FILE=$POSTGIS_SQL_PATH/postgis-64.sql +else + POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 + POSTGIS_SQL_FILE=$POSTGIS_SQL_PATH/postgis.sql +fi createdb -E UTF8 template_postgis # Create the template spatial database. createlang -d template_postgis plpgsql # Adding PLPGSQL language support. psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" -psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines +psql -d template_postgis -f $POSTGIS_SQL_FILE # Loading the PostGIS SQL routines psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" From 92ebb29c5376c1811e8607e96bddc90d24e44e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 14:37:04 +0200 Subject: [PATCH 023/249] Fixes #19919: get_language_from_request() disregards "en-us" and "en" languages when matching Accept-Language --- django/utils/translation/trans_real.py | 27 +++++++++-------- tests/i18n/tests.py | 40 +++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 07353c35ee..f7b1524085 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -364,10 +364,14 @@ def get_supported_language_variant(lang_code, supported=None): if supported is None: from django.conf import settings supported = dict(settings.LANGUAGES) - if lang_code and lang_code not in supported: - lang_code = lang_code.split('-')[0] # e.g. if fr-ca is not supported fallback to fr - if lang_code and lang_code in supported and check_for_language(lang_code): - return lang_code + if lang_code: + # e.g. if fr-CA is not supported, try fr-ca; + # if that fails, fallback to fr. + variants = (lang_code, lang_code.lower(), lang_code.split('-')[0], + lang_code.lower().split('-')[0]) + for code in variants: + if code in supported and check_for_language(code): + return code raise LookupError(lang_code) def get_language_from_path(path, supported=None): @@ -438,14 +442,13 @@ def get_language_from_request(request, check_path=False): # need to check again. return _accepted[normalized] - for lang, dirname in ((accept_lang, normalized), - (accept_lang.split('-')[0], normalized.split('_')[0])): - if lang.lower() not in supported: - continue - for path in all_locale_paths(): - if os.path.exists(os.path.join(path, dirname, 'LC_MESSAGES', 'django.mo')): - _accepted[normalized] = lang - return lang + try: + accept_lang = get_supported_language_variant(accept_lang, supported) + except LookupError: + continue + else: + _accepted[normalized] = accept_lang + return accept_lang try: return get_supported_language_variant(settings.LANGUAGE_CODE, supported) diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 1022c8d2f1..29223e5add 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -30,7 +30,8 @@ from django.utils.translation import (activate, deactivate, ngettext, ngettext_lazy, ungettext, ungettext_lazy, pgettext, pgettext_lazy, - npgettext, npgettext_lazy) + npgettext, npgettext_lazy, + check_for_language) from .commands.tests import can_run_extraction_tests, can_run_compilation_tests if can_run_extraction_tests: @@ -1114,3 +1115,40 @@ class LocaleMiddlewareTests(TestCase): self.assertContains(response, "Oui/Non") response = self.client.get('/en/streaming/') self.assertContains(response, "Yes/No") + +@override_settings( + USE_I18N=True, + LANGUAGES=( + ('bg', 'Bulgarian'), + ('en-us', 'English'), + ), + MIDDLEWARE_CLASSES=( + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), +) +class CountrySpecificLanguageTests(TestCase): + + urls = 'i18n.urls' + + def setUp(self): + trans_real._accepted = {} + self.rf = RequestFactory() + + def test_check_for_language(self): + self.assertTrue(check_for_language('en')) + self.assertTrue(check_for_language('en-us')) + self.assertTrue(check_for_language('en-US')) + + + def test_get_language_from_request(self): + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.8,bg;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('en-us', lang) + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'bg-bg,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('bg', lang) From 1c1695668f678fd8c2db282a79d7d93c11ee0aff Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 14:38:45 +0200 Subject: [PATCH 024/249] Fixed argument order for localized_fields to ensure backwards compatibility --- django/forms/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 6b61560cca..93c8b89efe 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -136,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None): data[f.name] = f.value_from_object(instance) return data -def fields_for_model(model, fields=None, exclude=None, widgets=None, localized_fields=None, formfield_callback=None): +def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -246,7 +246,7 @@ class ModelFormMetaclass(type): opts.fields = None fields = fields_for_model(opts.model, opts.fields, opts.exclude, - opts.widgets, opts.localized_fields, formfield_callback) + opts.widgets, formfield_callback, opts.localized_fields) # make sure opts.fields doesn't specify an invalid field none_model_fields = [k for k, v in six.iteritems(fields) if not v] @@ -413,7 +413,7 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): pass def modelform_factory(model, form=ModelForm, fields=None, exclude=None, - localized_fields=None, widgets=None, formfield_callback=None): + formfield_callback=None, widgets=None, localized_fields=None): """ Returns a ModelForm containing form fields for the given model. From 89955cc35f3636684ea6f2a6c9504b35a3050f0f Mon Sep 17 00:00:00 2001 From: Jacob Burch Date: Sat, 18 May 2013 12:54:59 +0200 Subject: [PATCH 025/249] Fixed #9595 -- Allow non-expiring cache timeouts. Also, streamline the use of 0 and None between cache backends. --- AUTHORS | 1 + django/core/cache/backends/base.py | 10 ++++++--- django/core/cache/backends/db.py | 15 +++++++------ django/core/cache/backends/dummy.py | 8 +++---- django/core/cache/backends/filebased.py | 15 ++++++------- django/core/cache/backends/locmem.py | 19 ++++++++--------- django/core/cache/backends/memcached.py | 22 +++++++++++++------ docs/releases/1.6.txt | 6 ++++++ docs/topics/cache.txt | 13 ++++++++---- tests/cache/tests.py | 28 +++++++++++++++++++++++++ 10 files changed, 97 insertions(+), 40 deletions(-) diff --git a/AUTHORS b/AUTHORS index 70f8bb22d9..0ad6522725 100644 --- a/AUTHORS +++ b/AUTHORS @@ -123,6 +123,7 @@ answer newbie questions, and generally made Django that much better: bthomas btoll@bestweb.net Jonathan Buchanan + Jacob Burch Keith Bussell C8E Chris Cahoon diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index 53b0270a57..deb98e7714 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning): pass +# Stub class to ensure not passing in a `timeout` argument results in +# the default timeout +DEFAULT_TIMEOUT = object() + # Memcached does not accept keys longer than this. MEMCACHE_MAX_KEY_LENGTH = 250 @@ -84,7 +88,7 @@ class BaseCache(object): new_key = self.key_func(key, self.key_prefix, version) return new_key - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache if the key does not already exist. If timeout is given, that timeout will be used for the key; otherwise @@ -101,7 +105,7 @@ class BaseCache(object): """ raise NotImplementedError - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. @@ -163,7 +167,7 @@ class BaseCache(object): # if a subclass overrides it. return self.has_key(key) - def set_many(self, data, timeout=None, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): """ Set a bunch of values in the cache at once from a dict of key/value pairs. For certain backends (memcached), this is much more efficient diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 7749284122..0e59c6d65e 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -9,7 +9,7 @@ except ImportError: import pickle from django.conf import settings -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.db import connections, transaction, router, DatabaseError from django.utils import timezone, six from django.utils.encoding import force_bytes @@ -65,6 +65,7 @@ class DatabaseCache(BaseDatabaseCache): if row is None: return default now = timezone.now() + if row[2] < now: db = router.db_for_write(self.cache_model_class) cursor = connections[db].cursor() @@ -74,18 +75,18 @@ class DatabaseCache(BaseDatabaseCache): value = connections[db].ops.process_clob(row[1]) return pickle.loads(base64.b64decode(force_bytes(value))) - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) self._base_set('set', key, value, timeout) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) return self._base_set('add', key, value, timeout) - def _base_set(self, mode, key, value, timeout=None): - if timeout is None: + def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT): + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout db = router.db_for_write(self.cache_model_class) table = connections[db].ops.quote_name(self._table) @@ -95,7 +96,9 @@ class DatabaseCache(BaseDatabaseCache): num = cursor.fetchone()[0] now = timezone.now() now = now.replace(microsecond=0) - if settings.USE_TZ: + if timeout is None: + exp = datetime.max + elif settings.USE_TZ: exp = datetime.utcfromtimestamp(time.time() + timeout) else: exp = datetime.fromtimestamp(time.time() + timeout) diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py index 9fe9b3b5f5..7ca6114ee4 100644 --- a/django/core/cache/backends/dummy.py +++ b/django/core/cache/backends/dummy.py @@ -1,12 +1,12 @@ "Dummy cache backend" -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT class DummyCache(BaseCache): def __init__(self, host, *args, **kwargs): BaseCache.__init__(self, *args, **kwargs) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) return True @@ -16,7 +16,7 @@ class DummyCache(BaseCache): self.validate_key(key) return default - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) @@ -32,7 +32,7 @@ class DummyCache(BaseCache): self.validate_key(key) return False - def set_many(self, data, timeout=0, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): pass def delete_many(self, keys, version=None): diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index 96194d458f..d19eed4a95 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -9,9 +9,10 @@ try: except ImportError: import pickle -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.encoding import force_bytes + class FileBasedCache(BaseCache): def __init__(self, dir, params): BaseCache.__init__(self, params) @@ -19,7 +20,7 @@ class FileBasedCache(BaseCache): if not os.path.exists(self._dir): self._createdir() - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): if self.has_key(key, version=version): return False @@ -35,7 +36,7 @@ class FileBasedCache(BaseCache): with open(fname, 'rb') as f: exp = pickle.load(f) now = time.time() - if exp < now: + if exp is not None and exp < now: self._delete(fname) else: return pickle.load(f) @@ -43,14 +44,14 @@ class FileBasedCache(BaseCache): pass return default - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) fname = self._key_to_file(key) dirname = os.path.dirname(fname) - if timeout is None: + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout self._cull() @@ -60,8 +61,8 @@ class FileBasedCache(BaseCache): os.makedirs(dirname) with open(fname, 'wb') as f: - now = time.time() - pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL) + expiry = None if timeout is None else time.time() + timeout + pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL) pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) except (IOError, OSError): pass diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py index 76667e9609..1fa17052fd 100644 --- a/django/core/cache/backends/locmem.py +++ b/django/core/cache/backends/locmem.py @@ -6,7 +6,7 @@ try: except ImportError: import pickle -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.synch import RWLock # Global in-memory store of cache data. Keyed by name, to provide @@ -23,7 +23,7 @@ class LocMemCache(BaseCache): self._expire_info = _expire_info.setdefault(name, {}) self._lock = _locks.setdefault(name, RWLock()) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.writer(): @@ -41,10 +41,8 @@ class LocMemCache(BaseCache): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.reader(): - exp = self._expire_info.get(key) - if exp is None: - return default - elif exp > time.time(): + exp = self._expire_info.get(key, 0) + if exp is None or exp > time.time(): try: pickled = self._cache[key] return pickle.loads(pickled) @@ -58,15 +56,16 @@ class LocMemCache(BaseCache): pass return default - def _set(self, key, value, timeout=None): + def _set(self, key, value, timeout=DEFAULT_TIMEOUT): if len(self._cache) >= self._max_entries: self._cull() - if timeout is None: + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout + expiry = None if timeout is None else time.time() + timeout self._cache[key] = value - self._expire_info[key] = time.time() + timeout + self._expire_info[key] = expiry - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.writer(): diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index c942acd52f..64d1c41dc5 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -4,7 +4,7 @@ import time import pickle from threading import local -from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils import six from django.utils.encoding import force_str @@ -36,12 +36,22 @@ class BaseMemcachedCache(BaseCache): return self._client - def _get_memcache_timeout(self, timeout): + def _get_memcache_timeout(self, timeout=DEFAULT_TIMEOUT): """ Memcached deals with long (> 30 days) timeouts in a special way. Call this function to obtain a safe value for your timeout. """ - timeout = timeout or self.default_timeout + if timeout == DEFAULT_TIMEOUT: + return self.default_timeout + + if timeout is None: + # Using 0 in memcache sets a non-expiring timeout. + return 0 + elif int(timeout) == 0: + # Other cache backends treat 0 as set-and-expire. To achieve this + # in memcache backends, a negative timeout must be passed. + timeout = -1 + if timeout > 2592000: # 60*60*24*30, 30 days # See http://code.google.com/p/memcached/wiki/FAQ # "You can set expire times up to 30 days in the future. After that @@ -56,7 +66,7 @@ class BaseMemcachedCache(BaseCache): # Python 2 memcache requires the key to be a byte string. return force_str(super(BaseMemcachedCache, self).make_key(key, version)) - def add(self, key, value, timeout=0, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) return self._cache.add(key, value, self._get_memcache_timeout(timeout)) @@ -67,7 +77,7 @@ class BaseMemcachedCache(BaseCache): return default return val - def set(self, key, value, timeout=0, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self._cache.set(key, value, self._get_memcache_timeout(timeout)) @@ -125,7 +135,7 @@ class BaseMemcachedCache(BaseCache): raise ValueError("Key '%s' not found" % key) return val - def set_many(self, data, timeout=0, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): safe_data = {} for key, value in data.items(): key = self.make_key(key, version=version) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index c9b3715c45..60b3381dd6 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -485,6 +485,12 @@ Miscellaneous changes in 1.6 particularly affect :class:`~django.forms.DecimalField` and :class:`~django.forms.ModelMultipleChoiceField`. +* There have been changes in the way timeouts are handled in cache backends. + Explicitly passing in ``timeout=None`` no longer results in using the + default timeout. It will now set a non-expiring timeout. Passing 0 into the + memcache backend no longer uses the default timeout, and now will + set-and-expire-immediately the value. + Features deprecated in 1.6 ========================== diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 6b6d57511a..a7d54fbeb0 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -707,10 +707,15 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: >>> cache.get('my_key') 'hello, world!' -The ``timeout`` argument is optional and defaults to the ``timeout`` -argument of the appropriate backend in the :setting:`CACHES` setting -(explained above). It's the number of seconds the value should be stored -in the cache. +The ``timeout`` argument is optional and defaults to the ``timeout`` argument +of the appropriate backend in the :setting:`CACHES` setting (explained above). +It's the number of seconds the value should be stored in the cache. Passing in +``None`` for ``timeout`` will cache the value forever. + +.. versionchanged:: 1.6 + + Previously, passing ``None`` explicitly would use the default timeout + value. If the object doesn't exist in the cache, ``cache.get()`` returns ``None``:: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 00c51638b7..4f7ee8b525 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -441,6 +441,34 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key4'), 'lobster bisque') + def test_forever_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', None) + self.assertEqual(self.cache.get('key1'), 'eggs') + + self.cache.add('key2', 'ham', None) + self.assertEqual(self.cache.get('key2'), 'ham') + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, None) + self.assertEqual(self.cache.get('key3'), 'sausage') + self.assertEqual(self.cache.get('key4'), 'lobster bisque') + + def test_zero_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', 0) + self.assertEqual(self.cache.get('key1'), None) + + self.cache.add('key2', 'ham', 0) + self.assertEqual(self.cache.get('key2'), None) + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 0) + self.assertEqual(self.cache.get('key3'), None) + self.assertEqual(self.cache.get('key4'), None) + def test_float_timeout(self): # Make sure a timeout given as a float doesn't crash anything. self.cache.set("key1", "spam", 100.2) From dc43fbc2f21c12e34e309d0e8a121020391aa03a Mon Sep 17 00:00:00 2001 From: Jorge Bastida Date: Sat, 18 May 2013 13:46:31 +0200 Subject: [PATCH 026/249] Fixed #18998 - Prevented session crash when auth backend removed Removing a backend configured in AUTHENTICATION_BACKENDS should not raise an exception for existing sessions, but should make already logged-in users disconnect. Thanks Bradley Ayers for the report. --- AUTHORS | 1 + django/contrib/auth/__init__.py | 4 +- .../contrib/auth/tests/test_auth_backends.py | 53 ++++++++++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 0ad6522725..e83fc035a3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -92,6 +92,7 @@ answer newbie questions, and generally made Django that much better: Randy Barlow Scott Barr Jiri Barton + Jorge Bastida Ned Batchelder batiste@dosimple.ch Batman diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index ef9066657d..17f6b895c7 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -120,12 +120,14 @@ def get_user_model(): def get_user(request): + from django.conf import settings from django.contrib.auth.models import AnonymousUser try: user_id = request.session[SESSION_KEY] backend_path = request.session[BACKEND_SESSION_KEY] + assert backend_path in settings.AUTHENTICATION_BACKENDS backend = load_backend(backend_path) user = backend.get_user(user_id) or AnonymousUser() - except KeyError: + except (KeyError, AssertionError): user = AnonymousUser() return user diff --git a/django/contrib/auth/tests/test_auth_backends.py b/django/contrib/auth/tests/test_auth_backends.py index bb97c54a11..fc5a80e8dd 100644 --- a/django/contrib/auth/tests/test_auth_backends.py +++ b/django/contrib/auth/tests/test_auth_backends.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals from datetime import date from django.conf import settings +from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, Group, Permission, AnonymousUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.auth.tests.test_custom_user import ExtensionUser, CustomPermissionsUser, CustomUser from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user +from django.http import HttpRequest from django.test import TestCase from django.test.utils import override_settings @@ -402,3 +404,52 @@ class PermissionDeniedBackendTest(TestCase): settings.AUTHENTICATION_BACKENDS) + (backend, )) def test_authenticates(self): self.assertEqual(authenticate(username='test', password='test'), self.user1) + + +class NewModelBackend(ModelBackend): + pass + + +@skipIfCustomUser +class ChangedBackendSettingsTest(TestCase): + """ + Tests for changes in the settings.AUTHENTICATION_BACKENDS + """ + backend = 'django.contrib.auth.tests.test_auth_backends.NewModelBackend' + + TEST_USERNAME = 'test_user' + TEST_PASSWORD = 'test_password' + TEST_EMAIL = 'test@example.com' + + def setUp(self): + User.objects.create_user(self.TEST_USERNAME, + self.TEST_EMAIL, + self.TEST_PASSWORD) + + @override_settings(AUTHENTICATION_BACKENDS=(backend, )) + def test_changed_backend_settings(self): + """ + Tests that removing a backend configured in AUTHENTICATION_BACKENDS + make already logged-in users disconnect. + """ + + # Get a session for the test user + self.assertTrue(self.client.login( + username=self.TEST_USERNAME, + password=self.TEST_PASSWORD) + ) + + # Prepare a request object + request = HttpRequest() + request.session = self.client.session + + # Remove NewModelBackend + with self.settings(AUTHENTICATION_BACKENDS=( + 'django.contrib.auth.backends.ModelBackend',)): + # Get the user from the request + user = get_user(request) + + # Assert that the user retrieval is successful and the user is + # anonymous as the backend is not longer available. + self.assertIsNotNone(user) + self.assertTrue(user.is_anonymous()) From 710c59bf9b09ee5afae2e555c30e13ed59fa4583 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 18 May 2013 16:01:47 +0200 Subject: [PATCH 027/249] Slightly reworked imports in contrib.auth.__init__ --- django/contrib/auth/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 17f6b895c7..e032038775 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -1,9 +1,11 @@ import re -from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed +from django.conf import settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.utils.module_loading import import_by_path +from .signals import user_logged_in, user_logged_out, user_login_failed + SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' REDIRECT_FIELD_NAME = 'next' @@ -14,7 +16,6 @@ def load_backend(path): def get_backends(): - from django.conf import settings backends = [] for backend_path in settings.AUTHENTICATION_BACKENDS: backends.append(load_backend(backend_path)) @@ -106,7 +107,6 @@ def logout(request): def get_user_model(): "Return the User model that is active in this project" - from django.conf import settings from django.db.models import get_model try: @@ -120,8 +120,7 @@ def get_user_model(): def get_user(request): - from django.conf import settings - from django.contrib.auth.models import AnonymousUser + from .models import AnonymousUser try: user_id = request.session[SESSION_KEY] backend_path = request.session[BACKEND_SESSION_KEY] From 186ec21a3dd8c87704c579b6620da931e0f68bb3 Mon Sep 17 00:00:00 2001 From: Deni Bertovic Date: Sat, 18 May 2013 16:06:08 +0200 Subject: [PATCH 028/249] Added stripping of whitespace for SlugField and URLField --- django/forms/fields.py | 8 ++++++++ tests/forms_tests/tests/test_extra.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/django/forms/fields.py b/django/forms/fields.py index 4ce57d34a3..3ef0d72463 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -670,6 +670,10 @@ class URLField(CharField): value = urlunsplit(url_fields) return value + def clean(self, value): + value = self.to_python(value).strip() + return super(URLField, self).clean(value) + class BooleanField(Field): widget = CheckboxInput @@ -1105,3 +1109,7 @@ class GenericIPAddressField(CharField): class SlugField(CharField): default_validators = [validators.validate_slug] + + def clean(self, value): + value = self.to_python(value).strip() + return super(SlugField, self).clean(value) diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index a83cdfc05f..ea0f063c30 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -569,6 +569,14 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): f = GenericIPAddressField(unpack_ipv4=True) self.assertEqual(f.clean(' ::ffff:0a0a:0a0a'), '10.10.10.10') + def test_slugfield_normalization(self): + f = SlugField() + self.assertEqual(f.clean(' aa-bb-cc '), 'aa-bb-cc') + + def test_urlfield_normalization(self): + f = URLField() + self.assertEqual(f.clean('http://example.com/ '), 'http://example.com/') + def test_smart_text(self): class Test: if six.PY3: From 63a9555d57069cee32de388821dbe580da1f97c0 Mon Sep 17 00:00:00 2001 From: Olivier Sels Date: Sat, 18 May 2013 13:09:38 +0200 Subject: [PATCH 029/249] Fixed #19436 -- Don't log warnings in ensure_csrf_cookie. --- AUTHORS | 1 + django/middleware/csrf.py | 34 +++++----------------- django/views/decorators/csrf.py | 2 +- tests/csrf_tests/tests.py | 51 +++++++++++++++++++++++++-------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/AUTHORS b/AUTHORS index e83fc035a3..4a9981d1fe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -505,6 +505,7 @@ answer newbie questions, and generally made Django that much better: Bernd Schlapsi schwank@gmail.com scott@staplefish.com + Olivier Sels Ilya Semenov Aleksandra Sendecka serbaut@gmail.com diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 423034478b..98974f011a 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -83,6 +83,13 @@ class CsrfViewMiddleware(object): return None def _reject(self, request, reason): + logger.warning('Forbidden (%s): %s', + reason, request.path, + extra={ + 'status_code': 403, + 'request': request, + } + ) return _get_failure_view()(request, reason=reason) def process_view(self, request, callback, callback_args, callback_kwargs): @@ -134,38 +141,18 @@ class CsrfViewMiddleware(object): # we can use strict Referer checking. referer = request.META.get('HTTP_REFERER') if referer is None: - logger.warning('Forbidden (%s): %s', - REASON_NO_REFERER, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_NO_REFERER) # Note that request.get_host() includes the port. good_referer = 'https://%s/' % request.get_host() if not same_origin(referer, good_referer): reason = REASON_BAD_REFERER % (referer, good_referer) - logger.warning('Forbidden (%s): %s', reason, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, reason) if csrf_token is None: # No CSRF cookie. For POST requests, we insist on a CSRF cookie, # and in this way we can avoid all CSRF attacks, including login # CSRF. - logger.warning('Forbidden (%s): %s', - REASON_NO_CSRF_COOKIE, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_NO_CSRF_COOKIE) # Check non-cookie token for match. @@ -179,13 +166,6 @@ class CsrfViewMiddleware(object): request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') if not constant_time_compare(request_csrf_token, csrf_token): - logger.warning('Forbidden (%s): %s', - REASON_BAD_TOKEN, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_BAD_TOKEN) return self._accept(request) diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index 7a7eb6bba6..a6bd7d8526 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -15,7 +15,7 @@ using the decorator multiple times, is harmless and efficient. class _EnsureCsrfToken(CsrfViewMiddleware): # We need this to behave just like the CsrfViewMiddleware, but not reject - # requests. + # requests or log warnings. def _reject(self, request, reason): return None diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index b9e8cb5f75..841b24bb42 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import logging from django.conf import settings from django.core.context_processors import csrf @@ -78,18 +79,18 @@ class CsrfViewMiddlewareTest(TestCase): def _check_token_present(self, response, csrf_id=None): self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) - def test_process_view_token_too_long(self): - """ - Check that if the token is longer than expected, it is ignored and - a new token is created. - """ - req = self._get_GET_no_csrf_cookie_request() - req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) + def test_process_view_token_too_long(self): + """ + Check that if the token is longer than expected, it is ignored and + a new token is created. + """ + req = self._get_GET_no_csrf_cookie_request() + req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) def test_process_response_get_token_used(self): """ @@ -353,3 +354,29 @@ class CsrfViewMiddlewareTest(TestCase): resp2 = CsrfViewMiddleware().process_response(req, resp) self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)) self.assertTrue('Cookie' in resp2.get('Vary','')) + + def test_ensures_csrf_cookie_no_logging(self): + """ + Tests that ensure_csrf_cookie doesn't log warnings. See #19436. + """ + @ensure_csrf_cookie + def view(request): + # Doesn't insert a token or anything + return HttpResponse(content="") + + class TestHandler(logging.Handler): + def emit(self, record): + raise Exception("This shouldn't have happened!") + + logger = logging.getLogger('django.request') + test_handler = TestHandler() + old_log_level = logger.level + try: + logger.addHandler(test_handler) + logger.setLevel(logging.WARNING) + + req = self._get_GET_no_csrf_cookie_request() + resp = view(req) + finally: + logger.removeHandler(test_handler) + logger.setLevel(old_log_level) From 3634948c883c89a9dbf303b9687331a2a5db43ce Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 16:40:03 +0200 Subject: [PATCH 030/249] Moved IgnorePendingDeprecationWarningsMixin in django.test.utils. This mixin is useful whenever deprecating a large part of Django. --- django/test/utils.py | 21 +++++++++++++++++++++ tests/middleware/tests.py | 4 +--- tests/transactions/tests.py | 14 +------------- tests/transactions_regress/tests.py | 4 +--- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/django/test/utils.py b/django/test/utils.py index 92cef59f72..fb9221d25c 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,4 +1,5 @@ import re +import sys import warnings from functools import wraps from xml.dom.minidom import parseString, Node @@ -380,3 +381,23 @@ class CaptureQueriesContext(object): if exc_type is not None: return self.final_queries = len(self.connection.queries) + + +class IgnoreDeprecationWarningsMixin(object): + + warning_class = DeprecationWarning + + def setUp(self): + super(IgnoreDeprecationWarningsMixin, self).setUp() + self.catch_warnings = warnings.catch_warnings() + self.catch_warnings.__enter__() + warnings.filterwarnings("ignore", category=self.warning_class) + + def tearDown(self): + self.catch_warnings.__exit__(*sys.exc_info()) + super(IgnoreDeprecationWarningsMixin, self).tearDown() + + +class IgnorePendingDeprecationWarningsMixin(IgnoreDeprecationWarningsMixin): + + warning_class = PendingDeprecationWarning diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index e526da4772..1ff8390f31 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -18,14 +18,12 @@ from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.middleware.transaction import TransactionMiddleware from django.test import TransactionTestCase, TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange from django.utils.unittest import expectedFailure, skipIf -from transactions.tests import IgnorePendingDeprecationWarningsMixin - from .models import Band diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index aeb9bc3d2c..0f16a9c805 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -5,6 +5,7 @@ import warnings from django.db import connection, transaction, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature +from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.unittest import skipIf, skipUnless @@ -319,19 +320,6 @@ class AtomicMiscTests(TransactionTestCase): transaction.atomic(Callable()) -class IgnorePendingDeprecationWarningsMixin(object): - - def setUp(self): - super(IgnorePendingDeprecationWarningsMixin, self).setUp() - self.catch_warnings = warnings.catch_warnings() - self.catch_warnings.__enter__() - warnings.filterwarnings("ignore", category=PendingDeprecationWarning) - - def tearDown(self): - self.catch_warnings.__exit__(*sys.exc_info()) - super(IgnorePendingDeprecationWarningsMixin, self).tearDown() - - class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def create_a_reporter_then_fail(self, first, last): diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index fb3f257dab..5339b4a8ea 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -4,11 +4,9 @@ from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, D IntegrityError) from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError from django.test import TransactionTestCase, skipUnlessDBFeature -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils.unittest import skipIf, skipUnless -from transactions.tests import IgnorePendingDeprecationWarningsMixin - from .models import Mod, M2mA, M2mB, SubMod class ModelInheritanceTests(TransactionTestCase): From d5ce2ff5e485bf94fcade340bc803ba4671bd95a Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 16:35:39 +0200 Subject: [PATCH 031/249] Fixed #20444 -- Cookie-based sessions does not include a remote code execution-warning --- docs/topics/http/sessions.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index acad61eb2a..0f2955fadd 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -125,6 +125,17 @@ and the :setting:`SECRET_KEY` setting. .. warning:: + **If the :setting:`SECRET_KEY` is not kept secret, this can lead to + arbitrary remote code execution.** + + An attacker in possession of the :setting:`SECRET_KEY` can not only + generate falsified session data, which your site will trust, but also + remotely execute arbitrary code, as the data is serialized using pickle. + + If you use cookie-based sessions, pay extra care that your secret key is + always kept completely secret, for any system which might be remotely + accessible. + **The session data is signed but not encrypted** When using the cookies backend the session data can be read by the client. From 64e11a68f19793d11915e83574b1bb693da3980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 16:38:11 +0200 Subject: [PATCH 032/249] Fixed #13285: populate_xheaders breaks caching --- django/contrib/flatpages/views.py | 2 - django/core/xheaders.py | 24 ------- docs/releases/1.6.txt | 5 ++ tests/special_headers/__init__.py | 0 tests/special_headers/fixtures/data.xml | 20 ------ tests/special_headers/models.py | 5 -- .../special_headers/article_detail.html | 1 - tests/special_headers/tests.py | 62 ------------------- tests/special_headers/urls.py | 13 ---- tests/special_headers/views.py | 21 ------- 10 files changed, 5 insertions(+), 148 deletions(-) delete mode 100644 django/core/xheaders.py delete mode 100644 tests/special_headers/__init__.py delete mode 100644 tests/special_headers/fixtures/data.xml delete mode 100644 tests/special_headers/models.py delete mode 100644 tests/special_headers/templates/special_headers/article_detail.html delete mode 100644 tests/special_headers/tests.py delete mode 100644 tests/special_headers/urls.py delete mode 100644 tests/special_headers/views.py diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 497979e497..20e930f343 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,7 +1,6 @@ from django.conf import settings from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import get_current_site -from django.core.xheaders import populate_xheaders from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.shortcuts import get_object_or_404 from django.template import loader, RequestContext @@ -70,5 +69,4 @@ def render_flatpage(request, f): 'flatpage': f, }) response = HttpResponse(t.render(c)) - populate_xheaders(request, response, FlatPage, f.id) return response diff --git a/django/core/xheaders.py b/django/core/xheaders.py deleted file mode 100644 index 3766628c98..0000000000 --- a/django/core/xheaders.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Pages in Django can are served up with custom HTTP headers containing useful -information about those pages -- namely, the content type and object ID. - -This module contains utility functions for retrieving and doing interesting -things with these special "X-Headers" (so called because the HTTP spec demands -that custom headers are prefixed with "X-"). - -Next time you're at slashdot.org, watch out for X-Fry and X-Bender. :) -""" - -def populate_xheaders(request, response, model, object_id): - """ - Adds the "X-Object-Type" and "X-Object-Id" headers to the given - HttpResponse according to the given model and object_id -- but only if the - given HttpRequest object has an IP address within the INTERNAL_IPS setting - or if the request is from a logged in staff member. - """ - from django.conf import settings - if (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - or (hasattr(request, 'user') and request.user.is_active - and request.user.is_staff)): - response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name) - response['X-Object-Id'] = str(object_id) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..452baf7f76 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -491,6 +491,11 @@ Miscellaneous memcache backend no longer uses the default timeout, and now will set-and-expire-immediately the value. +* The ``django.contrib.flatpages`` app used to set custom HTTP headers for + debugging purposes. This functionality was not documented and made caching + ineffective so it has been removed, along with its generic implementation, + previously available in ``django.core.xheaders``. + Features deprecated in 1.6 ========================== diff --git a/tests/special_headers/__init__.py b/tests/special_headers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/special_headers/fixtures/data.xml b/tests/special_headers/fixtures/data.xml deleted file mode 100644 index 7e60d45199..0000000000 --- a/tests/special_headers/fixtures/data.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - super - Super - User - super@example.com - sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 - True - True - True - 2007-05-30 13:20:10 - 2007-05-30 13:20:10 - - - - - text - - diff --git a/tests/special_headers/models.py b/tests/special_headers/models.py deleted file mode 100644 index e05c5a6920..0000000000 --- a/tests/special_headers/models.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.db import models - - -class Article(models.Model): - text = models.TextField() diff --git a/tests/special_headers/templates/special_headers/article_detail.html b/tests/special_headers/templates/special_headers/article_detail.html deleted file mode 100644 index 3cbd38cb7e..0000000000 --- a/tests/special_headers/templates/special_headers/article_detail.html +++ /dev/null @@ -1 +0,0 @@ -{{ object }} diff --git a/tests/special_headers/tests.py b/tests/special_headers/tests.py deleted file mode 100644 index b4b704ae21..0000000000 --- a/tests/special_headers/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.contrib.auth.models import User -from django.test import TestCase -from django.test.utils import override_settings - - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SpecialHeadersTest(TestCase): - fixtures = ['data.xml'] - urls = 'special_headers.urls' - - def test_xheaders(self): - user = User.objects.get(username='super') - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - self.client.login(username='super', password='secret') - response = self.client.get('/special_headers/article/1/') - self.assertTrue('X-Object-Type' in response) - user.is_staff = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - - def test_xview_func(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/func/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.xview') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - - def test_xview_class(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/class/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.XViewClass') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) diff --git a/tests/special_headers/urls.py b/tests/special_headers/urls.py deleted file mode 100644 index f7ba141207..0000000000 --- a/tests/special_headers/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -# coding: utf-8 -from __future__ import absolute_import - -from django.conf.urls import patterns - -from . import views -from .models import Article - -urlpatterns = patterns('', - (r'^special_headers/article/(?P\d+)/$', views.xview_xheaders), - (r'^special_headers/xview/func/$', views.xview_dec(views.xview)), - (r'^special_headers/xview/class/$', views.xview_dec(views.XViewClass.as_view())), -) diff --git a/tests/special_headers/views.py b/tests/special_headers/views.py deleted file mode 100644 index a8bbd6542e..0000000000 --- a/tests/special_headers/views.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.core.xheaders import populate_xheaders -from django.http import HttpResponse -from django.utils.decorators import decorator_from_middleware -from django.views.generic import View -from django.middleware.doc import XViewMiddleware - -from .models import Article - -xview_dec = decorator_from_middleware(XViewMiddleware) - -def xview(request): - return HttpResponse() - -def xview_xheaders(request, object_id): - response = HttpResponse() - populate_xheaders(request, response, Article, 1) - return response - -class XViewClass(View): - def get(self, request): - return HttpResponse() From 5883ae56b3e993052c46f7d9e756465b1387dd34 Mon Sep 17 00:00:00 2001 From: Filipa Andrade Date: Sat, 18 May 2013 17:04:52 +0200 Subject: [PATCH 033/249] Fixed #20142 -- Added error handling for fixture setup TestCase._fixture_setup disables transactions so, in case of an error, cleanup is needed to re-enable transactions, otherwise following TransactionTestCase would fail. --- django/test/testcases.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index 6fe6b9c397..6f8cbabd86 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -843,13 +843,17 @@ class TestCase(TransactionTestCase): for db in self._databases_names(include_mirrors=False): if hasattr(self, 'fixtures'): - call_command('loaddata', *self.fixtures, - **{ - 'verbosity': 0, - 'commit': False, - 'database': db, - 'skip_validation': True, - }) + try: + call_command('loaddata', *self.fixtures, + **{ + 'verbosity': 0, + 'commit': False, + 'database': db, + 'skip_validation': True, + }) + except Exception: + self._fixture_teardown() + raise def _fixture_teardown(self): if not connections_support_transactions(): From a19e9d80ffa10f8da43addcaa4ddd440beee8a4d Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 17 May 2013 15:31:41 -0400 Subject: [PATCH 034/249] Fixed #20430 - Enable iterable of iterables for model choices Allows for any iterable, not just lists or tuples, to be used as the inner item for a list of choices in a model. --- django/core/management/validation.py | 4 +-- docs/ref/models/fields.txt | 7 ++--- docs/releases/1.6.txt | 3 +++ tests/invalid_models/invalid_models/models.py | 4 +-- tests/model_validation/__init__.py | 0 tests/model_validation/models.py | 27 +++++++++++++++++++ tests/model_validation/tests.py | 14 ++++++++++ 7 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 tests/model_validation/__init__.py create mode 100644 tests/model_validation/models.py create mode 100644 tests/model_validation/tests.py diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 0f0eade569..2040a14582 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -118,8 +118,8 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) else: for c in f.choices: - if not isinstance(c, (list, tuple)) or len(c) != 2: - e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name) + if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2: + e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name) if f.db_index not in (None, True, False): e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..38b6459909 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -80,9 +80,10 @@ If a field has ``blank=False``, the field will be required. .. attribute:: Field.choices -An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this -field. If this is given, the default form widget will be a select box with -these choices instead of the standard text field. +An iterable (e.g., a list or tuple) consisting itself of iterables of exactly +two items (e.g. ``[(A, B), (A, B) ...]``) to use as choices for this field. If +this is given, the default form widget will be a select box with these choices +instead of the standard text field. The first element in each tuple is the actual value to be stored, and the second element is the human-readable name. For example:: diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..b10a9ef81e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -238,6 +238,9 @@ Minor features Meta option: ``localized_fields``. Fields included in this list will be localized (by setting ``localize`` on the form field). +* The ``choices`` argument to model fields now accepts an iterable of iterables + instead of requiring an iterable of lists or tuples. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 3c21e1ddb8..6ba3f04e5e 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -375,8 +375,8 @@ invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). invalid_models.fielderrors: "index": "db_index" should be either None, True or False. invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. diff --git a/tests/model_validation/__init__.py b/tests/model_validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_validation/models.py b/tests/model_validation/models.py new file mode 100644 index 0000000000..9a2a5f7cd0 --- /dev/null +++ b/tests/model_validation/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class ThingItem(object): + + def __init__(self, value, display): + self.value = value + self.display = display + + def __iter__(self): + return (x for x in [self.value, self.display]) + + def __len__(self): + return 2 + + +class Things(object): + + def __iter__(self): + return (x for x in [ThingItem(1, 2), ThingItem(3, 4)]) + + +class ThingWithIterableChoices(models.Model): + + # Testing choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + thing = models.CharField(max_length=100, blank=True, choices=Things()) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py new file mode 100644 index 0000000000..96baf640eb --- /dev/null +++ b/tests/model_validation/tests.py @@ -0,0 +1,14 @@ +import io + +from django.core import management +from django.test import TestCase + + +class ModelValidationTest(TestCase): + + def test_models_validate(self): + # All our models should validate properly + # Validation Tests: + # * choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + management.call_command("validate", stdout=io.BytesIO()) From 56e2f6ccae76a6f0a7f1d64677bf29e11518e5c6 Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 17:16:07 +0200 Subject: [PATCH 035/249] Fixed #20446 -- Documentation for SmallIntegerField does not clarify 'small' --- docs/ref/models/fields.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..da5fe9bf60 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -889,7 +889,8 @@ The value ``0`` is accepted for backward compatibility reasons. .. class:: PositiveSmallIntegerField([**options]) Like a :class:`PositiveIntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values up to 32767 are safe in all databases +supported by Django. ``SlugField`` ------------- @@ -917,7 +918,8 @@ of some other value. You can do this automatically in the admin using .. class:: SmallIntegerField([**options]) Like an :class:`IntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values from -32768 to 32767 are safe in all databases +supported by Django. ``TextField`` ------------- From 96cabba80805ddda9c48307cb3d75177dc94ba29 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 18 May 2013 11:20:02 -0400 Subject: [PATCH 036/249] Fixed #20335 - Documented the {% language %} template tag. Thanks bmispelon for the suggestion and djangsters for the patch. --- docs/topics/i18n/translation.txt | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 5b4ffea528..72e000a86f 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -722,6 +722,31 @@ or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs +

{% trans "Welcome to our page" %}

+ + {% language 'en' %} + {% get_current_language as LANGUAGE_CODE %} + +

{% trans "Welcome to our page" %}

+ {% endlanguage %} + +While the first occurrence of "Welcome to our page" uses the current language, +the second will always be in English. + .. _template-translation-vars: Other tags @@ -1126,13 +1151,11 @@ active language. Example:: .. _reversing_in_templates: -.. templatetag:: language - Reversing in templates ---------------------- If localized URLs get reversed in templates they always use the current -language. To link to a URL in another language use the ``language`` +language. To link to a URL in another language use the :ttag:`language` template tag. It enables the given language in the enclosed template section: .. code-block:: html+django From 86e761fee88a8bf0499f6795c9a9b336d7c52873 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Sat, 18 May 2013 17:27:12 +0200 Subject: [PATCH 037/249] Fix NoneType error when fetching a stale ContentType with get_for_id When a stale ContentType is fetched, the _add_to_cache() function didn't detect that `model_class()` returns `None` (which it does by design). However, the `app_label` + `model` fields can be used instead to as local cache key. Third party apps can detect stale models by checking whether `model_class()` returns `None`. Ticket: https://code.djangoproject.com/ticket/20442 --- django/contrib/contenttypes/models.py | 6 ++++-- django/contrib/contenttypes/tests.py | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index f0bd109b00..ce220c8487 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -118,11 +118,13 @@ class ContentTypeManager(models.Manager): def _add_to_cache(self, using, ct): """Insert a ContentType into the cache.""" - model = ct.model_class() - key = (model._meta.app_label, model._meta.model_name) + # Note it's possible for ContentType objects to be stale; model_class() will return None. + # Hence, there is no reliance on model._meta.app_label here, just using the model fields instead. + key = (ct.app_label, ct.model) self.__class__._cache.setdefault(using, {})[key] = ct self.__class__._cache.setdefault(using, {})[ct.id] = ct + @python_2_unicode_compatible class ContentType(models.Model): name = models.CharField(max_length=100) diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 7937873a00..f300294cd6 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -274,3 +274,10 @@ class ContentTypesTests(TestCase): model = 'OldModel', ) self.assertEqual(six.text_type(ct), 'Old model') + self.assertIsNone(ct.model_class()) + + # Make sure stale ContentTypes can be fetched like any other object. + # Before Django 1.6 this caused a NoneType error in the caching mechanism. + # Instead, just return the ContentType object and let the app detect stale states. + ct_fetched = ContentType.objects.get_for_id(ct.pk) + self.assertIsNone(ct_fetched.model_class()) From acd0bb39df5c9ca486e49ec55ae34538242ce071 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sat, 18 May 2013 17:36:31 +0200 Subject: [PATCH 038/249] Fixed #14894 -- Ensure that activating a translation doesn't run into threading issues. Thanks to maxbublis for the report and sergeykolosov for the patch. --- django/utils/translation/trans_real.py | 2 +- tests/i18n/tests.py | 30 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index f7b1524085..26be0ed729 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -140,7 +140,7 @@ def translation(language): # doesn't affect en-gb), even though they will both use the core "en" # translation. So we have to subvert Python's internal gettext caching. base_lang = lambda x: x.split('-', 1)[0] - if base_lang(lang) in [base_lang(trans) for trans in _translations]: + if base_lang(lang) in [base_lang(trans) for trans in list(_translations)]: res._info = res._info.copy() res._catalog = res._catalog.copy() diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 29223e5add..1b15720c16 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -334,6 +334,36 @@ class TranslationTests(TestCase): self.assertEqual(rendered, 'My other name is James.') +class TranslationThreadSafetyTests(TestCase): + def setUp(self): + self._old_language = get_language() + self._translations = trans_real._translations + + # here we rely on .split() being called inside the _fetch() + # in trans_real.translation() + class sideeffect_str(str): + def split(self, *args, **kwargs): + res = str.split(self, *args, **kwargs) + trans_real._translations['en-YY'] = None + return res + + trans_real._translations = {sideeffect_str('en-XX'): None} + + def tearDown(self): + trans_real._translations = self._translations + activate(self._old_language) + + def test_bug14894_translation_activate_thread_safety(self): + translation_count = len(trans_real._translations) + try: + translation.activate('pl') + except RuntimeError: + self.fail('translation.activate() is not thread-safe') + + # make sure sideeffect_str actually added a new translation + self.assertLess(translation_count, len(trans_real._translations)) + + @override_settings(USE_L10N=True) class FormattingTests(TestCase): From bd97f7d0cb72191744552142817184e88ce8841d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 16:10:14 +0200 Subject: [PATCH 039/249] Fixed #15201: Marked CACHE_MIDDLEWARE_ANONYMOUS_ONLY as deprecated --- django/middleware/cache.py | 11 ++++++----- docs/faq/admin.txt | 6 ------ docs/internals/deprecation.txt | 2 ++ docs/ref/settings.txt | 8 ++++++-- docs/releases/1.6.txt | 17 +++++++++++++++++ docs/topics/cache.txt | 12 +++--------- tests/cache/tests.py | 8 +++++--- 7 files changed, 39 insertions(+), 25 deletions(-) diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 83860e15f3..e13a8c3918 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -29,11 +29,6 @@ More details about how the caching works: of the response's "Cache-Control" header, falling back to the CACHE_MIDDLEWARE_SECONDS setting if the section was not found. -* If CACHE_MIDDLEWARE_ANONYMOUS_ONLY is set to True, only anonymous requests - (i.e., those not made by a logged-in user) will be cached. This is a simple - and effective way of avoiding the caching of the Django admin (and any other - user-specific content). - * This middleware expects that a HEAD request is answered with the same response headers exactly like the corresponding GET request. @@ -48,6 +43,8 @@ More details about how the caching works: """ +import warnings + from django.conf import settings from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age @@ -200,5 +197,9 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): else: self.cache_anonymous_only = cache_anonymous_only + if self.cache_anonymous_only: + msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8." + warnings.warn(msg, PendingDeprecationWarning, stacklevel=1) + self.cache = get_cache(self.cache_alias, **cache_kwargs) self.cache_timeout = self.cache.default_timeout diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 1d9a7c7427..ec40754094 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -27,12 +27,6 @@ account has :attr:`~django.contrib.auth.models.User.is_active` and :attr:`~django.contrib.auth.models.User.is_staff` set to True. The admin site only allows access to users with those two fields both set to True. -How can I prevent the cache middleware from caching the admin site? -------------------------------------------------------------------- - -Set the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` setting to ``True``. See the -:doc:`cache documentation ` for more information. - How do I automatically set a field's value to the user who last edited the object in the admin? ----------------------------------------------------------------------------------------------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 774de2a2fd..095b6d0a33 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -390,6 +390,8 @@ these changes. ``django.test.testcases.OutputChecker`` will be removed. Instead use the doctest module from the Python standard library. +* The ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting will be removed. + 2.0 --- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index eb470cdd14..8ef59064f7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -280,6 +280,12 @@ CACHE_MIDDLEWARE_ANONYMOUS_ONLY Default: ``False`` +.. deprecated:: 1.6 + + This setting was largely ineffective because of using cookies for sessions + and CSRF. See the :doc:`Django 1.6 release notes` for more + information. + If the value of this setting is ``True``, only anonymous requests (i.e., not those made by a logged-in user) will be cached. Otherwise, the middleware caches every page that doesn't have GET or POST parameters. @@ -287,8 +293,6 @@ caches every page that doesn't have GET or POST parameters. If you set the value of this setting to ``True``, you should make sure you've activated ``AuthenticationMiddleware``. -See :doc:`/topics/cache`. - .. setting:: CACHE_MIDDLEWARE_KEY_PREFIX CACHE_MIDDLEWARE_KEY_PREFIX diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index ef79e5770c..f8e1fd6339 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -569,6 +569,23 @@ If necessary, you can temporarily disable auto-escaping with :func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %} `. +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CacheMiddleware`` used to provide a way to cache requests only if they +weren't made by a logged-in user. This mechanism was largely ineffective +because the middleware correctly takes into account the ``Vary: Cookie`` HTTP +header, and this header is being set on a variety of occasions, such as: + +* accessing the session, or +* using CSRF protection, which is turned on by default, or +* using a client-side library which sets cookies, like `Google Analytics`__. + +This makes the cache effectively work on a per-session basis regardless of the +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting. + +__ http://www.google.com/analytics/ + ``SEND_BROKEN_LINK_EMAILS`` setting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index a7d54fbeb0..46911a593f 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -443,15 +443,9 @@ Then, add the following required settings to your Django settings file: The cache middleware caches GET and HEAD responses with status 200, where the request and response headers allow. Responses to requests for the same URL with different query parameters are considered to be unique pages and are cached separately. -Optionally, if the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` -setting is ``True``, only anonymous requests (i.e., not those made by a -logged-in user) will be cached. This is a simple and effective way of disabling -caching for any user-specific pages (including Django's admin interface). Note -that if you use :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY`, you should make -sure you've activated ``AuthenticationMiddleware``. The cache middleware -expects that a HEAD request is answered with the same response headers as -the corresponding GET request; in which case it can return a cached GET -response for HEAD request. +The cache middleware expects that a HEAD request is answered with the same +response headers as the corresponding GET request; in which case it can return +a cached GET response for HEAD request. Additionally, the cache middleware automatically sets a few headers in each :class:`~django.http.HttpResponse`: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 4f7ee8b525..231a3bfb50 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -28,8 +28,8 @@ from django.middleware.cache import (FetchFromCacheMiddleware, from django.template import Template from django.template.response import TemplateResponse from django.test import TestCase, TransactionTestCase, RequestFactory -from django.test.utils import override_settings, six -from django.utils import timezone, translation, unittest +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin +from django.utils import six, timezone, translation, unittest from django.utils.cache import (patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control, patch_response_headers) from django.utils.encoding import force_text @@ -1592,9 +1592,10 @@ def hello_world_view(request, value): }, }, ) -class CacheMiddlewareTest(TestCase): +class CacheMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TestCase): def setUp(self): + super(CacheMiddlewareTest, self).setUp() self.factory = RequestFactory() self.default_cache = get_cache('default') self.other_cache = get_cache('other') @@ -1602,6 +1603,7 @@ class CacheMiddlewareTest(TestCase): def tearDown(self): self.default_cache.clear() self.other_cache.clear() + super(CacheMiddlewareTest, self).tearDown() def test_constructor(self): """ From f763227c7d2d91de6086e587aa3b52473001b2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 18:06:31 +0200 Subject: [PATCH 040/249] Fixes a Python 3.x regression introduced in a19e9d80 --- tests/model_validation/tests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py index 96baf640eb..ffd0d89412 100644 --- a/tests/model_validation/tests.py +++ b/tests/model_validation/tests.py @@ -1,7 +1,6 @@ -import io - from django.core import management from django.test import TestCase +from django.utils import six class ModelValidationTest(TestCase): @@ -11,4 +10,4 @@ class ModelValidationTest(TestCase): # Validation Tests: # * choices= Iterable of Iterables # See: https://code.djangoproject.com/ticket/20430 - management.call_command("validate", stdout=io.BytesIO()) + management.call_command("validate", stdout=six.StringIO()) From 2c84f4434cd0f6e74471d5737b049c648cff538f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 18 May 2013 12:05:35 -0400 Subject: [PATCH 041/249] Fixed #20284 - Added a note about DEBUG in static file docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks wim@ for the suggestion and Nicolas Noé for the patch. --- docs/howto/static-files/index.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/howto/static-files/index.txt b/docs/howto/static-files/index.txt index 1fdad94143..3668c5dc41 100644 --- a/docs/howto/static-files/index.txt +++ b/docs/howto/static-files/index.txt @@ -35,8 +35,20 @@ Configuring static files 4. Store your static files in a folder called ``static`` in your app. For example ``my_app/static/my_app/myimage.jpg``. -Now, if you use ``./manage.py runserver``, all static files should be served -automatically at the :setting:`STATIC_URL` and be shown correctly. +.. admonition:: Serving the files + + In addition to these configuration steps, you'll also need to actually + serve the static files. + + During development, this will be done automatically if you use + :djadmin:`runserver` and :setting:`DEBUG` is set to ``True`` (see + :func:`django.contrib.staticfiles.views.serve`). + + This method is **grossly inefficient** and probably **insecure**, + so it is **unsuitable for production**. + + See :doc:`/howto/static-files/deployment` for proper strategies to serve + static files in production environments. Your project will probably also have static assets that aren't tied to a particular app. In addition to using a ``static/`` directory inside your apps, From ff881aef536640d712fd2bc53fa1ff98e300adad Mon Sep 17 00:00:00 2001 From: Shai Berger Date: Sat, 18 May 2013 19:26:34 +0300 Subject: [PATCH 042/249] Fixed #13958 -- problem reporting exception from \r-line-ended file Thanks petrvanblokland for reporting and saz for the patch --- django/views/debug.py | 8 ++++---- tests/view_tests/tests/test_debug.py | 21 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/django/views/debug.py b/django/views/debug.py index 9b95b524d2..10c07e8f78 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -347,7 +347,7 @@ class ExceptionReporter(object): if source is None: try: with open(filename, 'rb') as fp: - source = fp.readlines() + source = fp.read().splitlines() except (OSError, IOError): pass if source is None: @@ -370,9 +370,9 @@ class ExceptionReporter(object): lower_bound = max(0, lineno - context_lines) upper_bound = lineno + context_lines - pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] - context_line = source[lineno].strip('\n') - post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]] + pre_context = source[lower_bound:lineno] + context_line = source[lineno] + post_context = source[lineno+1:upper_bound] return lower_bound, pre_context, context_line, post_context diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index b44cd88abe..1022d05998 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -6,6 +6,7 @@ from __future__ import absolute_import, unicode_literals import inspect import os import sys +import tempfile from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile @@ -13,7 +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.utils.encoding import force_text, force_bytes from django.views.debug import ExceptionReporter from .. import BrokenException, except_args @@ -122,6 +123,24 @@ class ExceptionReporterTests(TestCase): self.assertIn('

Request information

', html) self.assertIn('

Request data not supplied

', html) + def test_eol_support(self): + """Test that the ExceptionReporter supports Unix, Windows and Macintosh EOL markers""" + LINES = list(u'print %d' % i for i in range(1,6)) + reporter = ExceptionReporter(None, None, None, None) + + for newline in ['\n','\r\n','\r']: + fd,filename = tempfile.mkstemp(text = False) + os.write(fd, force_bytes(newline.join(LINES)+newline)) + os.close(fd) + + try: + self.assertEqual( + reporter._get_lines_from_file(filename, 3, 2), + (1, LINES[1:3], LINES[3], LINES[4:]) + ) + finally: + os.unlink(filename) + def test_no_exception(self): "An exception report can be generated for just a request" request = self.rf.get('/test_view/') From e4591debd19361e628317e936ed8123d9897dd6a Mon Sep 17 00:00:00 2001 From: Marc Egli Date: Sat, 18 May 2013 12:12:26 +0200 Subject: [PATCH 043/249] Add missing imports and models to the examples in the the model layer documentation --- docs/ref/models/fields.txt | 10 +++++++ docs/ref/models/instances.txt | 10 +++++++ docs/ref/models/options.txt | 6 ++++ docs/ref/models/querysets.txt | 7 +++++ docs/ref/models/relations.txt | 8 ++++-- docs/topics/db/aggregation.txt | 40 ++++++++++++++++----------- docs/topics/db/managers.txt | 22 +++++++++------ docs/topics/db/models.txt | 50 +++++++++++++++++++++++++++++----- docs/topics/db/queries.txt | 2 ++ 9 files changed, 122 insertions(+), 33 deletions(-) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..0ce97c3eaa 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -97,6 +97,8 @@ second element is the human-readable name. For example:: Generally, it's best to define choices inside a model class, and to define a suitably-named constant for each value:: + from django.db import models + class Student(models.Model): FRESHMAN = 'FR' SOPHOMORE = 'SO' @@ -994,12 +996,15 @@ relationship with itself -- use ``models.ForeignKey('self')``. If you need to create a relationship on a model that has not yet been defined, you can use the name of the model, rather than the model object itself:: + from django.db import models + class Car(models.Model): manufacturer = models.ForeignKey('Manufacturer') # ... class Manufacturer(models.Model): # ... + pass To refer to models defined in another application, you can explicitly specify a model with the full application label. For example, if the ``Manufacturer`` @@ -1132,6 +1137,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in necessary to avoid executing queries at the time your models.py is imported:: + from django.db import models + from django.contrib.auth.models import User + def get_sentinel_user(): return User.objects.get_or_create(username='deleted')[0] @@ -1204,6 +1212,8 @@ that control how the relationship functions. Only used in the definition of ManyToManyFields on self. Consider the following model:: + from django.db import models + class Person(models.Model): friends = models.ManyToManyField("self") diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index b4b162a9ea..f989ff1bec 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -34,6 +34,8 @@ that, you need to :meth:`~Model.save()`. 1. Add a classmethod on the model class:: + from django.db import models + class Book(models.Model): title = models.CharField(max_length=100) @@ -105,6 +107,7 @@ individually. You'll need to call ``full_clean`` manually when you want to run one-step model validation for your own manually created models. For example:: + from django.core.exceptions import ValidationError try: article.full_clean() except ValidationError as e: @@ -132,6 +135,7 @@ automatically provide a value for a field, or to do validation that requires access to more than a single field:: def clean(self): + import datetime from django.core.exceptions import ValidationError # Don't allow draft entries to have a pub_date. if self.status == 'draft' and self.pub_date is not None: @@ -434,6 +438,8 @@ representation of the model from the ``__unicode__()`` method. For example:: + from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -460,6 +466,9 @@ Thus, you should return a nice, human-readable string for the object's The previous :meth:`~Model.__unicode__()` example could be similarly written using ``__str__()`` like this:: + from django.db import models + from django.utils.encoding import force_bytes + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -490,6 +499,7 @@ function is usually the best approach.) For example:: def get_absolute_url(self): + from django.core.urlresolvers import reverse return reverse('people.views.details', args=[str(self.id)]) One place Django uses ``get_absolute_url()`` is in the admin app. If an object diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 5f9316bd2a..90099d13a3 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -145,6 +145,12 @@ Django quotes column and table names behind the scenes. and a question has more than one answer, and the order of answers matters, you'd do this:: + from django.db import models + + class Question(models.Model): + text = models.TextField() + # ... + class Answer(models.Model): question = models.ForeignKey(Question) # ... diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 14123cd79a..9677b321c6 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -232,6 +232,7 @@ the model field that is being aggregated. For example, if you were manipulating a list of blogs, you may want to determine how many entries have been made in each blog:: + >>> from django.db.models import Count >>> q = Blog.objects.annotate(Count('entry')) # The name of the first blog >>> q[0].name @@ -699,6 +700,8 @@ And here's ``select_related`` lookup:: ``select_related()`` follows foreign keys as far as possible. If you have the following models:: + from django.db import models + class City(models.Model): # ... pass @@ -814,6 +817,8 @@ that are supported by ``select_related``. It also supports prefetching of For example, suppose you have these models:: + from django.db import models + class Topping(models.Model): name = models.CharField(max_length=30) @@ -1565,6 +1570,7 @@ aggregated. For example, when you are working with blog entries, you may want to know the number of authors that have contributed blog entries:: + >>> from django.db.models import Count >>> q = Blog.objects.aggregate(Count('entry')) {'entry__count': 16} @@ -2042,6 +2048,7 @@ Range test (inclusive). Example:: + import datetime start_date = datetime.date(2005, 1, 1) end_date = datetime.date(2005, 3, 31) Entry.objects.filter(pub_date__range=(start_date, end_date)) diff --git a/docs/ref/models/relations.txt b/docs/ref/models/relations.txt index c923961a19..ffebe37193 100644 --- a/docs/ref/models/relations.txt +++ b/docs/ref/models/relations.txt @@ -12,8 +12,11 @@ Related objects reference * The "other side" of a :class:`~django.db.models.ForeignKey` relation. That is:: + from django.db import models + class Reporter(models.Model): - ... + # ... + pass class Article(models.Model): reporter = models.ForeignKey(Reporter) @@ -24,7 +27,8 @@ Related objects reference * Both sides of a :class:`~django.db.models.ManyToManyField` relation:: class Topping(models.Model): - ... + # ... + pass class Pizza(models.Model): toppings = models.ManyToManyField(Topping) diff --git a/docs/topics/db/aggregation.txt b/docs/topics/db/aggregation.txt index 125cd0bdee..1024d6b0c2 100644 --- a/docs/topics/db/aggregation.txt +++ b/docs/topics/db/aggregation.txt @@ -18,27 +18,29 @@ used to track the inventory for a series of online bookstores: .. code-block:: python + from django.db import models + class Author(models.Model): - name = models.CharField(max_length=100) - age = models.IntegerField() + name = models.CharField(max_length=100) + age = models.IntegerField() class Publisher(models.Model): - name = models.CharField(max_length=300) - num_awards = models.IntegerField() + name = models.CharField(max_length=300) + num_awards = models.IntegerField() class Book(models.Model): - name = models.CharField(max_length=300) - pages = models.IntegerField() - price = models.DecimalField(max_digits=10, decimal_places=2) - rating = models.FloatField() - authors = models.ManyToManyField(Author) - publisher = models.ForeignKey(Publisher) - pubdate = models.DateField() + name = models.CharField(max_length=300) + pages = models.IntegerField() + price = models.DecimalField(max_digits=10, decimal_places=2) + rating = models.FloatField() + authors = models.ManyToManyField(Author) + publisher = models.ForeignKey(Publisher) + pubdate = models.DateField() class Store(models.Model): - name = models.CharField(max_length=300) - books = models.ManyToManyField(Book) - registered_users = models.PositiveIntegerField() + name = models.CharField(max_length=300) + books = models.ManyToManyField(Book) + registered_users = models.PositiveIntegerField() Cheat sheet =========== @@ -123,7 +125,7 @@ If you want to generate more than one aggregate, you just add another argument to the ``aggregate()`` clause. So, if we also wanted to know the maximum and minimum price of all books, we would issue the query:: - >>> from django.db.models import Avg, Max, Min, Count + >>> from django.db.models import Avg, Max, Min >>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price')) {'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')} @@ -148,6 +150,7 @@ the number of authors: .. code-block:: python # Build an annotated queryset + >>> from django.db.models import Count >>> q = Book.objects.annotate(Count('authors')) # Interrogate the first object in the queryset >>> q[0] @@ -192,6 +195,7 @@ and aggregate the related value. For example, to find the price range of books offered in each store, you could use the annotation:: + >>> from django.db.models import Max, Min >>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price')) This tells Django to retrieve the ``Store`` model, join (through the @@ -222,7 +226,7 @@ For example, we can ask for all publishers, annotated with their respective total book stock counters (note how we use ``'book'`` to specify the ``Publisher`` -> ``Book`` reverse foreign key hop):: - >>> from django.db.models import Count, Min, Sum, Max, Avg + >>> from django.db.models import Count, Min, Sum, Avg >>> Publisher.objects.annotate(Count('book')) (Every ``Publisher`` in the resulting ``QuerySet`` will have an extra attribute @@ -269,6 +273,7 @@ constraining the objects for which an annotation is calculated. For example, you can generate an annotated list of all books that have a title starting with "Django" using the query:: + >>> from django.db.models import Count, Avg >>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors')) When used with an ``aggregate()`` clause, a filter has the effect of @@ -407,6 +412,8 @@ particularly, when counting things. By way of example, suppose you have a model like this:: + from django.db import models + class Item(models.Model): name = models.CharField(max_length=10) data = models.IntegerField() @@ -457,5 +464,6 @@ For example, if you wanted to calculate the average number of authors per book you first annotate the set of books with the author count, then aggregate that author count, referencing the annotation field:: + >>> from django.db.models import Count, Avg >>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors')) {'num_authors__avg': 1.66} diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index 2a0f7e4ce0..b940b09d33 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -62,6 +62,8 @@ For example, this custom ``Manager`` offers a method ``with_counts()``, which returns a list of all ``OpinionPoll`` objects, each with an extra ``num_responses`` attribute that is the result of an aggregate query:: + from django.db import models + class PollManager(models.Manager): def with_counts(self): from django.db import connection @@ -101,6 +103,8 @@ Modifying initial Manager QuerySets A ``Manager``'s base ``QuerySet`` returns all objects in the system. For example, using this model:: + from django.db import models + class Book(models.Model): title = models.CharField(max_length=100) author = models.CharField(max_length=50) @@ -236,7 +240,7 @@ class, but still customize the default manager. For example, suppose you have this base class:: class AbstractBase(models.Model): - ... + # ... objects = CustomManager() class Meta: @@ -246,14 +250,15 @@ If you use this directly in a subclass, ``objects`` will be the default manager if you declare no managers in the base class:: class ChildA(AbstractBase): - ... + # ... # This class has CustomManager as the default manager. + pass If you want to inherit from ``AbstractBase``, but provide a different default manager, you can provide the default manager on the child class:: class ChildB(AbstractBase): - ... + # ... # An explicit default manager. default_manager = OtherManager() @@ -274,9 +279,10 @@ it into the inheritance hierarchy *after* the defaults:: abstract = True class ChildC(AbstractBase, ExtraManager): - ... + # ... # Default manager is CustomManager, but OtherManager is # also available via the "extra_manager" attribute. + pass Note that while you can *define* a custom manager on the abstract model, you can't *invoke* any methods using the abstract model. That is:: @@ -349,8 +355,7 @@ the manager class:: class MyManager(models.Manager): use_for_related_fields = True - - ... + # ... If this attribute is set on the *default* manager for a model (only the default manager is considered in these situations), Django will use that class @@ -396,7 +401,8 @@ it, whereas the following will not work:: # BAD: Incorrect code class MyManager(models.Manager): - ... + # ... + pass # Sets the attribute on an instance of MyManager. Django will # ignore this setting. @@ -404,7 +410,7 @@ it, whereas the following will not work:: mgr.use_for_related_fields = True class MyModel(models.Model): - ... + # ... objects = mgr # End of incorrect code. diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index dd7714052d..baef01b6fb 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -90,6 +90,8 @@ attributes. Be careful not to choose field names that conflict with the Example:: + from django.db import models + class Musician(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -290,8 +292,11 @@ For example, if a ``Car`` model has a ``Manufacturer`` -- that is, a ``Manufacturer`` makes multiple cars but each ``Car`` only has one ``Manufacturer`` -- use the following definitions:: + from django.db import models + class Manufacturer(models.Model): # ... + pass class Car(models.Model): manufacturer = models.ForeignKey(Manufacturer) @@ -340,8 +345,11 @@ For example, if a ``Pizza`` has multiple ``Topping`` objects -- that is, a ``Topping`` can be on multiple pizzas and each ``Pizza`` has multiple toppings -- here's how you'd represent that:: + from django.db import models + class Topping(models.Model): # ... + pass class Pizza(models.Model): # ... @@ -403,6 +411,8 @@ intermediate model. The intermediate model is associated with the that will act as an intermediary. For our musician example, the code would look something like this:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -583,6 +593,7 @@ It's perfectly OK to relate a model to one from another app. To do this, import the related model at the top of the file where your model is defined. Then, just refer to the other model class wherever needed. For example:: + from django.db import models from geography.models import ZipCode class Restaurant(models.Model): @@ -630,6 +641,8 @@ Meta options Give your model metadata by using an inner ``class Meta``, like so:: + from django.db import models + class Ox(models.Model): horn_length = models.IntegerField() @@ -660,6 +673,8 @@ model. For example, this model has a few custom methods:: + from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -729,6 +744,8 @@ A classic use-case for overriding the built-in methods is if you want something to happen whenever you save an object. For example (see :meth:`~Model.save` for documentation of the parameters it accepts):: + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -740,6 +757,8 @@ to happen whenever you save an object. For example (see You can also prevent saving:: + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -826,6 +845,8 @@ the child (and Django will raise an exception). An example:: + from django.db import models + class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() @@ -854,14 +875,16 @@ attribute. If a child class does not declare its own :ref:`Meta ` class, it will inherit the parent's :ref:`Meta `. If the child wants to extend the parent's :ref:`Meta ` class, it can subclass it. For example:: + from django.db import models + class CommonInfo(models.Model): - ... + # ... class Meta: abstract = True ordering = ['name'] class Student(CommonInfo): - ... + # ... class Meta(CommonInfo.Meta): db_table = 'student_info' @@ -901,6 +924,8 @@ abstract base class (only), part of the name should contain For example, given an app ``common/models.py``:: + from django.db import models + class Base(models.Model): m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related") @@ -949,6 +974,8 @@ relationship introduces links between the child model and each of its parents (via an automatically-created :class:`~django.db.models.OneToOneField`). For example:: + from django.db import models + class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) @@ -998,7 +1025,7 @@ If the parent has an ordering and you don't want the child to have any natural ordering, you can explicitly disable it:: class ChildModel(ParentModel): - ... + # ... class Meta: # Remove parent's ordering effect ordering = [] @@ -1061,15 +1088,21 @@ Proxy models are declared like normal models. You tell Django that it's a proxy model by setting the :attr:`~django.db.models.Options.proxy` attribute of the ``Meta`` class to ``True``. -For example, suppose you want to add a method to the ``Person`` model described -above. You can do it like this:: +For example, suppose you want to add a method to the ``Person`` model. You can do it like this:: + + from django.db import models + + class Person(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) class MyPerson(Person): class Meta: proxy = True def do_something(self): - ... + # ... + pass The ``MyPerson`` class operates on the same database table as its parent ``Person`` class. In particular, any new instances of ``Person`` will also be @@ -1125,8 +1158,11 @@ classes will still be available. Continuing our example from above, you could change the default manager used when you query the ``Person`` model like this:: + from django.db import models + class NewManager(models.Manager): - ... + # ... + pass class MyPerson(Person): objects = NewManager() diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 2553eac27a..a1cb5c79c5 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -17,6 +17,8 @@ models, which comprise a Weblog application: .. code-block:: python + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() From cd72c55d8603751af40a55d2d18f264827fa0744 Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sat, 18 May 2013 14:00:52 +0200 Subject: [PATCH 044/249] Add missing imports and models to the examples in the view layer documentation --- docs/ref/template-response.txt | 3 ++ .../class-based-views/generic-display.txt | 6 ++-- .../class-based-views/generic-editing.txt | 1 + docs/topics/class-based-views/mixins.txt | 8 +++++ docs/topics/files.txt | 2 ++ docs/topics/http/file-uploads.txt | 2 ++ docs/topics/http/urls.txt | 31 +++++++++++++++++++ docs/topics/http/views.txt | 6 ++++ 8 files changed, 57 insertions(+), 2 deletions(-) diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index cdefe2fae8..4f34d150ed 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -215,6 +215,7 @@ re-rendered, you can re-evaluate the rendered content, and assign the content of the response manually:: # Set up a rendered TemplateResponse + >>> from django.template.response import TemplateResponse >>> t = TemplateResponse(request, 'original.html', {}) >>> t.render() >>> print(t.content) @@ -256,6 +257,8 @@ To define a post-render callback, just define a function that takes a single argument -- response -- and register that function with the template response:: + from django.template.response import TemplateResponse + def my_render_callback(response): # Do content-sensitive processing do_post_processing() diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 64b998770f..7ffa471e79 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -248,7 +248,7 @@ specify the objects that the view will operate upon -- you can also specify the list of objects using the ``queryset`` argument:: from django.views.generic import DetailView - from books.models import Publisher, Book + from books.models import Publisher class PublisherDetail(DetailView): @@ -326,6 +326,7 @@ various useful things are stored on ``self``; as well as the request Here, we have a URLconf with a single captured group:: # urls.py + from django.conf.urls import patterns from books.views import PublisherBookList urlpatterns = patterns('', @@ -375,6 +376,7 @@ Imagine we had a ``last_accessed`` field on our ``Author`` object that we were using to keep track of the last time anybody looked at that author:: # models.py + from django.db import models class Author(models.Model): salutation = models.CharField(max_length=10) @@ -390,6 +392,7 @@ updated. First, we'd need to add an author detail bit in the URLconf to point to a custom view:: + from django.conf.urls import patterns, url from books.views import AuthorDetailView urlpatterns = patterns('', @@ -401,7 +404,6 @@ Then we'd write our new view -- ``get_object`` is the method that retrieves the object -- so we simply override it and wrap the call:: from django.views.generic import DetailView - from django.shortcuts import get_object_or_404 from django.utils import timezone from books.models import Author diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 86c5280159..7c4e02cc4e 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -222,6 +222,7 @@ works for AJAX requests as well as 'normal' form POSTs:: from django.http import HttpResponse from django.views.generic.edit import CreateView + from myapp.models import Author class AjaxableResponseMixin(object): """ diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 9550d2fb86..980e571c85 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -258,6 +258,7 @@ mixin. We can hook this into our URLs easily enough:: # urls.py + from django.conf.urls import patterns, url from books.views import RecordInterest urlpatterns = patterns('', @@ -440,6 +441,7 @@ Our new ``AuthorDetail`` looks like this:: from django.core.urlresolvers import reverse from django.views.generic import DetailView from django.views.generic.edit import FormMixin + from books.models import Author class AuthorInterestForm(forms.Form): message = forms.CharField() @@ -546,6 +548,8 @@ template as ``AuthorDisplay`` is using on ``GET``. .. code-block:: python + from django.core.urlresolvers import reverse + from django.http import HttpResponseForbidden from django.views.generic import FormView from django.views.generic.detail import SingleObjectMixin @@ -657,6 +661,8 @@ own version of :class:`~django.views.generic.detail.DetailView` by mixing :class:`~django.views.generic.detail.DetailView` before template rendering behavior has been mixed in):: + from django.views.generic.detail import BaseDetailView + class JSONDetailView(JSONResponseMixin, BaseDetailView): pass @@ -675,6 +681,8 @@ and override the implementation of to defer to the appropriate subclass depending on the type of response that the user requested:: + from django.views.generic.detail import SingleObjectTemplateResponseMixin + class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): def render_to_response(self, context): # Look for a 'format=json' GET argument diff --git a/docs/topics/files.txt b/docs/topics/files.txt index fb3cdd4af9..492e6a20b5 100644 --- a/docs/topics/files.txt +++ b/docs/topics/files.txt @@ -27,6 +27,8 @@ to deal with that file. Consider the following model, using an :class:`~django.db.models.ImageField` to store a photo:: + from django.db import models + class Car(models.Model): name = models.CharField(max_length=255) price = models.DecimalField(max_digits=5, decimal_places=2) diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 80bd5f3c44..54d748d961 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -15,6 +15,7 @@ Basic file uploads Consider a simple form containing a :class:`~django.forms.FileField`:: + # In forms.py... from django import forms class UploadFileForm(forms.Form): @@ -39,6 +40,7 @@ something like:: from django.http import HttpResponseRedirect from django.shortcuts import render_to_response + from .forms import UploadFileForm # Imaginary function to handle an uploaded file. from somewhere import handle_uploaded_file diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 9a96199dba..8a3f240307 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -123,6 +123,8 @@ is ``(?Ppattern)``, where ``name`` is the name of the group and Here's the above example URLconf, rewritten to use named groups:: + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^articles/2003/$', 'news.views.special_case_2003'), url(r'^articles/(?P\d{4})/$', 'news.views.year_archive'), @@ -192,6 +194,8 @@ A convenient trick is to specify default parameters for your views' arguments. Here's an example URLconf and view:: # URLconf + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^blog/$', 'blog.views.page'), url(r'^blog/page(?P\d+)/$', 'blog.views.page'), @@ -370,11 +374,15 @@ An included URLconf receives any captured parameters from parent URLconfs, so the following example is valid:: # In settings/urls/main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^(?P\w+)/blog/', include('foo.urls.blog')), ) # In foo/urls/blog.py + from django.conf.urls import patterns, url + urlpatterns = patterns('foo.views', url(r'^$', 'blog.index'), url(r'^archive/$', 'blog.archive'), @@ -397,6 +405,8 @@ function. For example:: + from django.conf.urls import patterns, url + urlpatterns = patterns('blog.views', url(r'^blog/(?P\d{4})/$', 'year_archive', {'foo': 'bar'}), ) @@ -427,11 +437,15 @@ For example, these two URLconf sets are functionally identical: Set one:: # main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^blog/', include('inner'), {'blogid': 3}), ) # inner.py + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive'), url(r'^about/$', 'mysite.views.about'), @@ -440,11 +454,15 @@ Set one:: Set two:: # main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^blog/', include('inner')), ) # inner.py + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive', {'blogid': 3}), url(r'^about/$', 'mysite.views.about', {'blogid': 3}), @@ -464,6 +482,8 @@ supported -- you can pass any callable object as the view. For example, given this URLconf in "string" notation:: + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive'), url(r'^about/$', 'mysite.views.about'), @@ -473,6 +493,7 @@ For example, given this URLconf in "string" notation:: You can accomplish the same thing by passing objects rather than strings. Just be sure to import the objects:: + from django.conf.urls import patterns, url from mysite.views import archive, about, contact urlpatterns = patterns('', @@ -485,6 +506,7 @@ The following example is functionally identical. It's just a bit more compact because it imports the module that contains the views, rather than importing each view individually:: + from django.conf.urls import patterns, url from mysite import views urlpatterns = patterns('', @@ -501,6 +523,7 @@ the view prefix (as explained in "The view prefix" above) will have no effect. Note that :doc:`class based views` must be imported:: + from django.conf.urls import patterns, url from mysite.views import ClassBasedView urlpatterns = patterns('', @@ -612,6 +635,9 @@ It's fairly common to use the same view function in multiple URL patterns in your URLconf. For example, these two URL patterns both point to the ``archive`` view:: + from django.conf.urls import patterns, url + from mysite.views import archive + urlpatterns = patterns('', url(r'^archive/(\d{4})/$', archive), url(r'^archive-summary/(\d{4})/$', archive, {'summary': True}), @@ -630,6 +656,9 @@ matching. Here's the above example, rewritten to use named URL patterns:: + from django.conf.urls import patterns, url + from mysite.views import archive + urlpatterns = patterns('', url(r'^archive/(\d{4})/$', archive, name="full-archive"), url(r'^archive-summary/(\d{4})/$', archive, {'summary': True}, name="arch-summary"), @@ -803,6 +832,8 @@ However, you can also ``include()`` a 3-tuple containing:: For example:: + from django.conf.urls import include, patterns, url + help_patterns = patterns('', url(r'^basic/$', 'apps.help.views.views.basic'), url(r'^advanced/$', 'apps.help.views.views.advanced'), diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index f73ec4f5be..2ccedec2f7 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -70,6 +70,8 @@ documentation. Just return an instance of one of those subclasses instead of a normal :class:`~django.http.HttpResponse` in order to signify an error. For example:: + from django.http import HttpResponse, HttpResponseNotFound + def my_view(request): # ... if foo: @@ -83,6 +85,8 @@ the :class:`~django.http.HttpResponse` documentation, you can also pass the HTTP status code into the constructor for :class:`~django.http.HttpResponse` to create a return class for any status code you like. For example:: + from django.http import HttpResponse + def my_view(request): # ... @@ -110,6 +114,8 @@ standard error page for your application, along with an HTTP error code 404. Example usage:: from django.http import Http404 + from django.shortcuts import render_to_response + from polls.models import Poll def detail(request, poll_id): try: From ddd9ee16fa0c7cc19fd472fd7c1f6d28693a89be Mon Sep 17 00:00:00 2001 From: leandrafinger Date: Sat, 18 May 2013 13:34:29 +0200 Subject: [PATCH 045/249] Add missing imports to the examples in the 'First Steps' --- docs/intro/overview.txt | 4 ++++ docs/intro/tutorial01.txt | 2 ++ docs/intro/tutorial02.txt | 16 ++++++++++++++++ docs/intro/tutorial03.txt | 5 +++++ docs/intro/tutorial04.txt | 2 ++ 5 files changed, 29 insertions(+) diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 8753817256..1a6c8fa19a 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -24,6 +24,8 @@ representing your models -- so far, it's been solving two years' worth of database-schema problems. Here's a quick example, which might be saved in the file ``mysite/news/models.py``:: + from django.db import models + class Reporter(models.Model): full_name = models.CharField(max_length=70) @@ -214,6 +216,8 @@ Generally, a view retrieves data according to the parameters, loads a template and renders the template with the retrieved data. Here's an example view for ``year_archive`` from above:: + from django.shortcuts import render_to_response + def year_archive(request, year): a_list = Article.objects.filter(pub_date__year=year) return render_to_response('news/year_archive.html', {'year': year, 'article_list': a_list}) diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index d623bd8451..6e5988b15a 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -582,6 +582,8 @@ of this object. Let's fix that by editing the polls model (in the ``Choice``. On Python 3, simply replace ``__unicode__`` by ``__str__`` in the following example:: + from django.db import models + class Poll(models.Model): # ... def __unicode__(self): # Python 3: def __str__(self): diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 1987c51a67..dd3e86d8ae 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -158,6 +158,9 @@ you want when you register the object. Let's see how this works by re-ordering the fields on the edit form. Replace the ``admin.site.register(Poll)`` line with:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fields = ['pub_date', 'question'] @@ -179,6 +182,9 @@ of fields, choosing an intuitive order is an important usability detail. And speaking of forms with dozens of fields, you might want to split the form up into fieldsets:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question']}), @@ -198,6 +204,9 @@ You can assign arbitrary HTML classes to each fieldset. Django provides a This is useful when you have a long form that contains a number of fields that aren't commonly used:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question']}), @@ -218,6 +227,7 @@ Yet. There are two ways to solve this problem. The first is to register ``Choice`` with the admin just as we did with ``Poll``. That's easy:: + from django.contrib import admin from polls.models import Choice admin.site.register(Choice) @@ -342,6 +352,12 @@ representation of the output. You can improve that by giving that method (in :file:`polls/models.py`) a few attributes, as follows:: + import datetime + from django.utils import timezone + from django.db import models + + from polls.models import Poll + class Poll(models.Model): # ... def was_published_recently(self): diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 86cc5f97e6..0bbfcdd02f 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -393,6 +393,9 @@ Now, let's tackle the poll detail view -- the page that displays the question for a given poll. Here's the view:: from django.http import Http404 + from django.shortcuts import render + + from polls.models import Poll # ... def detail(request, poll_id): try: @@ -420,6 +423,8 @@ and raise :exc:`~django.http.Http404` if the object doesn't exist. Django provides a shortcut. Here's the ``detail()`` view, rewritten:: from django.shortcuts import render, get_object_or_404 + + from polls.models import Poll # ... def detail(request, poll_id): poll = get_object_or_404(Poll, pk=poll_id) diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 9f54243a3e..f81a7d6758 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -136,6 +136,8 @@ object. For more on :class:`~django.http.HttpRequest` objects, see the After somebody votes in a poll, the ``vote()`` view redirects to the results page for the poll. Let's write that view:: + from django.shortcuts import get_object_or_404, render + def results(request, poll_id): poll = get_object_or_404(Poll, pk=poll_id) return render(request, 'polls/results.html', {'poll': poll}) From a4a761ada2286e0f08282efe8dffcd1b384c052c Mon Sep 17 00:00:00 2001 From: Marc Egli Date: Sat, 18 May 2013 17:57:20 +0200 Subject: [PATCH 046/249] add Leandra Finger, Silvan Spross and Marc Egli to AUTHORS --- AUTHORS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index 4a9981d1fe..6b48047609 100644 --- a/AUTHORS +++ b/AUTHORS @@ -196,6 +196,7 @@ answer newbie questions, and generally made Django that much better: J. Clifford Dyer Clint Ecker Nick Efford + Marc Egli eibaan@gmail.com David Eklund Julia Elman @@ -220,6 +221,7 @@ answer newbie questions, and generally made Django that much better: Stefane Fermgier J. Pablo Fernandez Maciej Fijalkowski + Leandra Finger Juan Pedro Fisanotti Ben Firshman Matthew Flanagan @@ -530,6 +532,7 @@ answer newbie questions, and generally made Django that much better: Don Spaulding Calvin Spealman Dane Springmeyer + Silvan Spross Bjørn Stabell Georgi Stanojevski starrynight From 0038296135502331c302935106d7aa568f715200 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 20:48:47 +0200 Subject: [PATCH 047/249] Fixed ff881aef on Python 3. --- tests/view_tests/tests/test_debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 1022d05998..a84b41959c 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -125,7 +125,7 @@ class ExceptionReporterTests(TestCase): def test_eol_support(self): """Test that the ExceptionReporter supports Unix, Windows and Macintosh EOL markers""" - LINES = list(u'print %d' % i for i in range(1,6)) + LINES = list('print %d' % i for i in range(1, 6)) reporter = ExceptionReporter(None, None, None, None) for newline in ['\n','\r\n','\r']: From 3eba8c7f7fc4877e7df0f83fe3bacd88082ac33e Mon Sep 17 00:00:00 2001 From: Karol Sikora Date: Sat, 18 May 2013 12:42:18 +0200 Subject: [PATCH 048/249] Fixed #20234 and #20236 -- SingleObjectMixin fixes Added object on SingleObjectMixin returned context, some code clanup. --- django/views/generic/detail.py | 10 ++++++---- django/views/generic/edit.py | 14 -------------- tests/generic_views/test_base.py | 13 +++++++++++++ tests/generic_views/views.py | 5 ++++- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index 58302bbe23..23000641b4 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -93,9 +93,11 @@ class SingleObjectMixin(ContextMixin): Insert the single object into the context dict. """ context = {} - context_object_name = self.get_context_object_name(self.object) - if context_object_name: - context[context_object_name] = self.object + if self.object: + context['object'] = self.object + context_object_name = self.get_context_object_name(self.object) + if context_object_name: + context[context_object_name] = self.object context.update(kwargs) return super(SingleObjectMixin, self).get_context_data(**context) @@ -122,7 +124,7 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin): * the value of ``template_name`` on the view (if provided) * the contents of the ``template_name_field`` field on the object instance that the view is operating upon (if available) - * ``/.html`` + * ``/.html`` """ try: names = super(SingleObjectTemplateResponseMixin, self).get_template_names() diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py index e2cc741ffb..cf87aeed27 100644 --- a/django/views/generic/edit.py +++ b/django/views/generic/edit.py @@ -136,20 +136,6 @@ class ModelFormMixin(FormMixin, SingleObjectMixin): self.object = form.save() return super(ModelFormMixin, self).form_valid(form) - def get_context_data(self, **kwargs): - """ - If an object has been supplied, inject it into the context with the - supplied context_object_name name. - """ - context = {} - if self.object: - context['object'] = self.object - context_object_name = self.get_context_object_name(self.object) - if context_object_name: - context[context_object_name] = self.object - context.update(kwargs) - return super(ModelFormMixin, self).get_context_data(**context) - class ProcessFormView(View): """ diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 2eadee2b42..ffdad4b69f 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -412,6 +412,19 @@ class GetContextDataTest(unittest.TestCase): context = test_view.get_context_data(test_name='test_value') self.assertEqual(context['test_name'], 'test_value') + def test_object_at_custom_name_in_context_data(self): + # Checks 'pony' key presence in dict returned by get_context_date + test_view = views.CustomSingleObjectView() + test_view.context_object_name = 'pony' + context = test_view.get_context_data() + self.assertEqual(context['pony'], test_view.object) + + def test_object_in_get_context_data(self): + # Checks 'object' key presence in dict returned by get_context_date #20234 + test_view = views.CustomSingleObjectView() + context = test_view.get_context_data() + self.assertEqual(context['object'], test_view.object) + class UseMultipleObjectMixinTest(unittest.TestCase): rf = RequestFactory() diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index 4dda3fe0e0..fd331f14b7 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from django.contrib.auth.decorators import login_required -from django.contrib.messages.views import SuccessMessageMixin from django.core.paginator import Paginator from django.core.urlresolvers import reverse, reverse_lazy from django.utils.decorators import method_decorator @@ -227,6 +226,10 @@ class CustomContextView(generic.detail.SingleObjectMixin, generic.View): def get_context_object_name(self, obj): return "test_name" +class CustomSingleObjectView(generic.detail.SingleObjectMixin, generic.View): + model = Book + object = Book(name="dummy") + class BookSigningConfig(object): model = BookSigning date_field = 'event_date' From caf56ad1743778bad8af6b51b08f5baa342d4cd2 Mon Sep 17 00:00:00 2001 From: Tome Cvitan Date: Sat, 18 May 2013 13:26:07 +0200 Subject: [PATCH 049/249] Fixed #20440 -- Ensured CharField's max_length/min_length are integers --- AUTHORS | 1 + django/forms/fields.py | 14 ++++++++++++-- tests/forms_tests/tests/test_fields.py | 9 +++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4a9981d1fe..6ecda3ff57 100644 --- a/AUTHORS +++ b/AUTHORS @@ -164,6 +164,7 @@ answer newbie questions, and generally made Django that much better: Leah Culver Raúl Cumplido flavio.curella@gmail.com + Tome Cvitan John D'Agostino dackze+django@gmail.com Jim Dalton diff --git a/django/forms/fields.py b/django/forms/fields.py index 3ef0d72463..ac68b9f1fc 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -198,14 +198,15 @@ class Field(object): result.validators = self.validators[:] return result + class CharField(Field): def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length super(CharField, self).__init__(*args, **kwargs) if min_length is not None: - self.validators.append(validators.MinLengthValidator(min_length)) + self.validators.append(validators.MinLengthValidator(int(min_length))) if max_length is not None: - self.validators.append(validators.MaxLengthValidator(max_length)) + self.validators.append(validators.MaxLengthValidator(int(max_length))) def to_python(self, value): "Returns a Unicode object." @@ -220,6 +221,7 @@ class CharField(Field): attrs.update({'maxlength': str(self.max_length)}) return attrs + class IntegerField(Field): default_error_messages = { 'invalid': _('Enter a whole number.'), @@ -444,6 +446,7 @@ class TimeField(BaseTemporalField): def strptime(self, value, format): return datetime.datetime.strptime(force_str(value), format).time() + class DateTimeField(BaseTemporalField): widget = DateTimeInput input_formats = formats.get_format_lazy('DATETIME_INPUT_FORMATS') @@ -482,6 +485,7 @@ class DateTimeField(BaseTemporalField): def strptime(self, value, format): return datetime.datetime.strptime(force_str(value), format) + class RegexField(CharField): def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): """ @@ -511,6 +515,7 @@ class RegexField(CharField): regex = property(_get_regex, _set_regex) + class EmailField(CharField): widget = EmailInput default_validators = [validators.validate_email] @@ -519,6 +524,7 @@ class EmailField(CharField): value = self.to_python(value).strip() return super(EmailField, self).clean(value) + class FileField(Field): widget = ClearableFileInput default_error_messages = { @@ -626,6 +632,7 @@ class ImageField(FileField): f.seek(0) return f + class URLField(CharField): widget = URLInput default_error_messages = { @@ -792,6 +799,7 @@ class ChoiceField(Field): return True return False + class TypedChoiceField(ChoiceField): def __init__(self, *args, **kwargs): self.coerce = kwargs.pop('coerce', lambda val: val) @@ -903,6 +911,7 @@ class ComboField(Field): value = field.clean(value) return value + class MultiValueField(Field): """ A Field that aggregates the logic of multiple Fields. @@ -1047,6 +1056,7 @@ class FilePathField(ChoiceField): self.widget.choices = self.choices + class SplitDateTimeField(MultiValueField): widget = SplitDateTimeWidget hidden_widget = SplitHiddenDateTimeWidget diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 7516de29b4..47c637befa 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -125,6 +125,15 @@ class FieldsTests(SimpleTestCase): self.assertEqual(f.max_length, None) self.assertEqual(f.min_length, 10) + def test_charfield_length_not_int(self): + """ + Ensure that setting min_length or max_length to something that is not a + number returns an exception. + """ + self.assertRaises(ValueError, CharField, min_length='a') + self.assertRaises(ValueError, CharField, max_length='a') + self.assertRaises(ValueError, CharField, 'a') + def test_charfield_widget_attrs(self): """ Ensure that CharField.widget_attrs() always returns a dictionary. From 7d77e9786a118dd95a268872dd9d36664066b96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emil=20Stenstro=CC=88m?= Date: Sat, 18 May 2013 13:58:45 +0200 Subject: [PATCH 050/249] Fixed #20246 -- Added non-breaking spaces between values an units --- AUTHORS | 1 + .../contrib/humanize/templatetags/humanize.py | 18 +++-- django/contrib/humanize/tests.py | 26 +++---- django/template/defaultfilters.py | 28 ++++---- django/utils/html.py | 7 ++ django/utils/timesince.py | 7 +- tests/defaultfilters/tests.py | 68 +++++++++--------- tests/template_tests/filters.py | 59 +++++++-------- tests/utils_tests/test_timesince.py | 71 ++++++++++--------- 9 files changed, 155 insertions(+), 130 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6ecda3ff57..2e02f7c07e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -536,6 +536,7 @@ answer newbie questions, and generally made Django that much better: starrynight Vasiliy Stavenko Thomas Steinacher + Emil Stenström Johan C. Stöver Nowell Strite Thomas Stromberg diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 21f4c452fa..eaee734f75 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -194,17 +194,20 @@ def naturaltime(value): return _('now') elif delta.seconds < 60: return ungettext( - 'a second ago', '%(count)s seconds ago', delta.seconds + # Translators: \\u00a0 is non-breaking space + 'a second ago', '%(count)s\u00a0seconds ago', delta.seconds ) % {'count': delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return ungettext( - 'a minute ago', '%(count)s minutes ago', count + # Translators: \\u00a0 is non-breaking space + 'a minute ago', '%(count)s\u00a0minutes ago', count ) % {'count': count} else: count = delta.seconds // 60 // 60 return ungettext( - 'an hour ago', '%(count)s hours ago', count + # Translators: \\u00a0 is non-breaking space + 'an hour ago', '%(count)s\u00a0hours ago', count ) % {'count': count} else: delta = value - now @@ -216,15 +219,18 @@ def naturaltime(value): return _('now') elif delta.seconds < 60: return ungettext( - 'a second from now', '%(count)s seconds from now', delta.seconds + # Translators: \\u00a0 is non-breaking space + 'a second from now', '%(count)s\u00a0seconds from now', delta.seconds ) % {'count': delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return ungettext( - 'a minute from now', '%(count)s minutes from now', count + # Translators: \\u00a0 is non-breaking space + 'a minute from now', '%(count)s\u00a0minutes from now', count ) % {'count': count} else: count = delta.seconds // 60 // 60 return ungettext( - 'an hour from now', '%(count)s hours from now', count + # Translators: \\u00a0 is non-breaking space + 'an hour from now', '%(count)s\u00a0hours from now', count ) % {'count': count} diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py index 1e1c8424e6..5c39c79ac3 100644 --- a/django/contrib/humanize/tests.py +++ b/django/contrib/humanize/tests.py @@ -195,22 +195,22 @@ class HumanizeTests(TestCase): result_list = [ 'now', 'a second ago', - '30 seconds ago', + '30\xa0seconds ago', 'a minute ago', - '2 minutes ago', + '2\xa0minutes ago', 'an hour ago', - '23 hours ago', - '1 day ago', - '1 year, 4 months ago', + '23\xa0hours ago', + '1\xa0day ago', + '1\xa0year, 4\xa0months ago', 'a second from now', - '30 seconds from now', + '30\xa0seconds from now', 'a minute from now', - '2 minutes from now', + '2\xa0minutes from now', 'an hour from now', - '23 hours from now', - '1 day from now', - '2 days, 6 hours from now', - '1 year, 4 months from now', + '23\xa0hours from now', + '1\xa0day from now', + '2\xa0days, 6\xa0hours from now', + '1\xa0year, 4\xa0months from now', 'now', 'now', ] @@ -218,8 +218,8 @@ class HumanizeTests(TestCase): # date in naive arithmetic is only 2 days and 5 hours after in # aware arithmetic. result_list_with_tz_support = result_list[:] - assert result_list_with_tz_support[-4] == '2 days, 6 hours from now' - result_list_with_tz_support[-4] == '2 days, 5 hours from now' + assert result_list_with_tz_support[-4] == '2\xa0days, 6\xa0hours from now' + result_list_with_tz_support[-4] == '2\xa0days, 5\xa0hours from now' orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime try: diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 88526e5a20..4201cfeb67 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -14,7 +14,7 @@ from django.utils import formats from django.utils.dateformat import format, time_format from django.utils.encoding import force_text, iri_to_uri from django.utils.html import (conditional_escape, escapejs, fix_ampersands, - escape, urlize as urlize_impl, linebreaks, strip_tags) + escape, urlize as urlize_impl, linebreaks, strip_tags, avoid_wrapping) from django.utils.http import urlquote from django.utils.text import Truncator, wrap, phone2numeric from django.utils.safestring import mark_safe, SafeData, mark_for_escaping @@ -810,7 +810,8 @@ def filesizeformat(bytes): try: bytes = float(bytes) except (TypeError,ValueError,UnicodeDecodeError): - return ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} + value = ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} + return avoid_wrapping(value) filesize_number_format = lambda value: formats.number_format(round(value, 1), 1) @@ -821,16 +822,19 @@ def filesizeformat(bytes): PB = 1<<50 if bytes < KB: - return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} - if bytes < MB: - return ugettext("%s KB") % filesize_number_format(bytes / KB) - if bytes < GB: - return ugettext("%s MB") % filesize_number_format(bytes / MB) - if bytes < TB: - return ugettext("%s GB") % filesize_number_format(bytes / GB) - if bytes < PB: - return ugettext("%s TB") % filesize_number_format(bytes / TB) - return ugettext("%s PB") % filesize_number_format(bytes / PB) + value = ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} + elif bytes < MB: + value = ugettext("%s KB") % filesize_number_format(bytes / KB) + elif bytes < GB: + value = ugettext("%s MB") % filesize_number_format(bytes / MB) + elif bytes < TB: + value = ugettext("%s GB") % filesize_number_format(bytes / GB) + elif bytes < PB: + value = ugettext("%s TB") % filesize_number_format(bytes / TB) + else: + value = ugettext("%s PB") % filesize_number_format(bytes / PB) + + return avoid_wrapping(value) @register.filter(is_safe=False) def pluralize(value, arg='s'): diff --git a/django/utils/html.py b/django/utils/html.py index 8b28d97d13..edddc48e62 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -281,3 +281,10 @@ def clean_html(text): text = trailing_empty_content_re.sub('', text) return text clean_html = allow_lazy(clean_html, six.text_type) + +def avoid_wrapping(value): + """ + Avoid text wrapping in the middle of a phrase by adding non-breaking + spaces where there previously were normal spaces. + """ + return value.replace(" ", "\xa0") diff --git a/django/utils/timesince.py b/django/utils/timesince.py index d70ab2ffe1..46c387f262 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime +from django.utils.html import avoid_wrapping from django.utils.timezone import is_aware, utc from django.utils.translation import ugettext, ungettext_lazy @@ -40,18 +41,18 @@ def timesince(d, now=None, reversed=False): since = delta.days * 24 * 60 * 60 + delta.seconds if since <= 0: # d is in the future compared to now, stop processing. - return ugettext('0 minutes') + return avoid_wrapping(ugettext('0 minutes')) for i, (seconds, name) in enumerate(chunks): count = since // seconds if count != 0: break - result = name % count + result = avoid_wrapping(name % count) if i + 1 < len(chunks): # Now get the second item seconds2, name2 = chunks[i + 1] count2 = (since - (seconds * count)) // seconds2 if count2 != 0: - result += ugettext(', ') + name2 % count2 + result += ugettext(', ') + avoid_wrapping(name2 % count2) return result def timeuntil(d, now=None): diff --git a/tests/defaultfilters/tests.py b/tests/defaultfilters/tests.py index 21734faf95..be16719c8e 100644 --- a/tests/defaultfilters/tests.py +++ b/tests/defaultfilters/tests.py @@ -527,24 +527,26 @@ class DefaultFiltersTests(TestCase): def test_timesince(self): # real testing is done in timesince.py, where we can provide our own 'now' + # NOTE: \xa0 avoids wrapping between value and unit self.assertEqual( timesince_filter(datetime.datetime.now() - datetime.timedelta(1)), - '1 day') + '1\xa0day') self.assertEqual( timesince_filter(datetime.datetime(2005, 12, 29), datetime.datetime(2005, 12, 30)), - '1 day') + '1\xa0day') def test_timeuntil(self): + # NOTE: \xa0 avoids wrapping between value and unit self.assertEqual( timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1, 1)), - '1 day') + '1\xa0day') self.assertEqual( timeuntil_filter(datetime.datetime(2005, 12, 30), datetime.datetime(2005, 12, 29)), - '1 day') + '1\xa0day') def test_default(self): self.assertEqual(default("val", "default"), 'val') @@ -574,43 +576,45 @@ class DefaultFiltersTests(TestCase): 'get out of town') def test_filesizeformat(self): - self.assertEqual(filesizeformat(1023), '1023 bytes') - self.assertEqual(filesizeformat(1024), '1.0 KB') - self.assertEqual(filesizeformat(10*1024), '10.0 KB') - self.assertEqual(filesizeformat(1024*1024-1), '1024.0 KB') - self.assertEqual(filesizeformat(1024*1024), '1.0 MB') - self.assertEqual(filesizeformat(1024*1024*50), '50.0 MB') - self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0 MB') - self.assertEqual(filesizeformat(1024*1024*1024), '1.0 GB') - self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0 TB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0 PB') + # NOTE: \xa0 avoids wrapping between value and unit + self.assertEqual(filesizeformat(1023), '1023\xa0bytes') + self.assertEqual(filesizeformat(1024), '1.0\xa0KB') + self.assertEqual(filesizeformat(10*1024), '10.0\xa0KB') + self.assertEqual(filesizeformat(1024*1024-1), '1024.0\xa0KB') + self.assertEqual(filesizeformat(1024*1024), '1.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*50), '50.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024), '1.0\xa0GB') + self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0\xa0TB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0\xa0PB') self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), - '2000.0 PB') - self.assertEqual(filesizeformat(complex(1,-1)), '0 bytes') - self.assertEqual(filesizeformat(""), '0 bytes') + '2000.0\xa0PB') + self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0bytes') + self.assertEqual(filesizeformat(""), '0\xa0bytes') self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), - '0 bytes') + '0\xa0bytes') def test_localized_filesizeformat(self): + # NOTE: \xa0 avoids wrapping between value and unit with self.settings(USE_L10N=True): with translation.override('de', deactivate=True): - self.assertEqual(filesizeformat(1023), '1023 Bytes') - self.assertEqual(filesizeformat(1024), '1,0 KB') - self.assertEqual(filesizeformat(10*1024), '10,0 KB') - self.assertEqual(filesizeformat(1024*1024-1), '1024,0 KB') - self.assertEqual(filesizeformat(1024*1024), '1,0 MB') - self.assertEqual(filesizeformat(1024*1024*50), '50,0 MB') - self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0 MB') - self.assertEqual(filesizeformat(1024*1024*1024), '1,0 GB') - self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0 TB') + self.assertEqual(filesizeformat(1023), '1023\xa0Bytes') + self.assertEqual(filesizeformat(1024), '1,0\xa0KB') + self.assertEqual(filesizeformat(10*1024), '10,0\xa0KB') + self.assertEqual(filesizeformat(1024*1024-1), '1024,0\xa0KB') + self.assertEqual(filesizeformat(1024*1024), '1,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*50), '50,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024), '1,0\xa0GB') + self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0\xa0TB') self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), - '1,0 PB') + '1,0\xa0PB') self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), - '2000,0 PB') - self.assertEqual(filesizeformat(complex(1,-1)), '0 Bytes') - self.assertEqual(filesizeformat(""), '0 Bytes') + '2000,0\xa0PB') + self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0Bytes') + self.assertEqual(filesizeformat(""), '0\xa0Bytes') self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), - '0 Bytes') + '0\xa0Bytes') def test_pluralize(self): self.assertEqual(pluralize(1), '') diff --git a/tests/template_tests/filters.py b/tests/template_tests/filters.py index 7ba1681fd5..68ef15d827 100644 --- a/tests/template_tests/filters.py +++ b/tests/template_tests/filters.py @@ -35,59 +35,60 @@ def get_filter_tests(): now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone today = date.today() + # NOTE: \xa0 avoids wrapping between value and unit return { # Default compare with datetime.now() - 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), - 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), - 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), + 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1\xa0minute'), + 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1\xa0day'), + 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1\xa0hour, 25\xa0minutes'), # Compare to a given parameter - 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1 day'), - 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1 minute'), + 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1\xa0day'), + 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1\xa0minute'), # Check that timezone is respected - 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8 hours'), + 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8\xa0hours'), # Regression for #7443 - 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1 week'), - 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1 week'), - 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0 minutes'), - 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0 minutes'), + 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1\xa0week'), + 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1\xa0week'), + 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0\xa0minutes'), + 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0\xa0minutes'), # Ensures that differing timezones are calculated correctly - 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0 minutes'), - 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0 minutes'), - 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0 minutes'), - 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0 minutes'), + 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0\xa0minutes'), + 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0\xa0minutes'), + 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0\xa0minutes'), + 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0\xa0minutes'), 'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''), 'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''), # Regression for #9065 (two date objects). - 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0 minutes'), - 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1 day'), + 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0\xa0minutes'), + 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1\xa0day'), # Default compare with datetime.now() - 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), - 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), - 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2\xa0minutes'), + 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1\xa0day'), + 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8\xa0hours, 10\xa0minutes'), # Compare to a given parameter - 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), - 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), + 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1\xa0day'), + 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1\xa0minute'), # Regression for #7443 - 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0 minutes'), - 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0 minutes'), - 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1 week'), - 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1 week'), + 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0\xa0minutes'), + 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0\xa0minutes'), + 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1\xa0week'), + 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1\xa0week'), # Ensures that differing timezones are calculated correctly - 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0 minutes'), - 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0 minutes'), + 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0\xa0minutes'), + 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0\xa0minutes'), # Regression for #9065 (two date objects). - 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0 minutes'), - 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1 day'), + 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0\xa0minutes'), + 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1\xa0day'), 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "'", "b": mark_safe("'")}, r"\' \'"), 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "'", "b": mark_safe("'")}, r"<a>\' \'"), diff --git a/tests/utils_tests/test_timesince.py b/tests/utils_tests/test_timesince.py index 5e641a42c4..cdb95e6877 100644 --- a/tests/utils_tests/test_timesince.py +++ b/tests/utils_tests/test_timesince.py @@ -21,32 +21,33 @@ class TimesinceTests(unittest.TestCase): def test_equal_datetimes(self): """ equal datetimes. """ - self.assertEqual(timesince(self.t, self.t), '0 minutes') + # NOTE: \xa0 avoids wrapping between value and unit + self.assertEqual(timesince(self.t, self.t), '0\xa0minutes') def test_ignore_microseconds_and_seconds(self): """ Microseconds and seconds are ignored. """ self.assertEqual(timesince(self.t, self.t+self.onemicrosecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t+self.onesecond), - '0 minutes') + '0\xa0minutes') def test_other_units(self): """ Test other units. """ self.assertEqual(timesince(self.t, self.t+self.oneminute), - '1 minute') - self.assertEqual(timesince(self.t, self.t+self.onehour), '1 hour') - self.assertEqual(timesince(self.t, self.t+self.oneday), '1 day') - self.assertEqual(timesince(self.t, self.t+self.oneweek), '1 week') + '1\xa0minute') + self.assertEqual(timesince(self.t, self.t+self.onehour), '1\xa0hour') + self.assertEqual(timesince(self.t, self.t+self.oneday), '1\xa0day') + self.assertEqual(timesince(self.t, self.t+self.oneweek), '1\xa0week') self.assertEqual(timesince(self.t, self.t+self.onemonth), - '1 month') - self.assertEqual(timesince(self.t, self.t+self.oneyear), '1 year') + '1\xa0month') + self.assertEqual(timesince(self.t, self.t+self.oneyear), '1\xa0year') def test_multiple_units(self): """ Test multiple units. """ self.assertEqual(timesince(self.t, - self.t+2*self.oneday+6*self.onehour), '2 days, 6 hours') + self.t+2*self.oneday+6*self.onehour), '2\xa0days, 6\xa0hours') self.assertEqual(timesince(self.t, - self.t+2*self.oneweek+2*self.oneday), '2 weeks, 2 days') + self.t+2*self.oneweek+2*self.oneday), '2\xa0weeks, 2\xa0days') def test_display_first_unit(self): """ @@ -55,10 +56,10 @@ class TimesinceTests(unittest.TestCase): """ self.assertEqual(timesince(self.t, self.t+2*self.oneweek+3*self.onehour+4*self.oneminute), - '2 weeks') + '2\xa0weeks') self.assertEqual(timesince(self.t, - self.t+4*self.oneday+5*self.oneminute), '4 days') + self.t+4*self.oneday+5*self.oneminute), '4\xa0days') def test_display_second_before_first(self): """ @@ -66,30 +67,30 @@ class TimesinceTests(unittest.TestCase): get 0 minutes. """ self.assertEqual(timesince(self.t, self.t-self.onemicrosecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onesecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneminute), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onehour), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneday), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneweek), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onemonth), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneyear), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-2*self.oneday-6*self.onehour), '0 minutes') + self.t-2*self.oneday-6*self.onehour), '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-2*self.oneweek-2*self.oneday), '0 minutes') + self.t-2*self.oneweek-2*self.oneday), '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-2*self.oneweek-3*self.onehour-4*self.oneminute), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-4*self.oneday-5*self.oneminute), '0 minutes') + self.t-4*self.oneday-5*self.oneminute), '0\xa0minutes') def test_different_timezones(self): """ When using two different timezones. """ @@ -97,28 +98,28 @@ class TimesinceTests(unittest.TestCase): now_tz = datetime.datetime.now(LocalTimezone(now)) now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15)) - self.assertEqual(timesince(now), '0 minutes') - self.assertEqual(timesince(now_tz), '0 minutes') - self.assertEqual(timeuntil(now_tz, now_tz_i), '0 minutes') + self.assertEqual(timesince(now), '0\xa0minutes') + self.assertEqual(timesince(now_tz), '0\xa0minutes') + self.assertEqual(timeuntil(now_tz, now_tz_i), '0\xa0minutes') def test_date_objects(self): """ Both timesince and timeuntil should work on date objects (#17937). """ today = datetime.date.today() - self.assertEqual(timesince(today + self.oneday), '0 minutes') - self.assertEqual(timeuntil(today - self.oneday), '0 minutes') + self.assertEqual(timesince(today + self.oneday), '0\xa0minutes') + self.assertEqual(timeuntil(today - self.oneday), '0\xa0minutes') def test_both_date_objects(self): """ Timesince should work with both date objects (#9672) """ today = datetime.date.today() - self.assertEqual(timeuntil(today + self.oneday, today), '1 day') - self.assertEqual(timeuntil(today - self.oneday, today), '0 minutes') - self.assertEqual(timeuntil(today + self.oneweek, today), '1 week') + self.assertEqual(timeuntil(today + self.oneday, today), '1\xa0day') + self.assertEqual(timeuntil(today - self.oneday, today), '0\xa0minutes') + self.assertEqual(timeuntil(today + self.oneweek, today), '1\xa0week') def test_naive_datetime_with_tzinfo_attribute(self): class naive(datetime.tzinfo): def utcoffset(self, dt): return None future = datetime.datetime(2080, 1, 1, tzinfo=naive()) - self.assertEqual(timesince(future), '0 minutes') + self.assertEqual(timesince(future), '0\xa0minutes') past = datetime.datetime(1980, 1, 1, tzinfo=naive()) - self.assertEqual(timeuntil(past), '0 minutes') + self.assertEqual(timeuntil(past), '0\xa0minutes') From 69523c1ba385cbf8a0524cab3b0396e9cec50114 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 18 May 2013 23:11:39 +0200 Subject: [PATCH 051/249] Updated contrib.humanize translation template --- .../humanize/locale/en/LC_MESSAGES/django.po | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/django/contrib/humanize/locale/en/LC_MESSAGES/django.po b/django/contrib/humanize/locale/en/LC_MESSAGES/django.po index fc75b677a0..2c3cd0c08d 100644 --- a/django/contrib/humanize/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/humanize/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-05-18 23:10+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -237,54 +237,60 @@ msgctxt "naturaltime" msgid "%(delta)s ago" msgstr "" -#: templatetags/humanize.py:194 templatetags/humanize.py:216 +#: templatetags/humanize.py:194 templatetags/humanize.py:219 msgid "now" msgstr "" -#: templatetags/humanize.py:197 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:198 #, python-format msgid "a second ago" -msgid_plural "%(count)s seconds ago" +msgid_plural "%(count)s\\u00a0seconds ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:202 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:204 #, python-format msgid "a minute ago" -msgid_plural "%(count)s minutes ago" +msgid_plural "%(count)s\\u00a0minutes ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:207 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:210 #, python-format msgid "an hour ago" -msgid_plural "%(count)s hours ago" +msgid_plural "%(count)s\\u00a0hours ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:213 +#: templatetags/humanize.py:216 #, python-format msgctxt "naturaltime" msgid "%(delta)s from now" msgstr "" -#: templatetags/humanize.py:219 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:223 #, python-format msgid "a second from now" -msgid_plural "%(count)s seconds from now" -msgstr[0] "" -msgstr[1] "" - -#: templatetags/humanize.py:224 -#, python-format -msgid "a minute from now" -msgid_plural "%(count)s minutes from now" +msgid_plural "%(count)s\\u00a0seconds from now" msgstr[0] "" msgstr[1] "" +#. Translators: \\u00a0 is non-breaking space #: templatetags/humanize.py:229 #, python-format -msgid "an hour from now" -msgid_plural "%(count)s hours from now" +msgid "a minute from now" +msgid_plural "%(count)s\\u00a0minutes from now" +msgstr[0] "" +msgstr[1] "" + +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:235 +#, python-format +msgid "an hour from now" +msgid_plural "%(count)s\\u00a0hours from now" msgstr[0] "" msgstr[1] "" From 0a50311063c416ec4d39f518e8d8110dd7eddbdf Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 18 May 2013 19:04:34 -0300 Subject: [PATCH 052/249] Fixed #20004 -- Moved non DB-related assertions to SimpleTestCase. Thanks zalew for the suggestion and work on a patch. Also updated, tweaked and fixed testing documentation. --- django/test/testcases.py | 515 +++++++++++++++--------------- docs/intro/tutorial05.txt | 4 +- docs/ref/contrib/contenttypes.txt | 2 +- docs/releases/1.3-alpha-1.txt | 2 +- docs/releases/1.3.txt | 2 +- docs/releases/1.4.txt | 8 +- docs/releases/1.6.txt | 7 +- docs/topics/python3.txt | 4 +- docs/topics/testing/overview.txt | 251 +++++++++------ 9 files changed, 425 insertions(+), 370 deletions(-) diff --git a/django/test/testcases.py b/django/test/testcases.py index 6f8cbabd86..311b50cfb7 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -231,6 +231,10 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): class SimpleTestCase(ut2.TestCase): + # The class we'll use for the test client self.client. + # Can be overridden in derived classes. + client_class = Client + _warn_txt = ("save_warnings_state/restore_warnings_state " "django.test.*TestCase methods are deprecated. Use Python's " "warnings.catch_warnings context manager instead.") @@ -264,10 +268,31 @@ class SimpleTestCase(ut2.TestCase): return def _pre_setup(self): - pass + """Performs any pre-test setup. This includes: + + * If the Test Case class has a 'urls' member, replace the + ROOT_URLCONF with it. + * Clearing the mail test outbox. + """ + self.client = self.client_class() + self._urlconf_setup() + mail.outbox = [] + + def _urlconf_setup(self): + set_urlconf(None) + if hasattr(self, 'urls'): + self._old_root_urlconf = settings.ROOT_URLCONF + settings.ROOT_URLCONF = self.urls + clear_url_caches() def _post_teardown(self): - pass + self._urlconf_teardown() + + def _urlconf_teardown(self): + set_urlconf(None) + if hasattr(self, '_old_root_urlconf'): + settings.ROOT_URLCONF = self._old_root_urlconf + clear_url_caches() def save_warnings_state(self): """ @@ -291,258 +316,6 @@ class SimpleTestCase(ut2.TestCase): """ return override_settings(**kwargs) - def assertRaisesMessage(self, expected_exception, expected_message, - callable_obj=None, *args, **kwargs): - """ - Asserts that the message in a raised exception matches the passed - value. - - Args: - expected_exception: Exception class expected to be raised. - expected_message: expected error message string value. - callable_obj: Function to be called. - args: Extra args. - kwargs: Extra kwargs. - """ - return six.assertRaisesRegex(self, expected_exception, - re.escape(expected_message), callable_obj, *args, **kwargs) - - def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, - field_kwargs=None, empty_value=''): - """ - Asserts that a form field behaves correctly with various inputs. - - Args: - fieldclass: the class of the field to be tested. - valid: a dictionary mapping valid inputs to their expected - cleaned values. - invalid: a dictionary mapping invalid inputs to one or more - raised error messages. - field_args: the args passed to instantiate the field - field_kwargs: the kwargs passed to instantiate the field - empty_value: the expected clean output for inputs in empty_values - - """ - if field_args is None: - field_args = [] - if field_kwargs is None: - field_kwargs = {} - required = fieldclass(*field_args, **field_kwargs) - optional = fieldclass(*field_args, - **dict(field_kwargs, required=False)) - # test valid inputs - for input, output in valid.items(): - self.assertEqual(required.clean(input), output) - self.assertEqual(optional.clean(input), output) - # test invalid inputs - for input, errors in invalid.items(): - with self.assertRaises(ValidationError) as context_manager: - required.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - - with self.assertRaises(ValidationError) as context_manager: - optional.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - # test required inputs - error_required = [force_text(required.error_messages['required'])] - for e in required.empty_values: - with self.assertRaises(ValidationError) as context_manager: - required.clean(e) - self.assertEqual(context_manager.exception.messages, - error_required) - self.assertEqual(optional.clean(e), empty_value) - # test that max_length and min_length are always accepted - if issubclass(fieldclass, CharField): - field_kwargs.update({'min_length':2, 'max_length':20}) - self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), - fieldclass)) - - def assertHTMLEqual(self, html1, html2, msg=None): - """ - Asserts that two HTML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid HTML. - """ - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 != dom2: - standardMsg = '%s != %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - diff = ('\n' + '\n'.join(difflib.ndiff( - six.text_type(dom1).splitlines(), - six.text_type(dom2).splitlines()))) - standardMsg = self._truncateMessage(standardMsg, diff) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertHTMLNotEqual(self, html1, html2, msg=None): - """Asserts that two HTML snippets are not semantically equivalent.""" - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 == dom2: - standardMsg = '%s == %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertInHTML(self, needle, haystack, count = None, msg_prefix=''): - needle = assert_and_parse_html(self, needle, None, - 'First argument is not valid HTML:') - haystack = assert_and_parse_html(self, haystack, None, - 'Second argument is not valid HTML:') - real_count = haystack.count(needle) - if count is not None: - self.assertEqual(real_count, count, - msg_prefix + "Found %d instances of '%s' in response" - " (expected %d)" % (real_count, needle, count)) - else: - self.assertTrue(real_count != 0, - msg_prefix + "Couldn't find '%s' in response" % needle) - - def assertJSONEqual(self, raw, expected_data, msg=None): - try: - data = json.loads(raw) - except ValueError: - self.fail("First argument is not valid JSON: %r" % raw) - if isinstance(expected_data, six.string_types): - try: - expected_data = json.loads(expected_data) - except ValueError: - self.fail("Second argument is not valid JSON: %r" % expected_data) - self.assertEqual(data, expected_data, msg=msg) - - def assertXMLEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if not result: - standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertXMLNotEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are not semantically equivalent. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if result: - standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - -class TransactionTestCase(SimpleTestCase): - - # The class we'll use for the test client self.client. - # Can be overridden in derived classes. - client_class = Client - - # Subclasses can ask for resetting of auto increment sequence before each - # test case - reset_sequences = False - - def _pre_setup(self): - """Performs any pre-test setup. This includes: - - * Flushing the database. - * If the Test Case class has a 'fixtures' member, installing the - named fixtures. - * If the Test Case class has a 'urls' member, replace the - ROOT_URLCONF with it. - * Clearing the mail test outbox. - """ - self.client = self.client_class() - self._fixture_setup() - self._urlconf_setup() - mail.outbox = [] - - def _databases_names(self, include_mirrors=True): - # If the test case has a multi_db=True flag, act on all databases, - # including mirrors or not. Otherwise, just on the default DB. - if getattr(self, 'multi_db', False): - return [alias for alias in connections - if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] - else: - return [DEFAULT_DB_ALIAS] - - def _reset_sequences(self, db_name): - conn = connections[db_name] - if conn.features.supports_sequence_reset: - sql_list = \ - conn.ops.sequence_reset_by_name_sql(no_style(), - conn.introspection.sequence_list()) - if sql_list: - with transaction.commit_on_success_unless_managed(using=db_name): - cursor = conn.cursor() - for sql in sql_list: - cursor.execute(sql) - - def _fixture_setup(self): - for db_name in self._databases_names(include_mirrors=False): - # Reset sequences - if self.reset_sequences: - self._reset_sequences(db_name) - - if hasattr(self, 'fixtures'): - # We have to use this slightly awkward syntax due to the fact - # that we're using *args and **kwargs together. - call_command('loaddata', *self.fixtures, - **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) - - def _urlconf_setup(self): - set_urlconf(None) - if hasattr(self, 'urls'): - self._old_root_urlconf = settings.ROOT_URLCONF - settings.ROOT_URLCONF = self.urls - clear_url_caches() - - def _post_teardown(self): - """ Performs any post-test things. This includes: - - * Putting back the original ROOT_URLCONF if it was changed. - * Force closing the connection, so that the next test gets - a clean cursor. - """ - self._fixture_teardown() - self._urlconf_teardown() - # Some DB cursors include SQL statements as part of cursor - # creation. If you have a test that does rollback, the effect - # of these statements is lost, which can effect the operation - # of tests (e.g., losing a timezone setting causing objects to - # be created with the wrong time). - # To make sure this doesn't happen, get a clean connection at the - # start of every test. - for conn in connections.all(): - conn.close() - - def _fixture_teardown(self): - for db in self._databases_names(include_mirrors=False): - call_command('flush', verbosity=0, interactive=False, database=db, - skip_validation=True, reset_sequences=False) - - def _urlconf_teardown(self): - set_urlconf(None) - if hasattr(self, '_old_root_urlconf'): - settings.ROOT_URLCONF = self._old_root_urlconf - clear_url_caches() - def assertRedirects(self, response, expected_url, status_code=302, target_status_code=200, host=None, msg_prefix=''): """Asserts that a response redirected to a specific URL, and that the @@ -787,6 +560,236 @@ class TransactionTestCase(SimpleTestCase): msg_prefix + "Template '%s' was used unexpectedly in rendering" " the response" % template_name) + def assertRaisesMessage(self, expected_exception, expected_message, + callable_obj=None, *args, **kwargs): + """ + Asserts that the message in a raised exception matches the passed + value. + + Args: + expected_exception: Exception class expected to be raised. + expected_message: expected error message string value. + callable_obj: Function to be called. + args: Extra args. + kwargs: Extra kwargs. + """ + return six.assertRaisesRegex(self, expected_exception, + re.escape(expected_message), callable_obj, *args, **kwargs) + + def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, + field_kwargs=None, empty_value=''): + """ + Asserts that a form field behaves correctly with various inputs. + + Args: + fieldclass: the class of the field to be tested. + valid: a dictionary mapping valid inputs to their expected + cleaned values. + invalid: a dictionary mapping invalid inputs to one or more + raised error messages. + field_args: the args passed to instantiate the field + field_kwargs: the kwargs passed to instantiate the field + empty_value: the expected clean output for inputs in empty_values + + """ + if field_args is None: + field_args = [] + if field_kwargs is None: + field_kwargs = {} + required = fieldclass(*field_args, **field_kwargs) + optional = fieldclass(*field_args, + **dict(field_kwargs, required=False)) + # test valid inputs + for input, output in valid.items(): + self.assertEqual(required.clean(input), output) + self.assertEqual(optional.clean(input), output) + # test invalid inputs + for input, errors in invalid.items(): + with self.assertRaises(ValidationError) as context_manager: + required.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + + with self.assertRaises(ValidationError) as context_manager: + optional.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + # test required inputs + error_required = [force_text(required.error_messages['required'])] + for e in required.empty_values: + with self.assertRaises(ValidationError) as context_manager: + required.clean(e) + self.assertEqual(context_manager.exception.messages, + error_required) + self.assertEqual(optional.clean(e), empty_value) + # test that max_length and min_length are always accepted + if issubclass(fieldclass, CharField): + field_kwargs.update({'min_length':2, 'max_length':20}) + self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), + fieldclass)) + + def assertHTMLEqual(self, html1, html2, msg=None): + """ + Asserts that two HTML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + """ + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 != dom2: + standardMsg = '%s != %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + diff = ('\n' + '\n'.join(difflib.ndiff( + six.text_type(dom1).splitlines(), + six.text_type(dom2).splitlines()))) + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertHTMLNotEqual(self, html1, html2, msg=None): + """Asserts that two HTML snippets are not semantically equivalent.""" + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 == dom2: + standardMsg = '%s == %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertInHTML(self, needle, haystack, count=None, msg_prefix=''): + needle = assert_and_parse_html(self, needle, None, + 'First argument is not valid HTML:') + haystack = assert_and_parse_html(self, haystack, None, + 'Second argument is not valid HTML:') + real_count = haystack.count(needle) + if count is not None: + self.assertEqual(real_count, count, + msg_prefix + "Found %d instances of '%s' in response" + " (expected %d)" % (real_count, needle, count)) + else: + self.assertTrue(real_count != 0, + msg_prefix + "Couldn't find '%s' in response" % needle) + + def assertJSONEqual(self, raw, expected_data, msg=None): + try: + data = json.loads(raw) + except ValueError: + self.fail("First argument is not valid JSON: %r" % raw) + if isinstance(expected_data, six.string_types): + try: + expected_data = json.loads(expected_data) + except ValueError: + self.fail("Second argument is not valid JSON: %r" % expected_data) + self.assertEqual(data, expected_data, msg=msg) + + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertXMLNotEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are not semantically equivalent. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if result: + standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + +class TransactionTestCase(SimpleTestCase): + + # Subclasses can ask for resetting of auto increment sequence before each + # test case + reset_sequences = False + + def _pre_setup(self): + """Performs any pre-test setup. This includes: + + * Flushing the database. + * If the Test Case class has a 'fixtures' member, installing the + named fixtures. + """ + super(TransactionTestCase, self)._pre_setup() + self._fixture_setup() + + def _databases_names(self, include_mirrors=True): + # If the test case has a multi_db=True flag, act on all databases, + # including mirrors or not. Otherwise, just on the default DB. + if getattr(self, 'multi_db', False): + return [alias for alias in connections + if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] + else: + return [DEFAULT_DB_ALIAS] + + def _reset_sequences(self, db_name): + conn = connections[db_name] + if conn.features.supports_sequence_reset: + sql_list = \ + conn.ops.sequence_reset_by_name_sql(no_style(), + conn.introspection.sequence_list()) + if sql_list: + with transaction.commit_on_success_unless_managed(using=db_name): + cursor = conn.cursor() + for sql in sql_list: + cursor.execute(sql) + + def _fixture_setup(self): + for db_name in self._databases_names(include_mirrors=False): + # Reset sequences + if self.reset_sequences: + self._reset_sequences(db_name) + + if hasattr(self, 'fixtures'): + # We have to use this slightly awkward syntax due to the fact + # that we're using *args and **kwargs together. + call_command('loaddata', *self.fixtures, + **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) + + def _post_teardown(self): + """Performs any post-test things. This includes: + + * Putting back the original ROOT_URLCONF if it was changed. + * Force closing the connection, so that the next test gets + a clean cursor. + """ + self._fixture_teardown() + super(TransactionTestCase, self)._post_teardown() + # Some DB cursors include SQL statements as part of cursor + # creation. If you have a test that does rollback, the effect + # of these statements is lost, which can effect the operation + # of tests (e.g., losing a timezone setting causing objects to + # be created with the wrong time). + # To make sure this doesn't happen, get a clean connection at the + # start of every test. + for conn in connections.all(): + conn.close() + + def _fixture_teardown(self): + for db_name in self._databases_names(include_mirrors=False): + call_command('flush', verbosity=0, interactive=False, database=db_name, + skip_validation=True, reset_sequences=False) + def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True): items = six.moves.map(transform, qs) if not ordered: @@ -841,14 +844,14 @@ class TestCase(TransactionTestCase): # Remove this when the legacy transaction management goes away. disable_transaction_methods() - for db in self._databases_names(include_mirrors=False): + for db_name in self._databases_names(include_mirrors=False): if hasattr(self, 'fixtures'): try: call_command('loaddata', *self.fixtures, **{ 'verbosity': 0, 'commit': False, - 'database': db, + 'database': db_name, 'skip_validation': True, }) except Exception: diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index a276763d67..39c3785f7c 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -503,8 +503,8 @@ of the process of creating polls. message: "No polls are available." and verifies the ``latest_poll_list`` is empty. Note that the :class:`django.test.TestCase` class provides some additional assertion methods. In these examples, we use -:meth:`~django.test.TestCase.assertContains()` and -:meth:`~django.test.TestCase.assertQuerysetEqual()`. +:meth:`~django.test.SimpleTestCase.assertContains()` and +:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`. In ``test_index_view_with_a_past_poll``, we create a poll and verify that it appears in the list. diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 4fa119bc70..1bb0802442 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -329,7 +329,7 @@ model: .. admonition:: Serializing references to ``ContentType`` objects If you're serializing data (for example, when generating - :class:`~django.test.TestCase.fixtures`) from a model that implements + :class:`~django.test.TransactionTestCase.fixtures`) from a model that implements generic relations, you should probably be using a natural key to uniquely identify related :class:`~django.contrib.contenttypes.models.ContentType` objects. See :ref:`natural keys` and diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index 42947d9a44..634e6afaf2 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -154,7 +154,7 @@ requests. These include: requests in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 89cece941b..45ebb2f1fe 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -299,7 +299,7 @@ requests. These include: in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. * Support for lookups spanning relations in admin's diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 83a5f54fc7..a013665ad3 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -541,8 +541,8 @@ compare HTML directly with the new :meth:`~django.test.SimpleTestCase.assertHTMLEqual` and :meth:`~django.test.SimpleTestCase.assertHTMLNotEqual` assertions, or use the ``html=True`` flag with -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains` to test whether the +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains` to test whether the client's response contains a given HTML fragment. See the :ref:`assertions documentation ` for more. @@ -1093,8 +1093,8 @@ wild, because they would confuse browsers too. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's now possible to check whether a template was used within a block of -code with :meth:`~django.test.TestCase.assertTemplateUsed` and -:meth:`~django.test.TestCase.assertTemplateNotUsed`. And they +code with :meth:`~django.test.SimpleTestCase.assertTemplateUsed` and +:meth:`~django.test.SimpleTestCase.assertTemplateNotUsed`. And they can be used as a context manager:: with self.assertTemplateUsed('index.html'): diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index f8e1fd6339..0eab8540b0 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -271,9 +271,10 @@ The changes in transaction management may result in additional statements to create, release or rollback savepoints. This is more likely to happen with SQLite, since it didn't support savepoints until this release. -If tests using :meth:`~django.test.TestCase.assertNumQueries` fail because of -a higher number of queries than expected, check that the extra queries are -related to savepoints, and adjust the expected number of queries accordingly. +If tests using :meth:`~django.test.TransactionTestCase.assertNumQueries` fail +because of a higher number of queries than expected, check that the extra +queries are related to savepoints, and adjust the expected number of queries +accordingly. Autocommit option for PostgreSQL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index 22e609c75c..9a0438e9e5 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -201,8 +201,8 @@ According to :pep:`3333`: Specifically, :attr:`HttpResponse.content ` contains ``bytes``, which may become an issue if you compare it with a ``str`` in your tests. The preferred solution is to rely on -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains`. These methods accept a +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains`. These methods accept a response and a unicode string as arguments. Coding guidelines diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index fc2b393898..d543099ae6 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -21,17 +21,16 @@ module defines tests using a class-based approach. .. admonition:: unittest2 - Python 2.7 introduced some major changes to the unittest library, + Python 2.7 introduced some major changes to the ``unittest`` library, adding some extremely useful features. To ensure that every Django project can benefit from these new features, Django ships with a - copy of unittest2_, a copy of the Python 2.7 unittest library, - backported for Python 2.6 compatibility. + copy of unittest2_, a copy of Python 2.7's ``unittest``, backported for + Python 2.6 compatibility. To access this library, Django provides the ``django.utils.unittest`` module alias. If you are using Python - 2.7, or you have installed unittest2 locally, Django will map the - alias to the installed version of the unittest library. Otherwise, - Django will use its own bundled version of unittest2. + 2.7, or you have installed ``unittest2`` locally, Django will map the alias + to it. Otherwise, Django will use its own bundled version of ``unittest2``. To use this alias, simply use:: @@ -41,8 +40,8 @@ module defines tests using a class-based approach. import unittest - If you want to continue to use the base unittest library, you can -- - you just won't get any of the nice new unittest2 features. + If you want to continue to use the legacy ``unittest`` library, you can -- + you just won't get any of the nice new ``unittest2`` features. .. _unittest2: http://pypi.python.org/pypi/unittest2 @@ -858,24 +857,46 @@ SimpleTestCase .. class:: SimpleTestCase() -A very thin subclass of :class:`unittest.TestCase`, it extends it with some -basic functionality like: +A thin subclass of :class:`unittest.TestCase`, it extends it with some basic +functionality like: * Saving and restoring the Python warning machinery state. -* Checking that a callable :meth:`raises a certain exception `. -* :meth:`Testing form field rendering `. -* Testing server :ref:`HTML responses for the presence/lack of a given fragment `. -* The ability to run tests with :ref:`modified settings ` +* Some useful assertions like: + + * Checking that a callable :meth:`raises a certain exception + `. + * Testing form field :meth:`rendering and error treatment + `. + * Testing :meth:`HTML responses for the presence/lack of a given fragment + `. + * Verifying that a template :meth:`has/hasn't been used to generate a given + response content `. + * Verifying a HTTP :meth:`redirect ` is + performed by the app. + * Robustly testing two :meth:`HTML fragments ` + for equality/inequality or :meth:`containment `. + * Robustly testing two :meth:`XML fragments ` + for equality/inequality. + * Robustly testing two :meth:`JSON fragments ` + for equality. + +* The ability to run tests with :ref:`modified settings `. +* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.client.Client`. +* Custom test-time :attr:`URL maps `. + +.. versionchanged:: 1.6 + + The latter two features were moved from ``TransactionTestCase`` to + ``SimpleTestCase`` in Django 1.6. If you need any of the other more complex and heavyweight Django-specific features like: -* Using the :attr:`~TestCase.client` :class:`~django.test.client.Client`. * Testing or using the ORM. -* Database :attr:`~TestCase.fixtures`. -* Custom test-time :attr:`URL maps `. +* Database :attr:`~TransactionTestCase.fixtures`. * Test :ref:`skipping based on database backend features `. -* The remaining specialized :ref:`assert* ` methods. +* The remaining specialized :meth:`assert* + ` methods. then you should use :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase` instead. @@ -1137,9 +1158,9 @@ Test cases features Default test client ~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client +.. attribute:: SimpleTestCase.client -Every test case in a ``django.test.TestCase`` instance has access to an +Every test case in a ``django.test.*TestCase`` instance has access to an instance of a Django test client. This client can be accessed as ``self.client``. This client is recreated for each test, so you don't have to worry about state (such as cookies) carrying over from one test to another. @@ -1176,10 +1197,10 @@ This means, instead of instantiating a ``Client`` in each test:: Customizing the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client_class +.. attribute:: SimpleTestCase.client_class If you want to use a different ``Client`` class (for example, a subclass -with customized behavior), use the :attr:`~TestCase.client_class` class +with customized behavior), use the :attr:`~SimpleTestCase.client_class` class attribute:: from django.test import TestCase @@ -1200,11 +1221,12 @@ attribute:: Fixture loading ~~~~~~~~~~~~~~~ -.. attribute:: TestCase.fixtures +.. attribute:: TransactionTestCase.fixtures A test case for a database-backed Web site isn't much use if there isn't any data in the database. To make it easy to put test data into the database, -Django's custom ``TestCase`` class provides a way of loading **fixtures**. +Django's custom ``TransactionTestCase`` class provides a way of loading +**fixtures**. A fixture is a collection of data that Django knows how to import into a database. For example, if your site has user accounts, you might set up a @@ -1273,7 +1295,7 @@ or by the order of test execution. URLconf configuration ~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.urls +.. attribute:: SimpleTestCase.urls If your application provides views, you may want to include tests that use the test client to exercise those views. However, an end user is free to deploy the @@ -1282,9 +1304,9 @@ tests can't rely upon the fact that your views will be available at a particular URL. In order to provide a reliable URL space for your test, -``django.test.TestCase`` provides the ability to customize the URLconf +``django.test.*TestCase`` classes provide the ability to customize the URLconf configuration for the duration of the execution of a test suite. If your -``TestCase`` instance defines an ``urls`` attribute, the ``TestCase`` will use +``*TestCase`` instance defines an ``urls`` attribute, the ``*TestCase`` will use the value of that attribute as the :setting:`ROOT_URLCONF` for the duration of that test. @@ -1307,7 +1329,7 @@ URLconf for the duration of the test case. Multi-database support ~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.multi_db +.. attribute:: TransactionTestCase.multi_db Django sets up a test database corresponding to every database that is defined in the :setting:`DATABASES` definition in your settings @@ -1340,12 +1362,12 @@ This test case will flush *all* the test databases before running Overriding settings ~~~~~~~~~~~~~~~~~~~ -.. method:: TestCase.settings +.. method:: SimpleTestCase.settings For testing purposes it's often useful to change a setting temporarily and revert to the original value after running the testing code. For this use case Django provides a standard Python context manager (see :pep:`343`) -:meth:`~django.test.TestCase.settings`, which can be used like this:: +:meth:`~django.test.SimpleTestCase.settings`, which can be used like this:: from django.test import TestCase @@ -1435,8 +1457,8 @@ MEDIA_ROOT, DEFAULT_FILE_STORAGE Default file storage Emptying the test outbox ~~~~~~~~~~~~~~~~~~~~~~~~ -If you use Django's custom ``TestCase`` class, the test runner will clear the -contents of the test email outbox at the start of each test case. +If you use any of Django's custom ``TestCase`` classes, the test runner will +clear thecontents of the test email outbox at the start of each test case. For more detail on email services during tests, see `Email services`_ below. @@ -1486,31 +1508,7 @@ your test suite. self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) - -.. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) - - Asserts that a ``Response`` instance produced the given ``status_code`` and - that ``text`` appears in the content of the response. If ``count`` is - provided, ``text`` must occur exactly ``count`` times in the response. - - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with - the response content will be based on HTML semantics instead of - character-by-character equality. Whitespace is ignored in most cases, - attribute ordering is not significant. See - :meth:`~SimpleTestCase.assertHTMLEqual` for more details. - -.. method:: TestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) - - Asserts that a ``Response`` instance produced the given ``status_code`` and - that ``text`` does not appears in the content of the response. - - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with - the response content will be based on HTML semantics instead of - character-by-character equality. Whitespace is ignored in most cases, - attribute ordering is not significant. See - :meth:`~SimpleTestCase.assertHTMLEqual` for more details. - -.. method:: TestCase.assertFormError(response, form, field, errors, msg_prefix='') +.. method:: SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='') Asserts that a field on a form raises the provided list of errors when rendered on the form. @@ -1525,7 +1523,30 @@ your test suite. ``errors`` is an error string, or a list of error strings, that are expected as a result of form validation. -.. method:: TestCase.assertTemplateUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) + + Asserts that a ``Response`` instance produced the given ``status_code`` and + that ``text`` appears in the content of the response. If ``count`` is + provided, ``text`` must occur exactly ``count`` times in the response. + + Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with + the response content will be based on HTML semantics instead of + character-by-character equality. Whitespace is ignored in most cases, + attribute ordering is not significant. See + :meth:`~SimpleTestCase.assertHTMLEqual` for more details. + +.. method:: SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) + + Asserts that a ``Response`` instance produced the given ``status_code`` and + that ``text`` does not appears in the content of the response. + + Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with + the response content will be based on HTML semantics instead of + character-by-character equality. Whitespace is ignored in most cases, + attribute ordering is not significant. See + :meth:`~SimpleTestCase.assertHTMLEqual` for more details. + +.. method:: SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was used in rendering the response. @@ -1539,15 +1560,15 @@ your test suite. with self.assertTemplateUsed(template_name='index.html'): render_to_string('index.html') -.. method:: TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was *not* used in rendering the response. You can use this as a context manager in the same way as - :meth:`~TestCase.assertTemplateUsed`. + :meth:`~SimpleTestCase.assertTemplateUsed`. -.. method:: TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') +.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') Asserts that the response return a ``status_code`` redirect status, it redirected to ``expected_url`` (including any GET data), and the final @@ -1557,44 +1578,6 @@ your test suite. ``target_status_code`` will be the url and status code for the final point of the redirect chain. -.. method:: TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) - - Asserts that a queryset ``qs`` returns a particular list of values ``values``. - - The comparison of the contents of ``qs`` and ``values`` is performed using - the function ``transform``; by default, this means that the ``repr()`` of - each value is compared. Any other callable can be used if ``repr()`` doesn't - provide a unique or helpful comparison. - - By default, the comparison is also ordering dependent. If ``qs`` doesn't - provide an implicit ordering, you can set the ``ordered`` parameter to - ``False``, which turns the comparison into a Python set comparison. - - .. versionchanged:: 1.6 - - The method now checks for undefined order and raises ``ValueError`` - if undefined order is spotted. The ordering is seen as undefined if - the given ``qs`` isn't ordered and the comparison is against more - than one ordered values. - -.. method:: TestCase.assertNumQueries(num, func, *args, **kwargs) - - Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that - ``num`` database queries are executed. - - If a ``"using"`` key is present in ``kwargs`` it is used as the database - alias for which to check the number of queries. If you wish to call a - function with a ``using`` parameter you can do it by wrapping the call with - a ``lambda`` to add an extra parameter:: - - self.assertNumQueries(7, lambda: my_function(using=7)) - - You can also use this as a context manager:: - - with self.assertNumQueries(2): - Person.objects.create(name="Aaron") - Person.objects.create(name="Daniel") - .. method:: SimpleTestCase.assertHTMLEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are equal. The comparison @@ -1624,6 +1607,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are *not* equal. The @@ -1633,6 +1618,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1644,6 +1631,8 @@ your test suite. syntax differences. When unvalid XML is passed in any parameter, an ``AssertionError`` is always raised, even if both string are identical. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1652,6 +1641,68 @@ your test suite. comparison is based on XML semantics. See :meth:`~SimpleTestCase.assertXMLEqual` for details. + Output in case of error can be customized with the ``msg`` argument. + +.. method:: SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='') + + .. versionadded:: 1.5 + + Asserts that the HTML fragment ``needle`` is contained in the ``haystack`` one. + + If the ``count`` integer argument is specified, then additionally the number + of ``needle`` occurrences will be strictly verified. + + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + +.. method:: SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None) + + .. versionadded:: 1.5 + + Asserts that the JSON fragments ``raw`` and ``expected_data`` are equal. + Usual JSON non-significant whitespace rules apply as the heavyweight is + delegated to the :mod:`json` library. + + Output in case of error can be customized with the ``msg`` argument. + +.. method:: TransactionTestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) + + Asserts that a queryset ``qs`` returns a particular list of values ``values``. + + The comparison of the contents of ``qs`` and ``values`` is performed using + the function ``transform``; by default, this means that the ``repr()`` of + each value is compared. Any other callable can be used if ``repr()`` doesn't + provide a unique or helpful comparison. + + By default, the comparison is also ordering dependent. If ``qs`` doesn't + provide an implicit ordering, you can set the ``ordered`` parameter to + ``False``, which turns the comparison into a Python set comparison. + + .. versionchanged:: 1.6 + + The method now checks for undefined order and raises ``ValueError`` + if undefined order is spotted. The ordering is seen as undefined if + the given ``qs`` isn't ordered and the comparison is against more + than one ordered values. + +.. method:: TransactionTestCase.assertNumQueries(num, func, *args, **kwargs) + + Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that + ``num`` database queries are executed. + + If a ``"using"`` key is present in ``kwargs`` it is used as the database + alias for which to check the number of queries. If you wish to call a + function with a ``using`` parameter you can do it by wrapping the call with + a ``lambda`` to add an extra parameter:: + + self.assertNumQueries(7, lambda: my_function(using=7)) + + You can also use this as a context manager:: + + with self.assertNumQueries(2): + Person.objects.create(name="Aaron") + Person.objects.create(name="Daniel") + .. _topics-testing-email: Email services @@ -1701,7 +1752,7 @@ and contents:: self.assertEqual(mail.outbox[0].subject, 'Subject here') As noted :ref:`previously `, the test outbox is emptied -at the start of every test in a Django ``TestCase``. To empty the outbox +at the start of every test in a Django ``*TestCase``. To empty the outbox manually, assign the empty list to ``mail.outbox``:: from django.core import mail From cafcc22b01579b51c0b4304d1e30fa5f5cb24f42 Mon Sep 17 00:00:00 2001 From: Peter Inglesby Date: Sun, 19 May 2013 09:28:36 +0200 Subject: [PATCH 053/249] Typo in comment --- django/contrib/auth/management/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 685e50d498..fdf822ff74 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -80,7 +80,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw for perm in _get_all_permissions(klass._meta, ctype): searched_perms.append((ctype, perm)) - # Find all the Permissions that have a context_type for a model we're + # Find all the Permissions that have a content_type for a model we're # looking for. We don't need to check for codenames since we already have # a list of the ones we're going to create. all_perms = set(auth_app.Permission.objects.using(db).filter( From 004fde0702957bd73b49313ed17919da71dc909f Mon Sep 17 00:00:00 2001 From: Axel Hecht Date: Sun, 19 May 2013 11:29:24 +0300 Subject: [PATCH 054/249] Make Urdu (ur) recognized as RTL language. Refs #20454 --- django/conf/global_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 53aef351c0..596f4ae78a 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -131,7 +131,7 @@ LANGUAGES = ( ) # Languages using BiDi (right-to-left) layout -LANGUAGES_BIDI = ("he", "ar", "fa") +LANGUAGES_BIDI = ("he", "ar", "fa", "ur") # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. From 5124ab7620a2cb448a8edec7eb6cfa69c60bcc8c Mon Sep 17 00:00:00 2001 From: Andrea Crotti Date: Sun, 19 May 2013 10:36:04 +0200 Subject: [PATCH 055/249] add comment to explain why it's necessary to do an explicit check for Python2 and how metaclasses are defined --- tests/base/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/base/models.py b/tests/base/models.py index bddb406820..d47ddcfd66 100644 --- a/tests/base/models.py +++ b/tests/base/models.py @@ -14,8 +14,10 @@ class CustomBaseModel(models.base.ModelBase): class MyModel(six.with_metaclass(CustomBaseModel, models.Model)): - """Model subclass with a custom base using six.with_metaclass.""" + """Model subclass with a custom base using six.with_metaclass.""" +# This is done to ensure that for Python2 only, defining metaclasses +# still does not fail to create the model. if not six.PY3: class MyModel(models.Model): From 0b3a6ead88c84aa30f11ad51ad3d89a016f9dd67 Mon Sep 17 00:00:00 2001 From: postrational Date: Sun, 19 May 2013 10:48:30 +0200 Subject: [PATCH 056/249] refs #20233 - Full custom user model example isn't really full Addition and fix for custom user model example documentation. https://code.djangoproject.com/ticket/20233 --- docs/topics/auth/customizing.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 56f3e60350..4b6721ba21 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -1075,7 +1075,6 @@ code would be required in the app's ``admin.py`` file:: (None, {'fields': ('email', 'password')}), ('Personal info', {'fields': ('date_of_birth',)}), ('Permissions', {'fields': ('is_admin',)}), - ('Important dates', {'fields': ('last_login',)}), ) add_fieldsets = ( (None, { @@ -1092,3 +1091,8 @@ code would be required in the app's ``admin.py`` file:: # ... and, since we're not using Django's builtin permissions, # unregister the Group model from admin. admin.site.unregister(Group) + +Finally specify the custom model as the default user model for your project using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: + + AUTH_USER_MODEL = 'customauth.MyUser' + From cc3b3ba93a7bfdd2ece739e97e36150a719acd3e Mon Sep 17 00:00:00 2001 From: Senko Rasic Date: Sat, 18 May 2013 17:51:14 +0200 Subject: [PATCH 057/249] Fixed #18990: Loaddata now complains if fixture doesn't exist The fixture named "initial_data" is exceptional though; if it doesn't exist, the error is not raised. This allows syncdb and flush management commands to attempt to load it without causing an error if it doesn't exist. --- django/core/management/commands/loaddata.py | 11 +++++++++-- tests/fixtures/tests.py | 17 ++++++++++++----- tests/fixtures_regress/tests.py | 15 ++++++++------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index c95d11cf60..fbafed3f92 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -162,9 +162,14 @@ class Command(BaseCommand): else: fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + [''] + label_found = False for fixture_dir in fixture_dirs: - self.process_dir(fixture_dir, fixture_name, compression_formats, - formats) + found = self.process_dir(fixture_dir, fixture_name, + compression_formats, formats) + label_found = label_found or found + + if fixture_name != 'initial_data' and not label_found: + raise CommandError("No fixture named '%s' found." % fixture_name) def process_dir(self, fixture_dir, fixture_name, compression_formats, serialization_formats): @@ -242,3 +247,5 @@ class Command(BaseCommand): raise CommandError( "No fixture data found for '%s'. (File format may be invalid.)" % (fixture_name)) + + return label_found diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index 103612198e..93f2438ce9 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -137,8 +137,14 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): '' ]) - # Load a fixture that doesn't exist - management.call_command('loaddata', 'unknown.json', verbosity=0, commit=False) + # Loading a fixture that doesn't exist results in an error + with self.assertRaises(management.CommandError): + management.call_command('loaddata', 'unknown.json', verbosity=0, + commit=False) + + # An attempt to load a nonexistent 'initial_data' fixture isn't an error + management.call_command('loaddata', 'initial_data.json', verbosity=0, + commit=False) # object list is unaffected self.assertQuerysetEqual(Article.objects.all(), [ @@ -273,10 +279,11 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): def test_unmatched_identifier_loading(self): # Try to load db fixture 3. This won't load because the database identifier doesn't match - management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - self.assertQuerysetEqual(Article.objects.all(), []) + with self.assertRaises(management.CommandError): + management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) + with self.assertRaises(management.CommandError): + management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) self.assertQuerysetEqual(Article.objects.all(), []) def test_output_formats(self): diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 02e923e386..df84d77a3f 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -441,13 +441,14 @@ class TestFixtures(TestCase): def test_loaddata_not_existant_fixture_file(self): stdout_output = StringIO() - management.call_command( - 'loaddata', - 'this_fixture_doesnt_exist', - verbosity=2, - commit=False, - stdout=stdout_output, - ) + with self.assertRaises(management.CommandError): + management.call_command( + 'loaddata', + 'this_fixture_doesnt_exist', + verbosity=2, + commit=False, + stdout=stdout_output, + ) self.assertTrue("No xml fixture 'this_fixture_doesnt_exist' in" in force_text(stdout_output.getvalue())) From d9c01da1f8ba3d4e0947fd1d619c8f4a9b013360 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Sat, 18 May 2013 18:37:39 +0200 Subject: [PATCH 058/249] Improve cookie based session backend docs. Note the don't require sessions to be in installed apps. --- docs/topics/http/sessions.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 0f2955fadd..268055b6fc 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -118,6 +118,13 @@ To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to stored using Django's tools for :doc:`cryptographic signing ` and the :setting:`SECRET_KEY` setting. +.. note:: + + When using cookies-based sessions :mod:`django.contrib.sessions` can be + removed from :setting:`INSTALLED_APPS` setting because data is loaded + from the key itself and not from the database, so there is no need for the + creation and usage of ``django.contrib.sessions.models.Session`` table. + .. note:: It's recommended to leave the :setting:`SESSION_COOKIE_HTTPONLY` setting From 413735b239239bc8ff38ee893e8448daf9b2bff6 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Sun, 19 May 2013 11:24:17 +0200 Subject: [PATCH 059/249] Fix a typo in a comment. --- django/utils/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/utils/http.py b/django/utils/http.py index 9897df4fb0..f4911b4ec0 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -71,7 +71,7 @@ urlunquote_plus = allow_lazy(urlunquote_plus, six.text_type) def urlencode(query, doseq=0): """ A version of Python's urllib.urlencode() function that can operate on - unicode strings. The parameters are first case to UTF-8 encoded strings and + unicode strings. The parameters are first cast to UTF-8 encoded strings and then encoded as per normal. """ if isinstance(query, MultiValueDict): From c44a2c40fe0ed79b0fa00233a204d41e9c677750 Mon Sep 17 00:00:00 2001 From: Senko Rasic Date: Sun, 19 May 2013 11:20:10 +0200 Subject: [PATCH 060/249] Fixed #18990 -- Loaddata now complains if fixture doesn't exist If the fixture doesn't exist, loaddata will output a warning. The fixture named "initial_data" is exceptional though; if it doesn't exist, the warning is not emitted. This allows syncdb and flush management commands to attempt to load it without causing spurious warnings. Thanks to Derega, ptone, dirigeant and d1ffuz0r for contributions to the ticket. --- django/core/management/commands/loaddata.py | 3 ++- tests/fixtures/tests.py | 18 ++++++++++++------ tests/fixtures_model_package/tests.py | 5 ++++- tests/fixtures_regress/tests.py | 3 ++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index fbafed3f92..ab9f7468c4 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -4,6 +4,7 @@ import os import gzip import zipfile from optparse import make_option +import warnings from django.conf import settings from django.core import serializers @@ -169,7 +170,7 @@ class Command(BaseCommand): label_found = label_found or found if fixture_name != 'initial_data' and not label_found: - raise CommandError("No fixture named '%s' found." % fixture_name) + warnings.warn("No fixture named '%s' found." % fixture_name) def process_dir(self, fixture_dir, fixture_name, compression_formats, serialization_formats): diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index 93f2438ce9..f954933046 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +import warnings + from django.contrib.sites.models import Site from django.core import management from django.db import connection, IntegrityError @@ -137,14 +139,18 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): '' ]) - # Loading a fixture that doesn't exist results in an error - with self.assertRaises(management.CommandError): + # Loading a fixture that doesn't exist emits a warning + with warnings.catch_warnings(record=True) as w: management.call_command('loaddata', 'unknown.json', verbosity=0, commit=False) + self.assertEqual(len(w), 1) + self.assertTrue(w[0].message, "No fixture named 'unknown' found.") # An attempt to load a nonexistent 'initial_data' fixture isn't an error - management.call_command('loaddata', 'initial_data.json', verbosity=0, - commit=False) + with warnings.catch_warnings(record=True) as w: + management.call_command('loaddata', 'initial_data.json', verbosity=0, + commit=False) + self.assertEqual(len(w), 0) # object list is unaffected self.assertQuerysetEqual(Article.objects.all(), [ @@ -279,10 +285,10 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): def test_unmatched_identifier_loading(self): # Try to load db fixture 3. This won't load because the database identifier doesn't match - with self.assertRaises(management.CommandError): + with warnings.catch_warnings(record=True): management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - with self.assertRaises(management.CommandError): + with warnings.catch_warnings(record=True): management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) self.assertQuerysetEqual(Article.objects.all(), []) diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index c250f647ce..af6b059c66 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -100,7 +100,10 @@ class FixtureTestCase(TestCase): ) # Load a fixture that doesn't exist - management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + import warnings + with warnings.catch_warnings(record=True): + management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + self.assertQuerysetEqual( Article.objects.all(), [ "Django conquers world!", diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index df84d77a3f..97ad6c326a 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -441,7 +441,8 @@ class TestFixtures(TestCase): def test_loaddata_not_existant_fixture_file(self): stdout_output = StringIO() - with self.assertRaises(management.CommandError): + import warnings + with warnings.catch_warnings(record=True): management.call_command( 'loaddata', 'this_fixture_doesnt_exist', From 56d6fdbbf50b29bd162fd5ab3296a25127d7af65 Mon Sep 17 00:00:00 2001 From: bbjay Date: Sat, 18 May 2013 18:50:58 +0200 Subject: [PATCH 061/249] Fixed #20452 -- Rename 'headers' to 'header fields'. --- docs/ref/request-response.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 2fac7f2f9c..fc26eabf1a 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -578,20 +578,20 @@ streaming response if (and only if) no middleware accesses the instantiated with an iterator. Django will consume and save the content of the iterator on first access. -Setting headers -~~~~~~~~~~~~~~~ +Setting header fields +~~~~~~~~~~~~~~~~~~~~~ -To set or remove a header in your response, treat it like a dictionary:: +To set or remove a header field in your response, treat it like a dictionary:: >>> response = HttpResponse() >>> response['Cache-Control'] = 'no-cache' >>> del response['Cache-Control'] Note that unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header -doesn't exist. +field doesn't exist. -HTTP headers cannot contain newlines. An attempt to set a header containing a -newline character (CR or LF) will raise ``BadHeaderError`` +HTTP header fields cannot contain newlines. An attempt to set a header field +containing a newline character (CR or LF) will raise ``BadHeaderError`` Telling the browser to treat the response as a file attachment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From e80636b6683893d40e171745a4ef0a2f1a09cc82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 19 May 2013 11:44:46 +0200 Subject: [PATCH 062/249] Added TransRealMixin to fix i18n global state pollution in the test suite --- tests/i18n/__init__.py | 17 +++++++++++++++++ tests/i18n/contenttypes/tests.py | 4 +++- tests/i18n/tests.py | 31 +++++++++++++++++++------------ 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/tests/i18n/__init__.py b/tests/i18n/__init__.py index e69de29bb2..a3e9ce7053 100644 --- a/tests/i18n/__init__.py +++ b/tests/i18n/__init__.py @@ -0,0 +1,17 @@ +from threading import local + + +class TransRealMixin(object): + """This is the only way to reset the translation machinery. Otherwise + the test suite occasionally fails because of global state pollution + between tests.""" + def flush_caches(self): + from django.utils.translation import trans_real + trans_real._translations = {} + trans_real._active = local() + trans_real._default = None + trans_real._accepted = {} + + def tearDown(self): + self.flush_caches() + super(TransRealMixin, self).tearDown() diff --git a/tests/i18n/contenttypes/tests.py b/tests/i18n/contenttypes/tests.py index 5e8a9823e1..cbac9ec5da 100644 --- a/tests/i18n/contenttypes/tests.py +++ b/tests/i18n/contenttypes/tests.py @@ -10,6 +10,8 @@ from django.utils._os import upath from django.utils import six from django.utils import translation +from i18n import TransRealMixin + @override_settings( USE_I18N=True, @@ -22,7 +24,7 @@ from django.utils import translation ('fr', 'French'), ), ) -class ContentTypeTests(TestCase): +class ContentTypeTests(TransRealMixin, TestCase): def test_verbose_name(self): company_type = ContentType.objects.get(app_label='i18n', model='company') with translation.override('en'): diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 1b15720c16..137270f830 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -44,6 +44,7 @@ if can_run_compilation_tests: from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests, MultipleLocaleCompilationTests, CompilationErrorHandling) +from . import TransRealMixin from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from .models import Company, TestModel @@ -53,7 +54,8 @@ extended_locale_paths = settings.LOCALE_PATHS + ( os.path.join(here, 'other', 'locale'), ) -class TranslationTests(TestCase): + +class TranslationTests(TransRealMixin, TestCase): def test_override(self): activate('de') @@ -335,6 +337,8 @@ class TranslationTests(TestCase): class TranslationThreadSafetyTests(TestCase): + """Specifically not using TransRealMixin here to test threading.""" + def setUp(self): self._old_language = get_language() self._translations = trans_real._translations @@ -365,9 +369,10 @@ class TranslationThreadSafetyTests(TestCase): @override_settings(USE_L10N=True) -class FormattingTests(TestCase): +class FormattingTests(TransRealMixin, TestCase): def setUp(self): + super(FormattingTests, self).setUp() self.n = decimal.Decimal('66666.666') self.f = 99999.999 self.d = datetime.date(2009, 12, 31) @@ -769,9 +774,10 @@ class FormattingTests(TestCase): self.assertEqual(template2.render(context), output2) self.assertEqual(template3.render(context), output3) -class MiscTests(TestCase): +class MiscTests(TransRealMixin, TestCase): def setUp(self): + super(MiscTests, self).setUp() self.rf = RequestFactory() def test_parse_spec_http_header(self): @@ -915,17 +921,15 @@ class MiscTests(TestCase): self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 4})), '%(percent)s% represents 4 objects') -class ResolutionOrderI18NTests(TestCase): +class ResolutionOrderI18NTests(TransRealMixin, TestCase): def setUp(self): - # Okay, this is brutal, but we have no other choice to fully reset - # the translation framework - trans_real._active = local() - trans_real._translations = {} + super(ResolutionOrderI18NTests, self).setUp() activate('de') def tearDown(self): deactivate() + super(ResolutionOrderI18NTests, self).tearDown() def assertUgettext(self, msgid, msgstr): result = ugettext(msgid) @@ -998,15 +1002,17 @@ class TestLanguageInfo(TestCase): six.assertRaisesRegex(self, KeyError, r"Unknown language code xx-xx and xx\.", get_language_info, 'xx-xx') -class MultipleLocaleActivationTests(TestCase): +class MultipleLocaleActivationTests(TransRealMixin, TestCase): """ Tests for template rendering behavior when multiple locales are activated during the lifetime of the same process. """ def setUp(self): + super(MultipleLocaleActivationTests, self).setUp() self._old_language = get_language() def tearDown(self): + super(MultipleLocaleActivationTests, self).tearDown() activate(self._old_language) def test_single_locale_activation(self): @@ -1135,7 +1141,7 @@ class MultipleLocaleActivationTests(TestCase): 'django.middleware.common.CommonMiddleware', ), ) -class LocaleMiddlewareTests(TestCase): +class LocaleMiddlewareTests(TransRealMixin, TestCase): urls = 'i18n.urls' @@ -1157,12 +1163,12 @@ class LocaleMiddlewareTests(TestCase): 'django.middleware.common.CommonMiddleware', ), ) -class CountrySpecificLanguageTests(TestCase): +class CountrySpecificLanguageTests(TransRealMixin, TestCase): urls = 'i18n.urls' def setUp(self): - trans_real._accepted = {} + super(CountrySpecificLanguageTests, self).setUp() self.rf = RequestFactory() def test_check_for_language(self): @@ -1172,6 +1178,7 @@ class CountrySpecificLanguageTests(TestCase): def test_get_language_from_request(self): + # issue 19919 r = self.rf.get('/') r.COOKIES = {} r.META = {'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.8,bg;q=0.6,ru;q=0.4'} From 753edfa4b5cf46be5fc1f04d80c25f30b18af798 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 19 May 2013 12:14:19 +0200 Subject: [PATCH 063/249] Fixed a rest mistake I introduced in d5ce2ff. --- docs/topics/http/sessions.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 268055b6fc..772ee122d5 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -132,8 +132,8 @@ and the :setting:`SECRET_KEY` setting. .. warning:: - **If the :setting:`SECRET_KEY` is not kept secret, this can lead to - arbitrary remote code execution.** + **If the SECRET_KEY is not kept secret, this can lead to arbitrary remote + code execution.** An attacker in possession of the :setting:`SECRET_KEY` can not only generate falsified session data, which your site will trust, but also From 4ad1eb1c14b629cf5bcfd253ed40e875f1bddd47 Mon Sep 17 00:00:00 2001 From: Honza Kral Date: Sat, 23 Feb 2013 16:10:32 +0100 Subject: [PATCH 064/249] Fixed #12674 -- provide a way to override admin validation Moved admin validation code to classes and have those be class attributes to the ModelAdmin classes. --- django/contrib/admin/options.py | 15 + django/contrib/admin/sites.py | 10 +- django/contrib/admin/validation.py | 721 +++++++++++++++-------------- tests/admin_validation/tests.py | 80 ++-- tests/modeladmin/tests.py | 239 ++++------ 5 files changed, 525 insertions(+), 540 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 7373837bb0..e7edccd585 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -10,6 +10,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, model_format_dict, NestedObjects) +from django.contrib.admin import validation from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect @@ -87,6 +88,14 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): readonly_fields = () ordering = None + # validation + validator_class = validation.BaseValidator + + @classmethod + def validate(cls, model): + validator = cls.validator_class() + validator.validate(cls, model) + def __init__(self): overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy() overrides.update(self.formfield_overrides) @@ -371,6 +380,9 @@ class ModelAdmin(BaseModelAdmin): actions_on_bottom = False actions_selection_counter = True + # validation + validator_class = validation.ModelAdminValidator + def __init__(self, model, admin_site): self.model = model self.opts = model._meta @@ -1447,6 +1459,9 @@ class InlineModelAdmin(BaseModelAdmin): verbose_name_plural = None can_delete = True + # validation + validator_class = validation.InlineValidator + def __init__(self, parent_model, admin_site): self.admin_site = admin_site self.parent_model = parent_model diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 414d1b4f72..e0f43dfbfe 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -66,12 +66,6 @@ class AdminSite(object): if not admin_class: admin_class = ModelAdmin - # Don't import the humongous validation code unless required - if admin_class and settings.DEBUG: - from django.contrib.admin.validation import validate - else: - validate = lambda model, adminclass: None - if isinstance(model_or_iterable, ModelBase): model_or_iterable = [model_or_iterable] for model in model_or_iterable: @@ -94,8 +88,8 @@ class AdminSite(object): options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - # Validate (which might be a no-op) - validate(admin_class, model) + if admin_class is not ModelAdmin and settings.DEBUG: + admin_class.validate(model) # Instantiate the admin class to save in the registry self._registry[model] = admin_class(model, self) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 8d65f96cf1..59c5ad35ef 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -3,358 +3,399 @@ from django.db import models from django.db.models.fields import FieldDoesNotExist from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model, _get_foreign_key) -from django.contrib.admin import ListFilter, FieldListFilter from django.contrib.admin.util import get_fields_from_path, NotRelationField -from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin, - ModelAdmin, HORIZONTAL, VERTICAL) + +""" +Does basic ModelAdmin option validation. Calls custom validation +classmethod in the end if it is provided in cls. The signature of the +custom validation classmethod should be: def validate(cls, model). +""" + +__all__ = ['BaseValidator', 'InlineValidator'] -__all__ = ['validate'] +class BaseValidator(object): + def __init__(self): + # Before we can introspect models, they need to be fully loaded so that + # inter-relations are set up correctly. We force that here. + models.get_apps() -def validate(cls, model): - """ - Does basic ModelAdmin option validation. Calls custom validation - classmethod in the end if it is provided in cls. The signature of the - custom validation classmethod should be: def validate(cls, model). - """ - # Before we can introspect models, they need to be fully loaded so that - # inter-relations are set up correctly. We force that here. - models.get_apps() + def validate(self, cls, model): + for m in dir(self): + if m.startswith('validate_'): + getattr(self, m)(cls, model) - opts = model._meta - validate_base(cls, model) + def check_field_spec(self, cls, model, flds, label): + """ + Validate the fields specification in `flds` from a ModelAdmin subclass + `cls` for the `model` model. Use `label` for reporting problems to the user. - # list_display - if hasattr(cls, 'list_display'): - check_isseq(cls, 'list_display', cls.list_display) - for idx, field in enumerate(cls.list_display): - if not callable(field): - if not hasattr(cls, field): - if not hasattr(model, field): - try: - opts.get_field(field) - except models.FieldDoesNotExist: - raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." - % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) - else: - # getattr(model, field) could be an X_RelatedObjectsDescriptor - f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) - if isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." - % (cls.__name__, idx, field)) - - # list_display_links - if hasattr(cls, 'list_display_links'): - check_isseq(cls, 'list_display_links', cls.list_display_links) - for idx, field in enumerate(cls.list_display_links): - if field not in cls.list_display: - raise ImproperlyConfigured("'%s.list_display_links[%d]' " - "refers to '%s' which is not defined in 'list_display'." - % (cls.__name__, idx, field)) - - # list_filter - if hasattr(cls, 'list_filter'): - check_isseq(cls, 'list_filter', cls.list_filter) - for idx, item in enumerate(cls.list_filter): - # There are three options for specifying a filter: - # 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') - # 2: ('field', SomeFieldListFilter) - a field-based list filter class - # 3: SomeListFilter - a non-field list filter class - if callable(item) and not isinstance(item, models.Field): - # If item is option 3, it should be a ListFilter... - if not issubclass(item, ListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" - " which is not a descendant of ListFilter." - % (cls.__name__, idx, item.__name__)) - # ... but not a FieldListFilter. - if issubclass(item, FieldListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" - " which is of type FieldListFilter but is not" - " associated with a field name." - % (cls.__name__, idx, item.__name__)) - else: - if isinstance(item, (tuple, list)): - # item is option #2 - field, list_filter_class = item - if not issubclass(list_filter_class, FieldListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d][1]'" - " is '%s' which is not of type FieldListFilter." - % (cls.__name__, idx, list_filter_class.__name__)) - else: - # item is option #1 - field = item - # Validate the field string + The fields specification can be a ``fields`` option or a ``fields`` + sub-option from a ``fieldsets`` option component. + """ + for fields in flds: + # The entry in fields might be a tuple. If it is a standalone + # field, make it into a tuple to make processing easier. + if type(fields) != tuple: + fields = (fields,) + for field in fields: + if field in cls.readonly_fields: + # Stuff can be put in fields that isn't actually a + # model field if it's in readonly_fields, + # readonly_fields will handle the validation of such + # things. + continue try: - get_fields_from_path(model, field) - except (NotRelationField, FieldDoesNotExist): - raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" - " which does not refer to a Field." + f = model._meta.get_field(field) + except models.FieldDoesNotExist: + # If we can't find a field on the model that matches, it could be an + # extra field on the form; nothing to check so move on to the next field. + continue + if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: + raise ImproperlyConfigured("'%s.%s' " + "can't include the ManyToManyField field '%s' because " + "'%s' manually specifies a 'through' model." % ( + cls.__name__, label, field, field)) + + def validate_raw_id_fields(self, cls, model): + " Validate that raw_id_fields only contains field names that are listed on the model. " + if hasattr(cls, 'raw_id_fields'): + check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) + for idx, field in enumerate(cls.raw_id_fields): + f = get_field(cls, model, 'raw_id_fields', field) + if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): + raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " + "be either a ForeignKey or ManyToManyField." % (cls.__name__, idx, field)) - # list_per_page = 100 - if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): - raise ImproperlyConfigured("'%s.list_per_page' should be a integer." - % cls.__name__) + def validate_fields(self, cls, model): + " Validate that fields only refer to existing fields, doesn't contain duplicates. " + # fields + if cls.fields: # default value is None + check_isseq(cls, 'fields', cls.fields) + self.check_field_spec(cls, model, cls.fields, 'fields') + if cls.fieldsets: + raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) + if len(cls.fields) > len(set(cls.fields)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) - # list_max_show_all - if hasattr(cls, 'list_max_show_all') and not isinstance(cls.list_max_show_all, int): - raise ImproperlyConfigured("'%s.list_max_show_all' should be an integer." - % cls.__name__) + def validate_fieldsets(self, cls, model): + " Validate that fieldsets is properly formatted and doesn't contain duplicates. " + from django.contrib.admin.options import flatten_fieldsets + if cls.fieldsets: # default value is None + check_isseq(cls, 'fieldsets', cls.fieldsets) + for idx, fieldset in enumerate(cls.fieldsets): + check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) + if len(fieldset) != 2: + raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " + "have exactly two elements." % (cls.__name__, idx)) + check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) + if 'fields' not in fieldset[1]: + raise ImproperlyConfigured("'fields' key is required in " + "%s.fieldsets[%d][1] field options dict." + % (cls.__name__, idx)) + self.check_field_spec(cls, model, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) + flattened_fieldsets = flatten_fieldsets(cls.fieldsets) + if len(flattened_fieldsets) > len(set(flattened_fieldsets)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - # list_editable - if hasattr(cls, 'list_editable') and cls.list_editable: - check_isseq(cls, 'list_editable', cls.list_editable) - for idx, field_name in enumerate(cls.list_editable): - try: - field = opts.get_field_by_name(field_name)[0] - except models.FieldDoesNotExist: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', not defined on %s.%s." - % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) - if field_name not in cls.list_display: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " - "'%s' which is not defined in 'list_display'." - % (cls.__name__, idx, field_name)) - if field_name in cls.list_display_links: - raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" - " and '%s.list_display_links'" - % (field_name, cls.__name__, cls.__name__)) - if not cls.list_display_links and cls.list_display[0] in cls.list_editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" - " the first field in list_display, '%s', which can't be" - " used unless list_display_links is set." - % (cls.__name__, idx, cls.list_display[0])) - if not field.editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', which isn't editable through the admin." - % (cls.__name__, idx, field_name)) + def validate_exclude(self, cls, model): + " Validate that exclude is a sequence without duplicates. " + if cls.exclude: # default value is None + check_isseq(cls, 'exclude', cls.exclude) + if len(cls.exclude) > len(set(cls.exclude)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) - # search_fields = () - if hasattr(cls, 'search_fields'): - check_isseq(cls, 'search_fields', cls.search_fields) + def validate_form(self, cls, model): + " Validate that form subclasses BaseModelForm. " + if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): + raise ImproperlyConfigured("%s.form does not inherit from " + "BaseModelForm." % cls.__name__) - # date_hierarchy = None - if cls.date_hierarchy: - f = get_field(cls, model, opts, 'date_hierarchy', cls.date_hierarchy) - if not isinstance(f, (models.DateField, models.DateTimeField)): - raise ImproperlyConfigured("'%s.date_hierarchy is " - "neither an instance of DateField nor DateTimeField." - % cls.__name__) + def validate_filter_vertical(self, cls, model): + " Validate that filter_vertical is a sequence of field names. " + if hasattr(cls, 'filter_vertical'): + check_isseq(cls, 'filter_vertical', cls.filter_vertical) + for idx, field in enumerate(cls.filter_vertical): + f = get_field(cls, model, 'filter_vertical', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " + "a ManyToManyField." % (cls.__name__, idx)) - # ordering = None - if cls.ordering: - check_isseq(cls, 'ordering', cls.ordering) - for idx, field in enumerate(cls.ordering): - if field == '?' and len(cls.ordering) != 1: - raise ImproperlyConfigured("'%s.ordering' has the random " - "ordering marker '?', but contains other fields as " - "well. Please either remove '?' or the other fields." + def validate_filter_horizontal(self, cls, model): + " Validate that filter_horizontal is a sequence of field names. " + if hasattr(cls, 'filter_horizontal'): + check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) + for idx, field in enumerate(cls.filter_horizontal): + f = get_field(cls, model, 'filter_horizontal', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " + "a ManyToManyField." % (cls.__name__, idx)) + + def validate_radio_fields(self, cls, model): + " Validate that radio_fields is a dictionary of choice or foreign key fields. " + from django.contrib.admin.options import HORIZONTAL, VERTICAL + if hasattr(cls, 'radio_fields'): + check_isdict(cls, 'radio_fields', cls.radio_fields) + for field, val in cls.radio_fields.items(): + f = get_field(cls, model, 'radio_fields', field) + if not (isinstance(f, models.ForeignKey) or f.choices): + raise ImproperlyConfigured("'%s.radio_fields['%s']' " + "is neither an instance of ForeignKey nor does " + "have choices set." % (cls.__name__, field)) + if not val in (HORIZONTAL, VERTICAL): + raise ImproperlyConfigured("'%s.radio_fields['%s']' " + "is neither admin.HORIZONTAL nor admin.VERTICAL." + % (cls.__name__, field)) + + def validate_prepopulated_fields(self, cls, model): + " Validate that prepopulated_fields if a dictionary containing allowed field types. " + # prepopulated_fields + if hasattr(cls, 'prepopulated_fields'): + check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) + for field, val in cls.prepopulated_fields.items(): + f = get_field(cls, model, 'prepopulated_fields', field) + if isinstance(f, (models.DateTimeField, models.ForeignKey, + models.ManyToManyField)): + raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " + "is either a DateTimeField, ForeignKey or " + "ManyToManyField. This isn't allowed." + % (cls.__name__, field)) + check_isseq(cls, "prepopulated_fields['%s']" % field, val) + for idx, f in enumerate(val): + get_field(cls, model, "prepopulated_fields['%s'][%d]" % (field, idx), f) + + def validate_ordering(self, cls, model): + " Validate that ordering refers to existing fields or is random. " + # ordering = None + if cls.ordering: + check_isseq(cls, 'ordering', cls.ordering) + for idx, field in enumerate(cls.ordering): + if field == '?' and len(cls.ordering) != 1: + raise ImproperlyConfigured("'%s.ordering' has the random " + "ordering marker '?', but contains other fields as " + "well. Please either remove '?' or the other fields." + % cls.__name__) + if field == '?': + continue + if field.startswith('-'): + field = field[1:] + # Skip ordering in the format field1__field2 (FIXME: checking + # this format would be nice, but it's a little fiddly). + if '__' in field: + continue + get_field(cls, model, 'ordering[%d]' % idx, field) + + def validate_readonly_fields(self, cls, model): + " Validate that readonly_fields refers to proper attribute or field. " + if hasattr(cls, "readonly_fields"): + check_isseq(cls, "readonly_fields", cls.readonly_fields) + for idx, field in enumerate(cls.readonly_fields): + if not callable(field): + if not hasattr(cls, field): + if not hasattr(model, field): + try: + model._meta.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r." + % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) + + +class ModelAdminValidator(BaseValidator): + def validate_save_as(self, cls, model): + " Validate save_as is a boolean. " + check_type(cls, 'save_as', bool) + + def validate_save_on_top(self, cls, model): + " Validate save_on_top is a boolean. " + check_type(cls, 'save_on_top', bool) + + def validate_inlines(self, cls, model): + " Validate inline model admin classes. " + from django.contrib.admin.options import BaseModelAdmin + if hasattr(cls, 'inlines'): + check_isseq(cls, 'inlines', cls.inlines) + for idx, inline in enumerate(cls.inlines): + if not issubclass(inline, BaseModelAdmin): + raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " + "from BaseModelAdmin." % (cls.__name__, idx)) + if not inline.model: + raise ImproperlyConfigured("'model' is a required attribute " + "of '%s.inlines[%d]'." % (cls.__name__, idx)) + if not issubclass(inline.model, models.Model): + raise ImproperlyConfigured("'%s.inlines[%d].model' does not " + "inherit from models.Model." % (cls.__name__, idx)) + inline.validate(inline.model) + self.check_inline(inline, model) + + def check_inline(self, cls, parent_model): + " Validate inline class's fk field is not excluded. " + fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + if hasattr(cls, 'exclude') and cls.exclude: + if fk and fk.name in cls.exclude: + raise ImproperlyConfigured("%s cannot exclude the field " + "'%s' - this is the foreign key to the parent model " + "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) + + def validate_list_display(self, cls, model): + " Validate that list_display only contains fields or usable attributes. " + if hasattr(cls, 'list_display'): + check_isseq(cls, 'list_display', cls.list_display) + for idx, field in enumerate(cls.list_display): + if not callable(field): + if not hasattr(cls, field): + if not hasattr(model, field): + try: + model._meta.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." + % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) + else: + # getattr(model, field) could be an X_RelatedObjectsDescriptor + f = fetch_attr(cls, model, "list_display[%d]" % idx, field) + if isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." + % (cls.__name__, idx, field)) + + def validate_list_display_links(self, cls, model): + " Validate that list_display_links is a unique subset of list_display. " + if hasattr(cls, 'list_display_links'): + check_isseq(cls, 'list_display_links', cls.list_display_links) + for idx, field in enumerate(cls.list_display_links): + if field not in cls.list_display: + raise ImproperlyConfigured("'%s.list_display_links[%d]' " + "refers to '%s' which is not defined in 'list_display'." + % (cls.__name__, idx, field)) + + def validate_list_filter(self, cls, model): + """ + Validate that list_filter is a sequence of one of three options: + 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') + 2: ('field', SomeFieldListFilter) - a field-based list filter class + 3: SomeListFilter - a non-field list filter class + """ + from django.contrib.admin import ListFilter, FieldListFilter + if hasattr(cls, 'list_filter'): + check_isseq(cls, 'list_filter', cls.list_filter) + for idx, item in enumerate(cls.list_filter): + if callable(item) and not isinstance(item, models.Field): + # If item is option 3, it should be a ListFilter... + if not issubclass(item, ListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" + " which is not a descendant of ListFilter." + % (cls.__name__, idx, item.__name__)) + # ... but not a FieldListFilter. + if issubclass(item, FieldListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" + " which is of type FieldListFilter but is not" + " associated with a field name." + % (cls.__name__, idx, item.__name__)) + else: + if isinstance(item, (tuple, list)): + # item is option #2 + field, list_filter_class = item + if not issubclass(list_filter_class, FieldListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d][1]'" + " is '%s' which is not of type FieldListFilter." + % (cls.__name__, idx, list_filter_class.__name__)) + else: + # item is option #1 + field = item + # Validate the field string + try: + get_fields_from_path(model, field) + except (NotRelationField, FieldDoesNotExist): + raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" + " which does not refer to a Field." + % (cls.__name__, idx, field)) + + def validate_list_select_related(self, cls, model): + " Validate that list_select_related is a boolean. " + check_type(cls, 'list_select_related', bool) + + def validate_list_per_page(self, cls, model): + " Validate that list_per_page is an integer. " + check_type(cls, 'list_per_page', int) + + def validate_list_max_show_all(self, cls, model): + " Validate that list_max_show_all is an integer. " + check_type(cls, 'list_max_show_all', int) + + def validate_list_editable(self, cls, model): + """ + Validate that list_editable is a sequence of editable fields from + list_display without first element. + """ + if hasattr(cls, 'list_editable') and cls.list_editable: + check_isseq(cls, 'list_editable', cls.list_editable) + for idx, field_name in enumerate(cls.list_editable): + try: + field = model._meta.get_field_by_name(field_name)[0] + except models.FieldDoesNotExist: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " + "field, '%s', not defined on %s.%s." + % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) + if field_name not in cls.list_display: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " + "'%s' which is not defined in 'list_display'." + % (cls.__name__, idx, field_name)) + if field_name in cls.list_display_links: + raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" + " and '%s.list_display_links'" + % (field_name, cls.__name__, cls.__name__)) + if not cls.list_display_links and cls.list_display[0] in cls.list_editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" + " the first field in list_display, '%s', which can't be" + " used unless list_display_links is set." + % (cls.__name__, idx, cls.list_display[0])) + if not field.editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " + "field, '%s', which isn't editable through the admin." + % (cls.__name__, idx, field_name)) + + def validate_search_fields(self, cls, model): + " Validate search_fields is a sequence. " + if hasattr(cls, 'search_fields'): + check_isseq(cls, 'search_fields', cls.search_fields) + + def validate_date_hierarchy(self, cls, model): + " Validate that date_hierarchy refers to DateField or DateTimeField. " + if cls.date_hierarchy: + f = get_field(cls, model, 'date_hierarchy', cls.date_hierarchy) + if not isinstance(f, (models.DateField, models.DateTimeField)): + raise ImproperlyConfigured("'%s.date_hierarchy is " + "neither an instance of DateField nor DateTimeField." % cls.__name__) - if field == '?': - continue - if field.startswith('-'): - field = field[1:] - # Skip ordering in the format field1__field2 (FIXME: checking - # this format would be nice, but it's a little fiddly). - if '__' in field: - continue - get_field(cls, model, opts, 'ordering[%d]' % idx, field) - - if hasattr(cls, "readonly_fields"): - check_readonly_fields(cls, model, opts) - - # list_select_related = False - # save_as = False - # save_on_top = False - for attr in ('list_select_related', 'save_as', 'save_on_top'): - if not isinstance(getattr(cls, attr), bool): - raise ImproperlyConfigured("'%s.%s' should be a boolean." - % (cls.__name__, attr)) - # inlines = [] - if hasattr(cls, 'inlines'): - check_isseq(cls, 'inlines', cls.inlines) - for idx, inline in enumerate(cls.inlines): - if not issubclass(inline, BaseModelAdmin): - raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " - "from BaseModelAdmin." % (cls.__name__, idx)) - if not inline.model: - raise ImproperlyConfigured("'model' is a required attribute " - "of '%s.inlines[%d]'." % (cls.__name__, idx)) - if not issubclass(inline.model, models.Model): - raise ImproperlyConfigured("'%s.inlines[%d].model' does not " - "inherit from models.Model." % (cls.__name__, idx)) - validate_base(inline, inline.model) - validate_inline(inline, cls, model) +class InlineValidator(BaseValidator): + def validate_fk_name(self, cls, model): + " Validate that fk_name refers to a ForeignKey. " + if cls.fk_name: # default value is None + f = get_field(cls, model, 'fk_name', cls.fk_name) + if not isinstance(f, models.ForeignKey): + raise ImproperlyConfigured("'%s.fk_name is not an instance of " + "models.ForeignKey." % cls.__name__) -def validate_inline(cls, parent, parent_model): + def validate_extra(self, cls, model): + " Validate that extra is an integer. " + check_type(cls, 'extra', int) - # model is already verified to exist and be a Model - if cls.fk_name: # default value is None - f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name) - if not isinstance(f, models.ForeignKey): - raise ImproperlyConfigured("'%s.fk_name is not an instance of " - "models.ForeignKey." % cls.__name__) + def validate_max_num(self, cls, model): + " Validate that max_num is an integer. " + check_type(cls, 'max_num', int) - fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + def validate_formset(self, cls, model): + " Validate formset is a subclass of BaseModelFormSet. " + if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): + raise ImproperlyConfigured("'%s.formset' does not inherit from " + "BaseModelFormSet." % cls.__name__) - # extra = 3 - if not isinstance(cls.extra, int): - raise ImproperlyConfigured("'%s.extra' should be a integer." - % cls.__name__) - # max_num = None - max_num = getattr(cls, 'max_num', None) - if max_num is not None and not isinstance(max_num, int): - raise ImproperlyConfigured("'%s.max_num' should be an integer or None (default)." - % cls.__name__) - - # formset - if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): - raise ImproperlyConfigured("'%s.formset' does not inherit from " - "BaseModelFormSet." % cls.__name__) - - # exclude - if hasattr(cls, 'exclude') and cls.exclude: - if fk and fk.name in cls.exclude: - raise ImproperlyConfigured("%s cannot exclude the field " - "'%s' - this is the foreign key to the parent model " - "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) - - if hasattr(cls, "readonly_fields"): - check_readonly_fields(cls, cls.model, cls.model._meta) - -def validate_fields_spec(cls, model, opts, flds, label): - """ - Validate the fields specification in `flds` from a ModelAdmin subclass - `cls` for the `model` model. `opts` is `model`'s Meta inner class. - Use `label` for reporting problems to the user. - - The fields specification can be a ``fields`` option or a ``fields`` - sub-option from a ``fieldsets`` option component. - """ - for fields in flds: - # The entry in fields might be a tuple. If it is a standalone - # field, make it into a tuple to make processing easier. - if type(fields) != tuple: - fields = (fields,) - for field in fields: - if field in cls.readonly_fields: - # Stuff can be put in fields that isn't actually a - # model field if it's in readonly_fields, - # readonly_fields will handle the validation of such - # things. - continue - try: - f = opts.get_field(field) - except models.FieldDoesNotExist: - # If we can't find a field on the model that matches, it could be an - # extra field on the form; nothing to check so move on to the next field. - continue - if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: - raise ImproperlyConfigured("'%s.%s' " - "can't include the ManyToManyField field '%s' because " - "'%s' manually specifies a 'through' model." % ( - cls.__name__, label, field, field)) - -def validate_base(cls, model): - opts = model._meta - - # raw_id_fields - if hasattr(cls, 'raw_id_fields'): - check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) - for idx, field in enumerate(cls.raw_id_fields): - f = get_field(cls, model, opts, 'raw_id_fields', field) - if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): - raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " - "be either a ForeignKey or ManyToManyField." - % (cls.__name__, idx, field)) - - # fields - if cls.fields: # default value is None - check_isseq(cls, 'fields', cls.fields) - validate_fields_spec(cls, model, opts, cls.fields, 'fields') - if cls.fieldsets: - raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) - if len(cls.fields) > len(set(cls.fields)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) - - # fieldsets - if cls.fieldsets: # default value is None - check_isseq(cls, 'fieldsets', cls.fieldsets) - for idx, fieldset in enumerate(cls.fieldsets): - check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) - if len(fieldset) != 2: - raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " - "have exactly two elements." % (cls.__name__, idx)) - check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) - if 'fields' not in fieldset[1]: - raise ImproperlyConfigured("'fields' key is required in " - "%s.fieldsets[%d][1] field options dict." - % (cls.__name__, idx)) - validate_fields_spec(cls, model, opts, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) - flattened_fieldsets = flatten_fieldsets(cls.fieldsets) - if len(flattened_fieldsets) > len(set(flattened_fieldsets)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - - # exclude - if cls.exclude: # default value is None - check_isseq(cls, 'exclude', cls.exclude) - if len(cls.exclude) > len(set(cls.exclude)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) - - # form - if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): - raise ImproperlyConfigured("%s.form does not inherit from " - "BaseModelForm." % cls.__name__) - - # filter_vertical - if hasattr(cls, 'filter_vertical'): - check_isseq(cls, 'filter_vertical', cls.filter_vertical) - for idx, field in enumerate(cls.filter_vertical): - f = get_field(cls, model, opts, 'filter_vertical', field) - if not isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " - "a ManyToManyField." % (cls.__name__, idx)) - - # filter_horizontal - if hasattr(cls, 'filter_horizontal'): - check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) - for idx, field in enumerate(cls.filter_horizontal): - f = get_field(cls, model, opts, 'filter_horizontal', field) - if not isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " - "a ManyToManyField." % (cls.__name__, idx)) - - # radio_fields - if hasattr(cls, 'radio_fields'): - check_isdict(cls, 'radio_fields', cls.radio_fields) - for field, val in cls.radio_fields.items(): - f = get_field(cls, model, opts, 'radio_fields', field) - if not (isinstance(f, models.ForeignKey) or f.choices): - raise ImproperlyConfigured("'%s.radio_fields['%s']' " - "is neither an instance of ForeignKey nor does " - "have choices set." % (cls.__name__, field)) - if not val in (HORIZONTAL, VERTICAL): - raise ImproperlyConfigured("'%s.radio_fields['%s']' " - "is neither admin.HORIZONTAL nor admin.VERTICAL." - % (cls.__name__, field)) - - # prepopulated_fields - if hasattr(cls, 'prepopulated_fields'): - check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) - for field, val in cls.prepopulated_fields.items(): - f = get_field(cls, model, opts, 'prepopulated_fields', field) - if isinstance(f, (models.DateTimeField, models.ForeignKey, - models.ManyToManyField)): - raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " - "is either a DateTimeField, ForeignKey or " - "ManyToManyField. This isn't allowed." - % (cls.__name__, field)) - check_isseq(cls, "prepopulated_fields['%s']" % field, val) - for idx, f in enumerate(val): - get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f) +def check_type(cls, attr, type_): + if getattr(cls, attr, None) is not None and not isinstance(getattr(cls, attr), type_): + raise ImproperlyConfigured("'%s.%s' should be a %s." + % (cls.__name__, attr, type_.__name__ )) def check_isseq(cls, label, obj): if not isinstance(obj, (list, tuple)): @@ -364,16 +405,16 @@ def check_isdict(cls, label, obj): if not isinstance(obj, dict): raise ImproperlyConfigured("'%s.%s' must be a dictionary." % (cls.__name__, label)) -def get_field(cls, model, opts, label, field): +def get_field(cls, model, label, field): try: - return opts.get_field(field) + return model._meta.get_field(field) except models.FieldDoesNotExist: raise ImproperlyConfigured("'%s.%s' refers to field '%s' that is missing from model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) -def fetch_attr(cls, model, opts, label, field): +def fetch_attr(cls, model, label, field): try: - return opts.get_field(field) + return model._meta.get_field(field) except models.FieldDoesNotExist: pass try: @@ -381,15 +422,3 @@ def fetch_attr(cls, model, opts, label, field): except AttributeError: raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) - -def check_readonly_fields(cls, model, opts): - check_isseq(cls, "readonly_fields", cls.readonly_fields) - for idx, field in enumerate(cls.readonly_fields): - if not callable(field): - if not hasattr(cls, field): - if not hasattr(model, field): - try: - opts.get_field(field) - except models.FieldDoesNotExist: - raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r." - % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 16f73c6390..5eee3e7105 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from django import forms from django.contrib import admin -from django.contrib.admin.validation import validate, validate_inline from django.core.exceptions import ImproperlyConfigured from django.test import TestCase @@ -38,13 +37,13 @@ class ValidationTestCase(TestCase): "fields": ["title", "original_release"], }), ] - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_custom_modelforms_with_fields_fieldsets(self): """ # Regression test for #8027: custom ModelForms with fields/fieldsets """ - validate(ValidFields, Song) + ValidFields.validate(Song) def test_custom_get_form_with_fieldsets(self): """ @@ -52,7 +51,7 @@ class ValidationTestCase(TestCase): is overridden. Refs #19445. """ - validate(ValidFormFieldsets, Song) + ValidFormFieldsets.validate(Song) def test_exclude_values(self): """ @@ -62,16 +61,16 @@ class ValidationTestCase(TestCase): exclude = ('foo') self.assertRaisesMessage(ImproperlyConfigured, "'ExcludedFields1.exclude' must be a list or tuple.", - validate, - ExcludedFields1, Book) + ExcludedFields1.validate, + Book) def test_exclude_duplicate_values(self): class ExcludedFields2(admin.ModelAdmin): exclude = ('name', 'name') self.assertRaisesMessage(ImproperlyConfigured, "There are duplicate field(s) in ExcludedFields2.exclude", - validate, - ExcludedFields2, Book) + ExcludedFields2.validate, + Book) def test_exclude_in_inline(self): class ExcludedFieldsInline(admin.TabularInline): @@ -84,8 +83,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'ExcludedFieldsInline.exclude' must be a list or tuple.", - validate, - ExcludedFieldsAlbumAdmin, Album) + ExcludedFieldsAlbumAdmin.validate, + Album) def test_exclude_inline_model_admin(self): """ @@ -102,8 +101,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "SongInline cannot exclude the field 'album' - this is the foreign key to the parent model admin_validation.Album.", - validate, - AlbumAdmin, Album) + AlbumAdmin.validate, + Album) def test_app_label_in_admin_validation(self): """ @@ -114,8 +113,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'RawIdNonexistingAdmin.raw_id_fields' refers to field 'nonexisting' that is missing from model 'admin_validation.Album'.", - validate, - RawIdNonexistingAdmin, Album) + RawIdNonexistingAdmin.validate, + Album) def test_fk_exclusion(self): """ @@ -127,28 +126,35 @@ class ValidationTestCase(TestCase): model = TwoAlbumFKAndAnE exclude = ("e",) fk_name = "album1" - validate_inline(TwoAlbumFKAndAnEInline, None, Album) + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + MyAdmin.validate(Album) + def test_inline_self_validation(self): class TwoAlbumFKAndAnEInline(admin.TabularInline): model = TwoAlbumFKAndAnE + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] self.assertRaisesMessage(Exception, " has more than 1 ForeignKey to ", - validate_inline, - TwoAlbumFKAndAnEInline, None, Album) + MyAdmin.validate, Album) def test_inline_with_specified(self): class TwoAlbumFKAndAnEInline(admin.TabularInline): model = TwoAlbumFKAndAnE fk_name = "album1" - validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + MyAdmin.validate(Album) def test_readonly(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ("title",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_on_method(self): def my_function(obj): @@ -157,7 +163,7 @@ class ValidationTestCase(TestCase): class SongAdmin(admin.ModelAdmin): readonly_fields = (my_function,) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_on_modeladmin(self): class SongAdmin(admin.ModelAdmin): @@ -166,13 +172,13 @@ class ValidationTestCase(TestCase): def readonly_method_on_modeladmin(self, obj): pass - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_method_on_model(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ("readonly_method_on_model",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_nonexistant_field(self): class SongAdmin(admin.ModelAdmin): @@ -180,8 +186,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.", - validate, - SongAdmin, Song) + SongAdmin.validate, + Song) def test_nonexistant_field_on_inline(self): class CityInline(admin.TabularInline): @@ -190,8 +196,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "CityInline.readonly_fields[0], 'i_dont_exist' is not a callable or an attribute of 'CityInline' or found in the model 'City'.", - validate_inline, - CityInline, None, State) + CityInline.validate, + City) def test_extra(self): class SongAdmin(admin.ModelAdmin): @@ -199,13 +205,13 @@ class ValidationTestCase(TestCase): if instance.title == "Born to Run": return "Best Ever!" return "Status unknown." - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_lambda(self): class SongAdmin(admin.ModelAdmin): readonly_fields = (lambda obj: "test",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_graceful_m2m_fail(self): """ @@ -219,8 +225,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", - validate, - BookAdmin, Book) + BookAdmin.validate, + Book) def test_cannot_include_through(self): class FieldsetBookAdmin(admin.ModelAdmin): @@ -230,20 +236,20 @@ class ValidationTestCase(TestCase): ) self.assertRaisesMessage(ImproperlyConfigured, "'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", - validate, - FieldsetBookAdmin, Book) + FieldsetBookAdmin.validate, + Book) def test_nested_fields(self): class NestedFieldsAdmin(admin.ModelAdmin): fields = ('price', ('name', 'subtitle')) - validate(NestedFieldsAdmin, Book) + NestedFieldsAdmin.validate(Book) def test_nested_fieldsets(self): class NestedFieldsetAdmin(admin.ModelAdmin): fieldsets = ( ('Main', {'fields': ('price', ('name', 'subtitle'))}), ) - validate(NestedFieldsetAdmin, Book) + NestedFieldsetAdmin.validate(Book) def test_explicit_through_override(self): """ @@ -260,7 +266,7 @@ class ValidationTestCase(TestCase): # If the through model is still a string (and hasn't been resolved to a model) # the validation will fail. - validate(BookAdmin, Book) + BookAdmin.validate(Book) def test_non_model_fields(self): """ @@ -274,7 +280,7 @@ class ValidationTestCase(TestCase): form = SongForm fields = ['title', 'extra_data'] - validate(FieldsOnFormOnlyAdmin, Song) + FieldsOnFormOnlyAdmin.validate(Song) def test_non_model_first_field(self): """ @@ -292,4 +298,4 @@ class ValidationTestCase(TestCase): form = SongForm fields = ['extra_data', 'title'] - validate(FieldsOnFormOnlyAdmin, Song) + FieldsOnFormOnlyAdmin.validate(Song) diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 0d933bc1f9..f89f1c20ec 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -7,7 +7,6 @@ from django.conf import settings from django.contrib.admin.options import (ModelAdmin, TabularInline, HORIZONTAL, VERTICAL) from django.contrib.admin.sites import AdminSite -from django.contrib.admin.validation import validate from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect from django.contrib.admin import (SimpleListFilter, BooleanFieldListFilter) @@ -523,8 +522,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -534,8 +532,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -545,15 +542,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields\[0\]', 'name' must be either a ForeignKey or ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = ('users',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_fieldsets_validation(self): @@ -563,8 +559,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -574,8 +569,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -585,8 +579,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]' does not have exactly two elements.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -596,8 +589,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]\[1\]' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -607,15 +599,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'fields' key is required in ValidationTestModelAdmin.fieldsets\[0\]\[1\] field options dict.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) @@ -624,8 +615,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "Both fieldsets and fields are specified in ValidationTestModelAdmin.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -635,8 +625,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "There are duplicate field\(s\) in ValidationTestModelAdmin.fieldsets", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -646,8 +635,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "There are duplicate field\(s\) in ValidationTestModelAdmin.fields", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -662,8 +650,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "ValidationTestModelAdmin.form does not inherit from BaseModelForm.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -676,7 +663,7 @@ class ValidationTests(unittest.TestCase): }), ) - validate(BandAdmin, Band) + BandAdmin.validate(Band) class AdminBandForm(forms.ModelForm): delete = forms.BooleanField() @@ -690,7 +677,7 @@ class ValidationTests(unittest.TestCase): }), ) - validate(BandAdmin, Band) + BandAdmin.validate(Band) def test_filter_vertical_validation(self): @@ -700,8 +687,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -711,8 +697,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -722,15 +707,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical\[0\]' must be a ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): filter_vertical = ("users",) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_filter_horizontal_validation(self): @@ -740,8 +724,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -751,8 +734,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -762,15 +744,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal\[0\]' must be a ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = ("users",) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_radio_fields_validation(self): @@ -780,8 +761,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -791,8 +771,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -802,8 +781,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields\['name'\]' is neither an instance of ForeignKey nor does have choices set.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -813,15 +791,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields\['state'\]' is neither admin.HORIZONTAL nor admin.VERTICAL.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): radio_fields = {"state": VERTICAL} - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_prepopulated_fields_validation(self): @@ -831,8 +808,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -842,8 +818,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -853,8 +828,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields\['slug'\]\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -864,15 +838,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields\['users'\]' is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = {"slug": ("name",)} - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_display_validation(self): @@ -882,8 +855,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -893,8 +865,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, str_prefix("ValidationTestModelAdmin.list_display\[0\], %(_)s'non_existent_field' is not a callable or an attribute of 'ValidationTestModelAdmin' or found in the model 'ValidationTestModel'."), - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -904,8 +875,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display\[0\]', 'users' is a ManyToManyField which is not supported.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -917,7 +887,7 @@ class ValidationTests(unittest.TestCase): pass list_display = ('name', 'decade_published_in', 'a_method', a_callable) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_display_links_validation(self): @@ -927,8 +897,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -938,8 +907,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'non_existent_field' which is not defined in 'list_display'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -949,8 +917,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'name' which is not defined in 'list_display'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -963,7 +930,7 @@ class ValidationTests(unittest.TestCase): list_display = ('name', 'decade_published_in', 'a_method', a_callable) list_display_links = ('name', 'decade_published_in', 'a_method', a_callable) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_filter_validation(self): @@ -973,8 +940,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -984,8 +950,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' refers to 'non_existent_field' which does not refer to a Field.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -998,8 +963,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of ListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1009,8 +973,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1028,8 +991,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1039,8 +1001,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1049,7 +1010,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter), 'no') - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_per_page_validation(self): @@ -1058,16 +1019,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_per_page' should be a integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_per_page' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_per_page = 100 - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_max_show_all_allowed_validation(self): @@ -1076,16 +1036,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_max_show_all' should be an integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_max_show_all' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_max_show_all = 200 - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_search_fields_validation(self): @@ -1095,8 +1054,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.search_fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1108,8 +1066,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.date_hierarchy' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1119,15 +1076,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): date_hierarchy = 'pub_date' - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_ordering_validation(self): @@ -1137,8 +1093,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1148,8 +1103,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1159,25 +1113,24 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering' has the random ordering marker '\?', but contains other fields as well. Please either remove '\?' or the other fields.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): ordering = ('?',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): ordering = ('band__name',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): ordering = ('name',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_select_related_validation(self): @@ -1186,16 +1139,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_select_related' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_select_related' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_select_related = False - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_save_as_validation(self): @@ -1204,16 +1156,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.save_as' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.save_as' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): save_as = True - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_save_on_top_validation(self): @@ -1222,16 +1173,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.save_on_top' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.save_on_top' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): save_on_top = True - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_inlines_validation(self): @@ -1241,8 +1191,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1255,8 +1204,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines\[0\]' does not inherit from BaseModelAdmin.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1269,8 +1217,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'model' is a required attribute of 'ValidationTestModelAdmin.inlines\[0\]'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1286,8 +1233,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines\[0\].model' does not inherit from models.Model.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1297,7 +1243,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_fields_validation(self): @@ -1311,8 +1257,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1328,8 +1273,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.fk_name' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestInlineModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1340,7 +1284,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_extra_validation(self): @@ -1353,9 +1297,8 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestInline.extra' should be a integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestInline.extra' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1366,7 +1309,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_max_num_validation(self): @@ -1379,9 +1322,8 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestInline.max_num' should be an integer or None \(default\).", - validate, - ValidationTestModelAdmin, + "'ValidationTestInline.max_num' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1392,7 +1334,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_formset_validation(self): @@ -1409,8 +1351,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.formset' does not inherit from BaseModelFormSet.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1424,4 +1365,4 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) From 65c557115f6e76293c39ce7b73b62216911f9489 Mon Sep 17 00:00:00 2001 From: Senko Rasic Date: Sun, 19 May 2013 12:49:03 +0200 Subject: [PATCH 065/249] fix warnings imports in fixtures tests --- tests/fixtures_model_package/tests.py | 3 ++- tests/fixtures_regress/tests.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index af6b059c66..fbd0271336 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import warnings + from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase @@ -100,7 +102,6 @@ class FixtureTestCase(TestCase): ) # Load a fixture that doesn't exist - import warnings with warnings.catch_warnings(record=True): management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 97ad6c326a..5114302267 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals import os import re +import warnings from django.core.serializers.base import DeserializationError from django.core import management @@ -441,7 +442,6 @@ class TestFixtures(TestCase): def test_loaddata_not_existant_fixture_file(self): stdout_output = StringIO() - import warnings with warnings.catch_warnings(record=True): management.call_command( 'loaddata', From 26e3e7ecb5e4c9af4cd5aa178f65ce1585d3ae07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sun, 19 May 2013 12:43:34 +0200 Subject: [PATCH 066/249] Fixed #11915: generic Accept-Language matches country-specific variants --- django/middleware/locale.py | 3 +- django/utils/translation/trans_real.py | 50 ++++++++++++++++---------- tests/i18n/__init__.py | 1 + tests/i18n/tests.py | 15 +++++++- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 9b2ef8ff32..25fbbaccdd 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -6,6 +6,7 @@ from django.core.urlresolvers import (is_valid_path, get_resolver, from django.http import HttpResponseRedirect from django.utils.cache import patch_vary_headers from django.utils import translation +from django.utils.datastructures import SortedDict class LocaleMiddleware(object): @@ -18,7 +19,7 @@ class LocaleMiddleware(object): """ def __init__(self): - self._supported_languages = dict(settings.LANGUAGES) + self._supported_languages = SortedDict(settings.LANGUAGES) self._is_language_prefix_patterns_used = False for url_pattern in get_resolver(None).url_patterns: if isinstance(url_pattern, LocaleRegexURLResolver): diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 26be0ed729..0aaa4ec99b 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -10,7 +10,9 @@ from threading import local import warnings from django.utils.importlib import import_module +from django.utils.datastructures import SortedDict from django.utils.encoding import force_str, force_text +from django.utils.functional import memoize from django.utils._os import upath from django.utils.safestring import mark_safe, SafeData from django.utils import six @@ -29,6 +31,7 @@ _default = None # This is a cache for normalized accept-header languages to prevent multiple # file lookups when checking the same locale on repeated requests. _accepted = {} +_checked_languages = {} # magic gettext number to separate context from message CONTEXT_SEPARATOR = "\x04" @@ -355,38 +358,54 @@ def check_for_language(lang_code): if gettext_module.find('django', path, [to_locale(lang_code)]) is not None: return True return False +check_for_language = memoize(check_for_language, _checked_languages, 1) -def get_supported_language_variant(lang_code, supported=None): +def get_supported_language_variant(lang_code, supported=None, strict=False): """ Returns the language-code that's listed in supported languages, possibly selecting a more generic variant. Raises LookupError if nothing found. + + If `strict` is False (the default), the function will look for an alternative + country-specific variant when the currently checked is not found. """ if supported is None: from django.conf import settings - supported = dict(settings.LANGUAGES) + supported = SortedDict(settings.LANGUAGES) if lang_code: - # e.g. if fr-CA is not supported, try fr-ca; - # if that fails, fallback to fr. - variants = (lang_code, lang_code.lower(), lang_code.split('-')[0], - lang_code.lower().split('-')[0]) + # if fr-CA is not supported, try fr-ca; if that fails, fallback to fr. + generic_lang_code = lang_code.split('-')[0] + variants = (lang_code, lang_code.lower(), generic_lang_code, + generic_lang_code.lower()) for code in variants: if code in supported and check_for_language(code): return code + if not strict: + # if fr-fr is not supported, try fr-ca. + for supported_code in supported: + if supported_code.startswith((generic_lang_code + '-', + generic_lang_code.lower() + '-')): + return supported_code raise LookupError(lang_code) -def get_language_from_path(path, supported=None): +def get_language_from_path(path, supported=None, strict=False): """ Returns the language-code if there is a valid language-code found in the `path`. + + If `strict` is False (the default), the function will look for an alternative + country-specific variant when the currently checked is not found. """ if supported is None: from django.conf import settings - supported = dict(settings.LANGUAGES) + supported = SortedDict(settings.LANGUAGES) regex_match = language_code_prefix_re.match(path) - if regex_match: - lang_code = regex_match.group(1) - if lang_code in supported and check_for_language(lang_code): - return lang_code + if not regex_match: + return None + lang_code = regex_match.group(1) + try: + return get_supported_language_variant(lang_code, supported, strict=strict) + except LookupError: + return None def get_language_from_request(request, check_path=False): """ @@ -400,7 +419,7 @@ def get_language_from_request(request, check_path=False): """ global _accepted from django.conf import settings - supported = dict(settings.LANGUAGES) + supported = SortedDict(settings.LANGUAGES) if check_path: lang_code = get_language_from_path(request.path_info, supported) @@ -424,11 +443,6 @@ def get_language_from_request(request, check_path=False): if accept_lang == '*': break - # We have a very restricted form for our language files (no encoding - # specifier, since they all must be UTF-8 and only one possible - # language each time. So we avoid the overhead of gettext.find() and - # work out the MO file manually. - # 'normalized' is the root name of the locale in POSIX format (which is # the format used for the directories holding the MO files). normalized = locale.locale_alias.get(to_locale(accept_lang, True)) diff --git a/tests/i18n/__init__.py b/tests/i18n/__init__.py index a3e9ce7053..c5aaa31fe3 100644 --- a/tests/i18n/__init__.py +++ b/tests/i18n/__init__.py @@ -11,6 +11,7 @@ class TransRealMixin(object): trans_real._active = local() trans_real._default = None trans_real._accepted = {} + trans_real._checked_languages = {} def tearDown(self): self.flush_caches() diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 137270f830..9f1e366c9f 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1157,6 +1157,7 @@ class LocaleMiddlewareTests(TransRealMixin, TestCase): LANGUAGES=( ('bg', 'Bulgarian'), ('en-us', 'English'), + ('pt-br', 'Portugese (Brazil)'), ), MIDDLEWARE_CLASSES=( 'django.middleware.locale.LocaleMiddleware', @@ -1176,7 +1177,6 @@ class CountrySpecificLanguageTests(TransRealMixin, TestCase): self.assertTrue(check_for_language('en-us')) self.assertTrue(check_for_language('en-US')) - def test_get_language_from_request(self): # issue 19919 r = self.rf.get('/') @@ -1189,3 +1189,16 @@ class CountrySpecificLanguageTests(TransRealMixin, TestCase): r.META = {'HTTP_ACCEPT_LANGUAGE': 'bg-bg,en-US;q=0.8,en;q=0.6,ru;q=0.4'} lang = get_language_from_request(r) self.assertEqual('bg', lang) + + def test_specific_language_codes(self): + # issue 11915 + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('pt-br', lang) + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('pt-br', lang) From cc62cbed76daeaea28c1e4892244bcf1e148f373 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 19 May 2013 06:51:55 -0400 Subject: [PATCH 067/249] Fixed some line wrapping, refs #20233 --- docs/topics/auth/customizing.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 4b6721ba21..9a8bab947c 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -1092,7 +1092,7 @@ code would be required in the app's ``admin.py`` file:: # unregister the Group model from admin. admin.site.unregister(Group) -Finally specify the custom model as the default user model for your project using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: +Finally, specify the custom model as the default user model for your project +using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: AUTH_USER_MODEL = 'customauth.MyUser' - From cb86f707a04e5635817d5f37a1443f9bf7d6af21 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 19 May 2013 12:58:13 +0200 Subject: [PATCH 068/249] Fixed #12747 -- Made reason phrases customizable. --- django/core/handlers/wsgi.py | 67 ++------------------------------- django/http/response.py | 70 +++++++++++++++++++++++++++++++++-- docs/ref/request-response.txt | 27 +++++++++++--- docs/releases/1.6.txt | 2 + tests/responses/__init__.py | 0 tests/responses/models.py | 0 tests/responses/tests.py | 15 ++++++++ 7 files changed, 109 insertions(+), 72 deletions(-) create mode 100644 tests/responses/__init__.py create mode 100644 tests/responses/models.py create mode 100644 tests/responses/tests.py diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index c348c6c8da..af78d1d269 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -13,67 +13,12 @@ from django.core.urlresolvers import set_script_prefix from django.utils import datastructures from django.utils.encoding import force_str, force_text, iri_to_uri +# For backwards compatibility -- lots of code uses this in the wild! +from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT + logger = logging.getLogger('django.request') -# See http://www.iana.org/assignments/http-status-codes -STATUS_CODE_TEXT = { - 100: 'CONTINUE', - 101: 'SWITCHING PROTOCOLS', - 102: 'PROCESSING', - 200: 'OK', - 201: 'CREATED', - 202: 'ACCEPTED', - 203: 'NON-AUTHORITATIVE INFORMATION', - 204: 'NO CONTENT', - 205: 'RESET CONTENT', - 206: 'PARTIAL CONTENT', - 207: 'MULTI-STATUS', - 208: 'ALREADY REPORTED', - 226: 'IM USED', - 300: 'MULTIPLE CHOICES', - 301: 'MOVED PERMANENTLY', - 302: 'FOUND', - 303: 'SEE OTHER', - 304: 'NOT MODIFIED', - 305: 'USE PROXY', - 306: 'RESERVED', - 307: 'TEMPORARY REDIRECT', - 400: 'BAD REQUEST', - 401: 'UNAUTHORIZED', - 402: 'PAYMENT REQUIRED', - 403: 'FORBIDDEN', - 404: 'NOT FOUND', - 405: 'METHOD NOT ALLOWED', - 406: 'NOT ACCEPTABLE', - 407: 'PROXY AUTHENTICATION REQUIRED', - 408: 'REQUEST TIMEOUT', - 409: 'CONFLICT', - 410: 'GONE', - 411: 'LENGTH REQUIRED', - 412: 'PRECONDITION FAILED', - 413: 'REQUEST ENTITY TOO LARGE', - 414: 'REQUEST-URI TOO LONG', - 415: 'UNSUPPORTED MEDIA TYPE', - 416: 'REQUESTED RANGE NOT SATISFIABLE', - 417: 'EXPECTATION FAILED', - 418: "I'M A TEAPOT", - 422: 'UNPROCESSABLE ENTITY', - 423: 'LOCKED', - 424: 'FAILED DEPENDENCY', - 426: 'UPGRADE REQUIRED', - 500: 'INTERNAL SERVER ERROR', - 501: 'NOT IMPLEMENTED', - 502: 'BAD GATEWAY', - 503: 'SERVICE UNAVAILABLE', - 504: 'GATEWAY TIMEOUT', - 505: 'HTTP VERSION NOT SUPPORTED', - 506: 'VARIANT ALSO NEGOTIATES', - 507: 'INSUFFICIENT STORAGE', - 508: 'LOOP DETECTED', - 510: 'NOT EXTENDED', -} - class LimitedStream(object): ''' LimitedStream wraps another stream in order to not allow reading from it @@ -254,11 +199,7 @@ class WSGIHandler(base.BaseHandler): response._handler_class = self.__class__ - try: - status_text = STATUS_CODE_TEXT[response.status_code] - except KeyError: - status_text = 'UNKNOWN STATUS CODE' - status = '%s %s' % (response.status_code, status_text) + status = '%s %s' % (response.status_code, response.reason_phrase) response_headers = [(str(k), str(v)) for k, v in response.items()] for c in response.cookies.values(): response_headers.append((str('Set-Cookie'), str(c.output(header='')))) diff --git a/django/http/response.py b/django/http/response.py index 88ac8848c2..671fb1c573 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -20,6 +20,65 @@ from django.utils.http import cookie_date from django.utils.six.moves import map +# See http://www.iana.org/assignments/http-status-codes +REASON_PHRASES = { + 100: 'CONTINUE', + 101: 'SWITCHING PROTOCOLS', + 102: 'PROCESSING', + 200: 'OK', + 201: 'CREATED', + 202: 'ACCEPTED', + 203: 'NON-AUTHORITATIVE INFORMATION', + 204: 'NO CONTENT', + 205: 'RESET CONTENT', + 206: 'PARTIAL CONTENT', + 207: 'MULTI-STATUS', + 208: 'ALREADY REPORTED', + 226: 'IM USED', + 300: 'MULTIPLE CHOICES', + 301: 'MOVED PERMANENTLY', + 302: 'FOUND', + 303: 'SEE OTHER', + 304: 'NOT MODIFIED', + 305: 'USE PROXY', + 306: 'RESERVED', + 307: 'TEMPORARY REDIRECT', + 400: 'BAD REQUEST', + 401: 'UNAUTHORIZED', + 402: 'PAYMENT REQUIRED', + 403: 'FORBIDDEN', + 404: 'NOT FOUND', + 405: 'METHOD NOT ALLOWED', + 406: 'NOT ACCEPTABLE', + 407: 'PROXY AUTHENTICATION REQUIRED', + 408: 'REQUEST TIMEOUT', + 409: 'CONFLICT', + 410: 'GONE', + 411: 'LENGTH REQUIRED', + 412: 'PRECONDITION FAILED', + 413: 'REQUEST ENTITY TOO LARGE', + 414: 'REQUEST-URI TOO LONG', + 415: 'UNSUPPORTED MEDIA TYPE', + 416: 'REQUESTED RANGE NOT SATISFIABLE', + 417: 'EXPECTATION FAILED', + 418: "I'M A TEAPOT", + 422: 'UNPROCESSABLE ENTITY', + 423: 'LOCKED', + 424: 'FAILED DEPENDENCY', + 426: 'UPGRADE REQUIRED', + 500: 'INTERNAL SERVER ERROR', + 501: 'NOT IMPLEMENTED', + 502: 'BAD GATEWAY', + 503: 'SERVICE UNAVAILABLE', + 504: 'GATEWAY TIMEOUT', + 505: 'HTTP VERSION NOT SUPPORTED', + 506: 'VARIANT ALSO NEGOTIATES', + 507: 'INSUFFICIENT STORAGE', + 508: 'LOOP DETECTED', + 510: 'NOT EXTENDED', +} + + class BadHeaderError(ValueError): pass @@ -33,8 +92,9 @@ class HttpResponseBase(six.Iterator): """ status_code = 200 + reason_phrase = None # Use default reason phrase for status code. - def __init__(self, content_type=None, status=None, mimetype=None): + def __init__(self, content_type=None, status=None, reason=None, mimetype=None): # _headers is a mapping of the lower-case name to the original case of # the header (required for working with legacy systems) and the header # value. Both the name of the header and its value are ASCII strings. @@ -53,9 +113,13 @@ class HttpResponseBase(six.Iterator): content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, self._charset) self.cookies = SimpleCookie() - if status: + if status is not None: self.status_code = status - + if reason is not None: + self.reason_phrase = reason + elif self.reason_phrase is None: + self.reason_phrase = REASON_PHRASES.get(self.status_code, + 'UNKNOWN STATUS CODE') self['Content-Type'] = content_type def serialize_headers(self): diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index fc26eabf1a..10c3f32e60 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -616,7 +616,13 @@ Attributes .. attribute:: HttpResponse.status_code - The `HTTP Status code`_ for the response. + The `HTTP status code`_ for the response. + +.. attribute:: HttpResponse.reason_phrase + + .. versionadded:: 1.6 + + The HTTP reason phrase for the response. .. attribute:: HttpResponse.streaming @@ -628,7 +634,7 @@ Attributes Methods ------- -.. method:: HttpResponse.__init__(content='', content_type=None, status=200) +.. method:: HttpResponse.__init__(content='', content_type=None, status=200, reason=None) Instantiates an ``HttpResponse`` object with the given page content and content type. @@ -646,8 +652,12 @@ Methods Historically, this parameter was called ``mimetype`` (now deprecated). - ``status`` is the `HTTP Status code`_ for the response. + ``status`` is the `HTTP status code`_ for the response. + .. versionadded:: 1.6 + + ``reason`` is the HTTP response phrase. If not provided, a default phrase + will be used. .. method:: HttpResponse.__setitem__(header, value) @@ -727,8 +737,7 @@ Methods This method makes an :class:`HttpResponse` instance a file-like object. -.. _HTTP Status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 - +.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 .. _ref-httpresponse-subclasses: @@ -851,7 +860,13 @@ Attributes .. attribute:: HttpResponse.status_code - The `HTTP Status code`_ for the response. + The `HTTP status code`_ for the response. + +.. attribute:: HttpResponse.reason_phrase + + .. versionadded:: 1.6 + + The HTTP reason phrase for the response. .. attribute:: HttpResponse.streaming diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 0eab8540b0..b4668c38d0 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -241,6 +241,8 @@ Minor features * The ``choices`` argument to model fields now accepts an iterable of iterables instead of requiring an iterable of lists or tuples. +* The reason phrase can be customized in HTTP responses. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/responses/__init__.py b/tests/responses/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/responses/models.py b/tests/responses/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/responses/tests.py b/tests/responses/tests.py new file mode 100644 index 0000000000..e5320f5af9 --- /dev/null +++ b/tests/responses/tests.py @@ -0,0 +1,15 @@ +from django.http import HttpResponse +import unittest + +class HttpResponseTests(unittest.TestCase): + + def test_status_code(self): + resp = HttpResponse(status=418) + self.assertEqual(resp.status_code, 418) + self.assertEqual(resp.reason_phrase, "I'M A TEAPOT") + + def test_reason_phrase(self): + reason = "I'm an anarchist coffee pot on crack." + resp = HttpResponse(status=814, reason=reason) + self.assertEqual(resp.status_code, 814) + self.assertEqual(resp.reason_phrase, reason) From 660762681cfbd8cabce0b6c83fae5b3b60c0d60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Sat, 18 May 2013 17:43:21 +0200 Subject: [PATCH 069/249] Fixed #20126 -- XViewMiddleware moved to django.contrib.admindocs.middleware --- django/contrib/admindocs/middleware.py | 23 +++++++++++++ django/middleware/doc.py | 27 +++------------- docs/ref/contrib/admin/admindocs.txt | 10 +++--- docs/ref/middleware.txt | 13 -------- docs/ref/settings.txt | 2 +- docs/releases/1.6.txt | 4 +++ tests/admin_docs/__init__.py | 0 tests/admin_docs/fixtures/data.xml | 17 ++++++++++ tests/admin_docs/models.py | 0 tests/admin_docs/tests.py | 45 ++++++++++++++++++++++++++ tests/admin_docs/urls.py | 11 +++++++ tests/admin_docs/views.py | 13 ++++++++ 12 files changed, 124 insertions(+), 41 deletions(-) create mode 100644 django/contrib/admindocs/middleware.py create mode 100644 tests/admin_docs/__init__.py create mode 100644 tests/admin_docs/fixtures/data.xml create mode 100644 tests/admin_docs/models.py create mode 100644 tests/admin_docs/tests.py create mode 100644 tests/admin_docs/urls.py create mode 100644 tests/admin_docs/views.py diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py new file mode 100644 index 0000000000..ee3fe2cb2f --- /dev/null +++ b/django/contrib/admindocs/middleware.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django import http + +class XViewMiddleware(object): + """ + Adds an X-View header to internal HEAD requests -- used by the documentation system. + """ + def process_view(self, request, view_func, view_args, view_kwargs): + """ + If the request method is HEAD and either the IP is internal or the + user is a logged-in staff member, quickly return with an x-header + indicating the view function. This is used by the documentation module + to lookup the view function for an arbitrary page. + """ + assert hasattr(request, 'user'), ( + "The XView middleware requires authentication middleware to be " + "installed. Edit your MIDDLEWARE_CLASSES setting to insert " + "'django.contrib.auth.middleware.AuthenticationMiddleware'.") + if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or + (request.user.is_active and request.user.is_staff)): + response = http.HttpResponse() + response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) + return response diff --git a/django/middleware/doc.py b/django/middleware/doc.py index ee3fe2cb2f..1af7b6150a 100644 --- a/django/middleware/doc.py +++ b/django/middleware/doc.py @@ -1,23 +1,6 @@ -from django.conf import settings -from django import http +"""XViewMiddleware has been moved to django.contrib.admindocs.middleware.""" -class XViewMiddleware(object): - """ - Adds an X-View header to internal HEAD requests -- used by the documentation system. - """ - def process_view(self, request, view_func, view_args, view_kwargs): - """ - If the request method is HEAD and either the IP is internal or the - user is a logged-in staff member, quickly return with an x-header - indicating the view function. This is used by the documentation module - to lookup the view function for an arbitrary page. - """ - assert hasattr(request, 'user'), ( - "The XView middleware requires authentication middleware to be " - "installed. Edit your MIDDLEWARE_CLASSES setting to insert " - "'django.contrib.auth.middleware.AuthenticationMiddleware'.") - if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or - (request.user.is_active and request.user.is_staff)): - response = http.HttpResponse() - response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) - return response +import warnings +warnings.warn(__doc__, PendingDeprecationWarning, stacklevel=2) + +from django.contrib.admindocs.middleware import XViewMiddleware diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 394d078e5b..4af94bdcf6 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -31,7 +31,7 @@ the following: * **Optional:** Linking to templates requires the :setting:`ADMIN_FOR` setting to be configured. * **Optional:** Using the admindocs bookmarklets requires the - :mod:`XViewMiddleware` to be installed. + :mod:`XViewMiddleware` to be installed. Once those steps are complete, you can start browsing the documentation by going to your admin interface and clicking the "Documentation" link in the @@ -156,7 +156,7 @@ Edit this object Using these bookmarklets requires that you are either logged into the :mod:`Django admin ` as a :class:`~django.contrib.auth.models.User` with -:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or -that the :mod:`django.middleware.doc` middleware and -:mod:`XViewMiddleware ` are installed and you -are accessing the site from an IP address listed in :setting:`INTERNAL_IPS`. +:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or that the +:mod:`XViewMiddleware ` is installed and +you are accessing the site from an IP address listed in +:setting:`INTERNAL_IPS`. diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 03885a2215..4898bab636 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -71,19 +71,6 @@ Adds a few conveniences for perfectionists: * Sends broken link notification emails to :setting:`MANAGERS` (see :doc:`/howto/error-reporting`). -View metadata middleware ------------------------- - -.. module:: django.middleware.doc - :synopsis: Middleware to help your app self-document. - -.. class:: XViewMiddleware - -Sends custom ``X-View`` HTTP headers to HEAD requests that come from IP -addresses defined in the :setting:`INTERNAL_IPS` setting. This is used by -Django's :doc:`automatic documentation system `. -Depends on :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`. - GZip middleware --------------- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 8ef59064f7..c1170e19c5 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1243,7 +1243,7 @@ Default: ``()`` (Empty tuple) A tuple of IP addresses, as strings, that: * See debug comments, when :setting:`DEBUG` is ``True`` -* Receive X headers if the ``XViewMiddleware`` is installed (see +* Receive X headers in admindocs if the ``XViewMiddleware`` is installed (see :doc:`/topics/http/middleware`) .. setting:: LANGUAGE_CODE diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index b4668c38d0..6643fb7d32 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -502,6 +502,10 @@ Miscellaneous ineffective so it has been removed, along with its generic implementation, previously available in ``django.core.xheaders``. +* The ``XViewMiddleware`` has been moved from ``django.middleware.doc`` to + ``django.contrib.admindocs.middleware`` because it is an implementation + detail of admindocs, proven not to be reusable in general. + Features deprecated in 1.6 ========================== diff --git a/tests/admin_docs/__init__.py b/tests/admin_docs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/admin_docs/fixtures/data.xml b/tests/admin_docs/fixtures/data.xml new file mode 100644 index 0000000000..aba8f4aace --- /dev/null +++ b/tests/admin_docs/fixtures/data.xml @@ -0,0 +1,17 @@ + + + + super + Super + User + super@example.com + sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 + True + True + True + 2007-05-30 13:20:10 + 2007-05-30 13:20:10 + + + + diff --git a/tests/admin_docs/models.py b/tests/admin_docs/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py new file mode 100644 index 0000000000..aeb527c7b9 --- /dev/null +++ b/tests/admin_docs/tests.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.utils import override_settings + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class XViewMiddlewareTest(TestCase): + fixtures = ['data.xml'] + urls = 'admin_docs.urls' + + def test_xview_func(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/func/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.xview') + user.is_staff = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + + def test_xview_class(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/class/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.XViewClass') + user.is_staff = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py new file mode 100644 index 0000000000..3c3a8fe5d8 --- /dev/null +++ b/tests/admin_docs/urls.py @@ -0,0 +1,11 @@ +# coding: utf-8 +from __future__ import absolute_import + +from django.conf.urls import patterns + +from . import views + +urlpatterns = patterns('', + (r'^xview/func/$', views.xview_dec(views.xview)), + (r'^xview/class/$', views.xview_dec(views.XViewClass.as_view())), +) diff --git a/tests/admin_docs/views.py b/tests/admin_docs/views.py new file mode 100644 index 0000000000..e47177c37f --- /dev/null +++ b/tests/admin_docs/views.py @@ -0,0 +1,13 @@ +from django.http import HttpResponse +from django.utils.decorators import decorator_from_middleware +from django.views.generic import View +from django.contrib.admindocs.middleware import XViewMiddleware + +xview_dec = decorator_from_middleware(XViewMiddleware) + +def xview(request): + return HttpResponse() + +class XViewClass(View): + def get(self, request): + return HttpResponse() From 60d94c2a80a68861021526c0fef7fc40e648e81f Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sun, 19 May 2013 13:28:09 +0200 Subject: [PATCH 070/249] Fixed #11442 -- Postgresql backend casts all inet types to text --- django/db/backends/__init__.py | 10 +++++----- django/db/backends/oracle/base.py | 2 +- django/db/backends/postgresql_psycopg2/operations.py | 4 ++-- django/db/models/sql/where.py | 8 +++++--- tests/string_lookup/tests.py | 4 +++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 9acef4ad19..6eb111270e 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -765,12 +765,12 @@ class BaseDatabaseOperations(object): """ return cursor.fetchone()[0] - def field_cast_sql(self, db_type): + def field_cast_sql(self, db_type, internal_type): """ - Given a column type (e.g. 'BLOB', 'VARCHAR'), returns the SQL necessary - to cast it before using it in a WHERE statement. Note that the - resulting string should contain a '%s' placeholder for the column being - searched against. + Given a column type (e.g. 'BLOB', 'VARCHAR'), and an internal type + (e.g. 'GenericIPAddressField'), returns the SQL necessary to cast it + before using it in a WHERE statement. Note that the resulting string + should contain a '%s' placeholder for the column being searched against. """ return '%s' diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 9e69743d33..6338ae09e7 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -254,7 +254,7 @@ WHEN (new.%(col_name)s IS NULL) def fetch_returned_insert_id(self, cursor): return int(cursor._insert_id_var.getvalue()) - def field_cast_sql(self, db_type): + def field_cast_sql(self, db_type, internal_type): if db_type and db_type.endswith('LOB'): return "DBMS_LOB.SUBSTR(%s)" else: diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index b17a0c17bb..f06eec5a1d 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -78,8 +78,8 @@ class DatabaseOperations(BaseDatabaseOperations): return lookup - def field_cast_sql(self, db_type): - if db_type == 'inet': + def field_cast_sql(self, db_type, internal_type): + if internal_type == "GenericIPAddressField" or internal_type == "IPAddressField": return 'HOST(%s)' return '%s' diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 029226383d..ff3d1bdcfb 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -174,6 +174,8 @@ class WhereNode(tree.Node): it. """ lvalue, lookup_type, value_annotation, params_or_value = child + field_internal_type = lvalue.field.get_internal_type() if lvalue.field else None + if isinstance(lvalue, Constraint): try: lvalue, params = lvalue.process(lookup_type, params_or_value, connection) @@ -187,7 +189,7 @@ class WhereNode(tree.Node): if isinstance(lvalue, tuple): # A direct database column lookup. - field_sql, field_params = self.sql_for_columns(lvalue, qn, connection), [] + field_sql, field_params = self.sql_for_columns(lvalue, qn, connection, field_internal_type), [] else: # A smart object with an as_sql() method. field_sql, field_params = lvalue.as_sql(qn, connection) @@ -257,7 +259,7 @@ class WhereNode(tree.Node): raise TypeError('Invalid lookup_type: %r' % lookup_type) - def sql_for_columns(self, data, qn, connection): + def sql_for_columns(self, data, qn, connection, internal_type=None): """ Returns the SQL fragment used for the left-hand side of a column constraint (for example, the "T1.foo" portion in the clause @@ -268,7 +270,7 @@ class WhereNode(tree.Node): lhs = '%s.%s' % (qn(table_alias), qn(name)) else: lhs = qn(name) - return connection.ops.field_cast_sql(db_type) % lhs + return connection.ops.field_cast_sql(db_type, internal_type) % lhs def relabel_aliases(self, change_map): """ diff --git a/tests/string_lookup/tests.py b/tests/string_lookup/tests.py index 02f766adce..b011720ddf 100644 --- a/tests/string_lookup/tests.py +++ b/tests/string_lookup/tests.py @@ -73,9 +73,11 @@ class StringLookupTests(TestCase): """ Regression test for #708 - "like" queries on IP address fields require casting to text (on PostgreSQL). + "like" queries on IP address fields require casting with HOST() (on PostgreSQL). """ a = Article(name='IP test', text='The body', submitted_from='192.0.2.100') a.save() self.assertEqual(repr(Article.objects.filter(submitted_from__contains='192.0.2')), repr([a])) + # Test that the searches do not match the subnet mask (/32 in this case) + self.assertEqual(Article.objects.filter(submitted_from__contains='32').count(), 0) \ No newline at end of file From 7264e5c66110b6748b1d40ee3b0d511c71f3232f Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sun, 19 May 2013 11:44:34 +0200 Subject: [PATCH 071/249] Add missing imports and models to the examples in the template layer documentation --- docs/howto/custom-template-tags.txt | 46 ++++++++++++++++++++--------- docs/ref/templates/api.txt | 7 +++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 0d35654a04..f334c0f418 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -300,18 +300,21 @@ Template filter code falls into one of two situations: .. code-block:: python - from django.utils.html import conditional_escape - from django.utils.safestring import mark_safe + from django import template + from django.utils.html import conditional_escape + from django.utils.safestring import mark_safe - @register.filter(needs_autoescape=True) - def initial_letter_filter(text, autoescape=None): - first, other = text[0], text[1:] - if autoescape: - esc = conditional_escape - else: - esc = lambda x: x - result = '%s%s' % (esc(first), esc(other)) - return mark_safe(result) + register = template.Library() + + @register.filter(needs_autoescape=True) + def initial_letter_filter(text, autoescape=None): + first, other = text[0], text[1:] + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + result = '%s%s' % (esc(first), esc(other)) + return mark_safe(result) The ``needs_autoescape`` flag and the ``autoescape`` keyword argument mean that our function will know whether automatic escaping is in effect when the @@ -454,8 +457,9 @@ Continuing the above example, we need to define ``CurrentTimeNode``: .. code-block:: python - from django import template import datetime + from django import template + class CurrentTimeNode(template.Node): def __init__(self, format_string): self.format_string = format_string @@ -498,6 +502,8 @@ The ``__init__`` method for the ``Context`` class takes a parameter called .. code-block:: python + from django.template import Context + def render(self, context): # ... new_context = Context({'var': obj}, autoescape=context.autoescape) @@ -545,7 +551,10 @@ A naive implementation of ``CycleNode`` might look something like this: .. code-block:: python - class CycleNode(Node): + import itertools + from django import template + + class CycleNode(template.Node): def __init__(self, cyclevars): self.cycle_iter = itertools.cycle(cyclevars) def render(self, context): @@ -576,7 +585,7 @@ Let's refactor our ``CycleNode`` implementation to use the ``render_context``: .. code-block:: python - class CycleNode(Node): + class CycleNode(template.Node): def __init__(self, cyclevars): self.cyclevars = cyclevars def render(self, context): @@ -664,6 +673,7 @@ Now your tag should begin to look like this: .. code-block:: python from django import template + def do_format_time(parser, token): try: # split_contents() knows not to split quoted strings. @@ -722,6 +732,11 @@ Our earlier ``current_time`` function could thus be written like this: .. code-block:: python + import datetime + from django import template + + register = template.Library() + def current_time(format_string): return datetime.datetime.now().strftime(format_string) @@ -965,6 +980,9 @@ outputting it: .. code-block:: python + import datetime + from django import template + class CurrentTimeNode2(template.Node): def __init__(self, format_string): self.format_string = format_string diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 677aa13cbb..160cdc7194 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -286,6 +286,7 @@ fully-populated dictionary to ``Context()``. But you can add and delete items from a ``Context`` object once it's been instantiated, too, using standard dictionary syntax:: + >>> from django.template import Context >>> c = Context({"foo": "bar"}) >>> c['foo'] 'bar' @@ -397,6 +398,9 @@ Also, you can give ``RequestContext`` a list of additional processors, using the optional, third positional argument, ``processors``. In this example, the ``RequestContext`` instance gets a ``ip_address`` variable:: + from django.http import HttpResponse + from django.template import RequestContext + def ip_address_processor(request): return {'ip_address': request.META['REMOTE_ADDR']} @@ -417,6 +421,9 @@ optional, third positional argument, ``processors``. In this example, the :func:`~django.shortcuts.render_to_response()`: a ``RequestContext`` instance. Your code might look like this:: + from django.shortcuts import render_to_response + from django.template import RequestContext + def some_view(request): # ... return render_to_response('my_template.html', From 6a479955f0dcb37e04593bda715fd5f13c1f1106 Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sun, 19 May 2013 12:22:40 +0200 Subject: [PATCH 072/249] Add missing imports and models to the examples in security documentation --- docs/ref/contrib/csrf.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 968ef0b07b..9e58548376 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -384,6 +384,7 @@ Utilities the middleware. Example:: from django.views.decorators.csrf import csrf_exempt + from django.http import HttpResponse @csrf_exempt def my_view(request): From 1d543949d7acc93a172e8a2c9272d8b983a421ef Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sun, 19 May 2013 12:42:44 +0200 Subject: [PATCH 073/249] Add missing imports and models to the examples in internationalization and localization documentation --- docs/topics/i18n/translation.txt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 72e000a86f..433d40b0bb 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -80,6 +80,7 @@ In this example, the text ``"Welcome to my site."`` is marked as a translation string:: from django.utils.translation import ugettext as _ + from django.http import HttpResponse def my_view(request): output = _("Welcome to my site.") @@ -89,6 +90,7 @@ Obviously, you could code this without using the alias. This example is identical to the previous one:: from django.utils.translation import ugettext + from django.http import HttpResponse def my_view(request): output = ugettext("Welcome to my site.") @@ -192,6 +194,7 @@ of its value.) For example:: from django.utils.translation import ungettext + from django.http import HttpResponse def hello_world(request, count): page = ungettext( @@ -208,6 +211,7 @@ languages as the ``count`` variable. Lets see a slightly more complex usage example:: from django.utils.translation import ungettext + from myapp.models import Report count = Report.objects.count() if count == 1: @@ -283,6 +287,7 @@ For example:: or:: + from django.db import models from django.utils.translation import pgettext_lazy class MyThing(models.Model): @@ -328,6 +333,7 @@ Model fields and relationships ``verbose_name`` and ``help_text`` option values For example, to translate the help text of the *name* field in the following model, do the following:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -336,8 +342,6 @@ model, do the following:: You can mark names of ``ForeignKey``, ``ManyTomanyField`` or ``OneToOneField`` relationship as translatable by using their ``verbose_name`` options:: - from django.utils.translation import ugettext_lazy as _ - class MyThing(models.Model): kind = models.ForeignKey(ThingKind, related_name='kinds', verbose_name=_('kind')) @@ -355,6 +359,7 @@ It is recommended to always provide explicit relying on the fallback English-centric and somewhat naïve determination of verbose names Django performs by looking at the model's class name:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -370,6 +375,7 @@ Model methods ``short_description`` attribute values For model methods, you can provide translations to Django and the admin site with the ``short_description`` attribute:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -404,6 +410,7 @@ If you ever see output that looks like ``"hello If you don't like the long ``ugettext_lazy`` name, you can just alias it as ``_`` (underscore), like so:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -429,6 +436,9 @@ definition. Therefore, you are authorized to pass a key name instead of an integer as the ``number`` argument. Then ``number`` will be looked up in the dictionary under that key during string interpolation. Here's example:: + from django import forms + from django.utils.translation import ugettext_lazy + class MyForm(forms.Form): error_message = ungettext_lazy("You only provided %(num)d argument", "You only provided %(num)d arguments", 'num') @@ -461,6 +471,7 @@ that concatenates its contents *and* converts them to strings only when the result is included in a string. For example:: from django.utils.translation import string_concat + from django.utils.translation import ugettext_lazy ... name = ugettext_lazy('John Lennon') instrument = ugettext_lazy('guitar') @@ -1663,6 +1674,8 @@ preference available as ``request.LANGUAGE_CODE`` for each :class:`~django.http.HttpRequest`. Feel free to read this value in your view code. Here's a simple example:: + from django.http import HttpResponse + def hello_world(request, count): if request.LANGUAGE_CODE == 'de-at': return HttpResponse("You prefer to read Austrian German.") From 08b501e7d314e9c45dd51d3ba27b2ecb0287df3b Mon Sep 17 00:00:00 2001 From: leandrafinger Date: Sun, 19 May 2013 11:15:35 +0200 Subject: [PATCH 074/249] add missing imports to the examples in the 'Forms' --- docs/ref/contrib/formtools/form-preview.txt | 1 + docs/ref/forms/api.txt | 101 ++++++++++---------- docs/ref/forms/fields.txt | 8 ++ docs/ref/forms/validation.txt | 9 ++ docs/ref/forms/widgets.txt | 3 + docs/topics/forms/formsets.txt | 26 +++++ docs/topics/forms/media.txt | 5 + docs/topics/forms/modelforms.txt | 31 ++++++ 8 files changed, 136 insertions(+), 48 deletions(-) diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt index 011e72c2e0..b86cc4dc90 100644 --- a/docs/ref/contrib/formtools/form-preview.txt +++ b/docs/ref/contrib/formtools/form-preview.txt @@ -53,6 +53,7 @@ How to use ``FormPreview`` overrides the ``done()`` method:: from django.contrib.formtools.preview import FormPreview + from django.http import HttpResponseRedirect from myapp.models import SomeModel class SomeModelFormPreview(FormPreview): diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 34ed2e493e..67e3aab712 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -154,6 +154,7 @@ you include ``initial`` when instantiating the ``Form``, then the latter at the field level and at the form instance level, and the latter gets precedence:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(initial='class') ... url = forms.URLField() @@ -238,6 +239,7 @@ When the ``Form`` is valid, ``cleaned_data`` will include a key and value for fields. In this example, the data dictionary doesn't include a value for the ``nick_name`` field, but ``cleaned_data`` includes it, with an empty value:: + >>> from django.forms import Form >>> class OptionalPersonForm(Form): ... first_name = CharField() ... last_name = CharField() @@ -327,54 +329,54 @@ a form object, and each rendering method returns a Unicode object. .. method:: Form.as_p - ``as_p()`` renders the form as a series of ``

`` tags, with each ``

`` - containing one field:: +``as_p()`` renders the form as a series of ``

`` tags, with each ``

`` +containing one field:: - >>> f = ContactForm() - >>> f.as_p() - u'

\n

\n

\n

' - >>> print(f.as_p()) -

-

-

-

+ >>> f = ContactForm() + >>> f.as_p() + u'

\n

\n

\n

' + >>> print(f.as_p()) +

+

+

+

``as_ul()`` ~~~~~~~~~~~ .. method:: Form.as_ul - ``as_ul()`` renders the form as a series of ``
  • `` tags, with each - ``
  • `` containing one field. It does *not* include the ``
      `` or - ``
    ``, so that you can specify any HTML attributes on the ``
      `` for - flexibility:: +``as_ul()`` renders the form as a series of ``
    • `` tags, with each +``
    • `` containing one field. It does *not* include the ``
        `` or +``
      ``, so that you can specify any HTML attributes on the ``
        `` for +flexibility:: - >>> f = ContactForm() - >>> f.as_ul() - u'
      • \n
      • \n
      • \n
      • ' - >>> print(f.as_ul()) -
      • -
      • -
      • -
      • + >>> f = ContactForm() + >>> f.as_ul() + u'
      • \n
      • \n
      • \n
      • ' + >>> print(f.as_ul()) +
      • +
      • +
      • +
      • ``as_table()`` ~~~~~~~~~~~~~~ .. method:: Form.as_table - Finally, ``as_table()`` outputs the form as an HTML ``
  • Request Method:Request URL:Exception Type:
    ``. This is - exactly the same as ``print``. In fact, when you ``print`` a form object, - it calls its ``as_table()`` method behind the scenes:: +Finally, ``as_table()`` outputs the form as an HTML ``
    ``. This is +exactly the same as ``print``. In fact, when you ``print`` a form object, +it calls its ``as_table()`` method behind the scenes:: - >>> f = ContactForm() - >>> f.as_table() - u'\n\n\n' - >>> print(f.as_table()) - - - - + >>> f = ContactForm() + >>> f.as_table() + u'\n\n\n' + >>> print(f.as_table()) + + + + Styling required or erroneous form rows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -391,6 +393,8 @@ attributes to required rows or to rows with errors: simply set the :attr:`Form.error_css_class` and/or :attr:`Form.required_css_class` attributes:: + from django.forms import Form + class ContactForm(Form): error_css_class = 'error' required_css_class = 'required' @@ -621,23 +625,23 @@ For a field's list of errors, access the field's ``errors`` attribute. .. attribute:: BoundField.errors - A list-like object that is displayed as an HTML ``
    - - - + + + @@ -49,6 +49,6 @@
    FieldTypeDescription{% trans 'Field' %}{% trans 'Type' %}{% trans 'Description' %}
    -

    ‹ Back to Models Documentation

    +

    ‹ {% trans 'Back to Models Documentation' %}

    {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_index.html b/django/contrib/admindocs/templates/admin_doc/model_index.html index 7a8c69953e..d4cde8334f 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_index.html +++ b/django/contrib/admindocs/templates/admin_doc/model_index.html @@ -11,11 +11,11 @@ {% endblock %} -{% block title %}Models{% endblock %} +{% block title %}{% trans 'Models' %}{% endblock %} {% block content %} -

    Model documentation

    +

    {% trans 'Model documentation' %}

    {% regroup models by app_label as grouped_models %} @@ -40,7 +40,7 @@ {% block sidebar %} {% endblock %} -{% block title %}Template filters{% endblock %} +{% block title %}{% trans 'Template filters' %}{% endblock %} {% block content %} -

    Template filter documentation

    +

    {% trans 'Template filter documentation' %}

    {% regroup filters|dictsort:"library" by library as filter_libraries %} {% for library in filter_libraries %}
    -

    {% firstof library.grouper "Built-in filters" %}

    - {% if library.grouper %}

    To use these filters, put {% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %} in your template before using the filter.


    {% endif %} +

    {% firstof library.grouper _("Built-in filters") %}

    + {% if library.grouper %}

    {% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these filters, put {{ code }} in your template before using the filter.{% endblocktrans %}


    {% endif %} {% for filter in library.list|dictsort:"name" %}

    {{ filter.name }}

    {{ filter.title }} @@ -40,7 +40,7 @@ {% regroup filters|dictsort:"library" by library as filter_libraries %} {% for library in filter_libraries %}
    -

    {% firstof library.grouper "Built-in filters" %}

    +

    {% firstof library.grouper _("Built-in filters") %}

      {% for filter in library.list|dictsort:"name" %}
    • {{ filter.name }}
    • diff --git a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html index c0fb243a99..a3c6eaadf4 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html @@ -9,18 +9,18 @@ › {% trans 'Tags' %}
    {% endblock %} -{% block title %}Template tags{% endblock %} +{% block title %}{% trans 'Template tags' %}{% endblock %} {% block content %} -

    Template tag documentation

    +

    {% trans 'Template tag documentation' %}

    {% regroup tags|dictsort:"library" by library as tag_libraries %} {% for library in tag_libraries %}
    -

    {% firstof library.grouper "Built-in tags" %}

    - {% if library.grouper %}

    To use these tags, put {% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %} in your template before using the tag.


    {% endif %} +

    {% firstof library.grouper _("Built-in tags") %}

    + {% if library.grouper %}

    {% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these tags, put {{ code }} in your template before using the tag.{% endblocktrans %}


    {% endif %} {% for tag in library.list|dictsort:"name" %}

    {{ tag.name }}

    {{ tag.title|striptags }}

    @@ -40,7 +40,7 @@ {% regroup tags|dictsort:"library" by library as tag_libraries %} {% for library in tag_libraries %}
    -

    {% firstof library.grouper "Built-in tags" %}

    +

    {% firstof library.grouper _("Built-in tags") %}

      {% for tag in library.list|dictsort:"name" %}
    • {{ tag.name }}
    • diff --git a/django/contrib/admindocs/templates/admin_doc/view_detail.html b/django/contrib/admindocs/templates/admin_doc/view_detail.html index efe5fed9ed..698f307843 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/view_detail.html @@ -9,7 +9,7 @@ › {{ name }}
    {% endblock %} -{% block title %}View: {{ name }}{% endblock %} +{% block title %}{% trans 'View: {{ name }}' %}{% endblock %} {% block content %} @@ -20,14 +20,14 @@ {{ body }} {% if meta.Context %} -

    Context:

    +

    {% trans 'Context:' %}

    {{ meta.Context }}

    {% endif %} {% if meta.Templates %} -

    Templates:

    +

    {% trans 'Templates:' %}

    {{ meta.Templates }}

    {% endif %} -

    ‹ Back to Views Documentation

    +

    ‹ {% trans 'Back to Views Documentation' %}

    {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/view_index.html b/django/contrib/admindocs/templates/admin_doc/view_index.html index 86342c6dd4..891eee7eec 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_index.html +++ b/django/contrib/admindocs/templates/admin_doc/view_index.html @@ -9,17 +9,17 @@ › {% trans 'Views' %}
    {% endblock %} -{% block title %}Views{% endblock %} +{% block title %}{% trans 'Views' %}{% endblock %} {% block content %} -

    View documentation

    +

    {% trans 'View documentation' %}

    {% regroup views|dictsort:"site_id" by site as views_by_site %}