diff --git a/AUTHORS b/AUTHORS index 6ca8b297f4..5fa957885c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -373,6 +373,7 @@ answer newbie questions, and generally made Django that much better: michael.mcewan@gmail.com Paul McLanahan Tobias McNulty + Andrews Medina Zain Memon Christian Metts michal@plovarna.cz @@ -467,6 +468,7 @@ answer newbie questions, and generally made Django that much better: Vinay Sajip Bartolome Sanchez Salado Kadesarin Sanjek + Tim Saylor Massimo Scamarcia Paulo Scardine David Schein diff --git a/MANIFEST.in b/MANIFEST.in index 2dde740b06..185e57646a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README +include README.rst include AUTHORS include INSTALL include LICENSE diff --git a/django/bin/django-2to3.py b/django/bin/django-2to3.py new file mode 100755 index 0000000000..35566abb94 --- /dev/null +++ b/django/bin/django-2to3.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +# This works exactly like 2to3, except that it uses Django's fixers rather +# than 2to3's built-in fixers. + +import sys +from lib2to3.main import main + +sys.exit(main("django.utils.2to3_fixes")) + diff --git a/django/conf/__init__.py b/django/conf/__init__.py index e1c3fd1808..f4d17ca9f3 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -152,17 +152,25 @@ class UserSettingsHolder(BaseSettings): Requests for configuration variables not in this class are satisfied from the module specified in default_settings (if possible). """ + self.__dict__['_deleted'] = set() self.default_settings = default_settings def __getattr__(self, name): + if name in self._deleted: + raise AttributeError return getattr(self.default_settings, name) + def __setattr__(self, name, value): + self._deleted.discard(name) + return super(UserSettingsHolder, self).__setattr__(name, value) + + def __delattr__(self, name): + self._deleted.add(name) + return super(UserSettingsHolder, self).__delattr__(name) + def __dir__(self): return list(self.__dict__) + dir(self.default_settings) - # For Python < 2.6: - __members__ = property(lambda self: self.__dir__()) - settings = LazySettings() diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py index 99590d6fd5..6bdaa34988 100644 --- a/django/conf/project_template/project_name/settings.py +++ b/django/conf/project_template/project_name/settings.py @@ -13,10 +13,11 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 'NAME': '', # Or path to database file if using sqlite3. - 'USER': '', # Not used with sqlite3. - 'PASSWORD': '', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. + # The following settings are not used with sqlite3: + 'USER': '', + 'PASSWORD': '', + 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. + 'PORT': '', # Set to empty string for default. } } diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index e31c6d84ed..2b12edd4e2 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.contrib.admin.util import quote from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text +from django.utils.encoding import python_2_unicode_compatible ADDITION = 1 CHANGE = 2 @@ -16,6 +17,7 @@ class LogEntryManager(models.Manager): e = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message) e.save() +@python_2_unicode_compatible class LogEntry(models.Model): action_time = models.DateTimeField(_('action time'), auto_now=True) user = models.ForeignKey(User) @@ -36,7 +38,7 @@ class LogEntry(models.Model): def __repr__(self): return smart_text(self.action_time) - def __unicode__(self): + def __str__(self): if self.action_flag == ADDITION: return _('Added "%(object)s".') % {'object': self.object_repr} elif self.action_flag == CHANGE: diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 82d7296c85..e27875cdad 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -29,7 +29,7 @@ {% if change %}{% if not is_popup %} diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index 888b5ed9c3..463e0792f0 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -17,7 +17,7 @@ class AdminLogNode(template.Node): user_id = self.user if not user_id.isdigit(): user_id = context[self.user].id - context[self.varname] = LogEntry.objects.filter(user__id__exact=user_id).select_related('content_type', 'user')[:self.limit] + context[self.varname] = LogEntry.objects.filter(user__id__exact=user_id).select_related('content_type', 'user')[:int(self.limit)] return '' @register.tag diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index ff90e1d007..889f692ac3 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -12,7 +12,7 @@ from django.utils import formats from django.utils.html import format_html from django.utils.text import capfirst from django.utils import timezone -from django.utils.encoding import force_text, smart_text, smart_bytes +from django.utils.encoding import force_str, force_text, smart_text from django.utils import six from django.utils.translation import ungettext from django.core.urlresolvers import reverse @@ -277,7 +277,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False): label = force_text(model._meta.verbose_name) attr = six.text_type elif name == "__str__": - label = smart_bytes(model._meta.verbose_name) + label = force_str(model._meta.verbose_name) attr = bytes else: if callable(name): diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 3eabf3dbeb..74ef095b4b 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -6,7 +6,7 @@ from django.core.paginator import InvalidPage from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils.datastructures import SortedDict -from django.utils.encoding import force_text, smart_bytes +from django.utils.encoding import force_str, force_text from django.utils.translation import ugettext, ugettext_lazy from django.utils.http import urlencode @@ -94,7 +94,7 @@ class ChangeList(object): # 'key' will be used as a keyword argument later, so Python # requires it to be a string. del lookup_params[key] - lookup_params[smart_bytes(key)] = value + lookup_params[force_str(key)] = value if not self.model_admin.lookup_allowed(key, value): raise SuspiciousOperation("Filtering by %s not allowed" % key) @@ -148,7 +148,7 @@ class ChangeList(object): if remove is None: remove = [] p = self.params.copy() for r in remove: - for k in p.keys(): + for k in list(p): if k.startswith(r): del p[k] for k, v in new_params.items(): diff --git a/django/contrib/admindocs/utils.py b/django/contrib/admindocs/utils.py index 0e10eb4fa3..9be0093dfc 100644 --- a/django/contrib/admindocs/utils.py +++ b/django/contrib/admindocs/utils.py @@ -6,7 +6,7 @@ from email.errors import HeaderParseError from django.utils.safestring import mark_safe from django.core.urlresolvers import reverse -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_bytes try: import docutils.core import docutils.nodes @@ -66,7 +66,7 @@ def parse_rst(text, default_reference_context, thing_being_parsed=None): "link_base" : reverse('django-admindocs-docroot').rstrip('/') } if thing_being_parsed: - thing_being_parsed = smart_bytes("<%s>" % thing_being_parsed) + thing_being_parsed = force_bytes("<%s>" % thing_being_parsed) parts = docutils.core.publish_parts(text, source_path=thing_being_parsed, destination_path=None, writer_name='html', settings_overrides=overrides) diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 7a608d0777..beeb284998 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME from django.core.exceptions import PermissionDenied from django.utils.decorators import available_attrs +from django.utils.encoding import force_str def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): @@ -22,9 +23,11 @@ def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIE if test_func(request.user): return view_func(request, *args, **kwargs) path = request.build_absolute_uri() + # urlparse chokes on lazy objects in Python 3 + login_url_as_str = force_str(login_url or settings.LOGIN_URL) # If the login url is the same scheme and net location then just # use the path as the "next" url. - login_scheme, login_netloc = urlparse(login_url or settings.LOGIN_URL)[:2] + login_scheme, login_netloc = urlparse(login_url_as_str)[:2] current_scheme, current_netloc = urlparse(path)[:2] if ((not login_scheme or login_scheme == current_scheme) and (not login_netloc or login_netloc == current_netloc)): diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index dfd039f018..75b3ca4ece 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -63,16 +63,16 @@ class UserCreationForm(forms.ModelForm): } username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^[\w.@+-]+$', - help_text = _("Required. 30 characters or fewer. Letters, digits and " + help_text=_("Required. 30 characters or fewer. Letters, digits and " "@/./+/-/_ only."), - error_messages = { + error_messages={ 'invalid': _("This value may contain only letters, numbers and " "@/./+/-/_ characters.")}) password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput, - help_text = _("Enter the same password as above, for verification.")) + help_text=_("Enter the same password as above, for verification.")) class Meta: model = User @@ -107,9 +107,9 @@ class UserCreationForm(forms.ModelForm): class UserChangeForm(forms.ModelForm): username = forms.RegexField( label=_("Username"), max_length=30, regex=r"^[\w.@+-]+$", - help_text = _("Required. 30 characters or fewer. Letters, digits and " + help_text=_("Required. 30 characters or fewer. Letters, digits and " "@/./+/-/_ only."), - error_messages = { + error_messages={ 'invalid': _("This value may contain only letters, numbers and " "@/./+/-/_ characters.")}) password = ReadOnlyPasswordHashField(label=_("Password"), diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 45c1f88ab2..bd0c6778c9 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -8,7 +8,7 @@ from django.conf import settings from django.test.signals import setting_changed from django.utils import importlib from django.utils.datastructures import SortedDict -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_bytes from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import ( pbkdf2, constant_time_compare, get_random_string) @@ -219,7 +219,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher): if not iterations: iterations = self.iterations hash = pbkdf2(password, salt, iterations, digest=self.digest) - hash = base64.b64encode(hash).strip() + hash = base64.b64encode(hash).decode('ascii').strip() return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) def verify(self, password, encoded): @@ -299,7 +299,7 @@ class SHA1PasswordHasher(BasePasswordHasher): def encode(self, password, salt): assert password assert salt and '$' not in salt - hash = hashlib.sha1(smart_bytes(salt + password)).hexdigest() + hash = hashlib.sha1(force_bytes(salt + password)).hexdigest() return "%s$%s$%s" % (self.algorithm, salt, hash) def verify(self, password, encoded): @@ -327,7 +327,7 @@ class MD5PasswordHasher(BasePasswordHasher): def encode(self, password, salt): assert password assert salt and '$' not in salt - hash = hashlib.md5(smart_bytes(salt + password)).hexdigest() + hash = hashlib.md5(force_bytes(salt + password)).hexdigest() return "%s$%s$%s" % (self.algorithm, salt, hash) def verify(self, password, encoded): @@ -361,7 +361,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher): return '' def encode(self, password, salt): - return hashlib.md5(smart_bytes(password)).hexdigest() + return hashlib.md5(force_bytes(password)).hexdigest() def verify(self, password, encoded): encoded_2 = self.encode(password, '') diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 7abd2abcf4..23a053d985 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -9,6 +9,7 @@ import unicodedata from django.contrib.auth import models as auth_app from django.db.models import get_models, signals from django.contrib.auth.models import User +from django.utils import six from django.utils.six.moves import input @@ -85,13 +86,22 @@ def get_system_username(): username could not be determined. """ try: - return getpass.getuser().decode(locale.getdefaultlocale()[1]) - except (ImportError, KeyError, UnicodeDecodeError): + result = getpass.getuser() + except (ImportError, KeyError): # KeyError will be raised by os.getpwuid() (called by getuser()) # if there is no corresponding entry in the /etc/passwd file # (a very restricted chroot environment, for example). - # UnicodeDecodeError - preventive treatment for non-latin Windows. return '' + if not six.PY3: + default_locale = locale.getdefaultlocale()[1] + if not default_locale: + return '' + try: + result = result.decode(default_locale) + except UnicodeDecodeError: + # UnicodeDecodeError - preventive treatment for non-latin Windows. + return '' + return result def get_default_username(check_db=True): @@ -108,7 +118,7 @@ def get_default_username(check_db=True): default_username = get_system_username() try: default_username = unicodedata.normalize('NFKD', default_username)\ - .encode('ascii', 'ignore').replace(' ', '').lower() + .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower() except UnicodeDecodeError: return '' if not RE_VALID_USERNAME.match(default_username): diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 1099aa195b..1c21917a8c 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -16,6 +16,7 @@ from django.contrib.auth.hashers import ( check_password, make_password, is_password_usable, UNUSABLE_PASSWORD) from django.contrib.auth.signals import user_logged_in from django.contrib.contenttypes.models import ContentType +from django.utils.encoding import python_2_unicode_compatible def update_last_login(sender, user, **kwargs): @@ -41,6 +42,7 @@ class PermissionManager(models.Manager): ) +@python_2_unicode_compatible class Permission(models.Model): """ The permissions system provides a way to assign permissions to specific @@ -76,7 +78,7 @@ class Permission(models.Model): ordering = ('content_type__app_label', 'content_type__model', 'codename') - def __unicode__(self): + def __str__(self): return "%s | %s | %s" % ( six.text_type(self.content_type.app_label), six.text_type(self.content_type), @@ -94,6 +96,7 @@ class GroupManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) +@python_2_unicode_compatible class Group(models.Model): """ Groups are a generic way of categorizing users to apply permissions, or @@ -121,7 +124,7 @@ class Group(models.Model): verbose_name = _('group') verbose_name_plural = _('groups') - def __unicode__(self): + def __str__(self): return self.name def natural_key(self): @@ -221,6 +224,7 @@ def _user_has_module_perms(user, app_label): return False +@python_2_unicode_compatible class User(models.Model): """ Users within the Django authentication system are represented by this @@ -259,7 +263,7 @@ class User(models.Model): verbose_name = _('user') verbose_name_plural = _('users') - def __unicode__(self): + def __str__(self): return self.username def natural_key(self): @@ -403,6 +407,7 @@ class User(models.Model): return self._profile_cache +@python_2_unicode_compatible class AnonymousUser(object): id = None pk = None @@ -416,11 +421,8 @@ class AnonymousUser(object): def __init__(self): pass - def __unicode__(self): - return 'AnonymousUser' - def __str__(self): - return six.text_type(self).encode('utf-8') + return 'AnonymousUser' def __eq__(self, other): return isinstance(other, self.__class__) diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 21acb2004f..710754b8f1 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -1,13 +1,11 @@ +import locale +import traceback + +from django.contrib.auth.management.commands import createsuperuser from django.contrib.auth.models import User, AnonymousUser from django.core.management import call_command from django.test import TestCase from django.utils.six import StringIO -from django.utils.unittest import skipUnless - -try: - import crypt as crypt_module -except ImportError: - crypt_module = None class BasicTestCase(TestCase): @@ -111,3 +109,37 @@ class BasicTestCase(TestCase): u = User.objects.get(username="joe+admin@somewhere.org") self.assertEqual(u.email, 'joe@somewhere.org') self.assertFalse(u.has_usable_password()) + + def test_createsuperuser_nolocale(self): + """ + Check that createsuperuser does not break when no locale is set. See + ticket #16017. + """ + + old_getdefaultlocale = locale.getdefaultlocale + old_getpass = createsuperuser.getpass + try: + # Temporarily remove locale information + locale.getdefaultlocale = lambda: (None, None) + + # Temporarily replace getpass to allow interactive code to be used + # non-interactively + class mock_getpass: pass + mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd") + createsuperuser.getpass = mock_getpass + + # Call the command in this new environment + new_io = StringIO() + call_command("createsuperuser", interactive=True, username="nolocale@somewhere.org", email="nolocale@somewhere.org", stdout=new_io) + + except TypeError as e: + self.fail("createsuperuser fails if the OS provides no information about the current locale") + + finally: + # Re-apply locale and getpass information + createsuperuser.getpass = old_getpass + locale.getdefaultlocale = old_getdefaultlocale + + # If we were successful, a user should have been created + u = User.objects.get(username="nolocale@somewhere.org") + self.assertEqual(u.email, 'nolocale@somewhere.org') diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index c98b7491c8..ac83086dc3 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -4,16 +4,20 @@ from django.contrib.auth import models, management from django.contrib.auth.management.commands import changepassword from django.core.management.base import CommandError from django.test import TestCase +from django.utils import six from django.utils.six import StringIO class GetDefaultUsernameTestCase(TestCase): def setUp(self): - self._getpass_getuser = management.get_system_username + self.old_get_system_username = management.get_system_username def tearDown(self): - management.get_system_username = self._getpass_getuser + management.get_system_username = self.old_get_system_username + + def test_actual_implementation(self): + self.assertIsInstance(management.get_system_username(), six.text_type) def test_simple(self): management.get_system_username = lambda: 'joe' diff --git a/django/contrib/auth/tests/tokens.py b/django/contrib/auth/tests/tokens.py index beccfc5d07..44117a4f84 100644 --- a/django/contrib/auth/tests/tokens.py +++ b/django/contrib/auth/tests/tokens.py @@ -1,9 +1,11 @@ +import sys from datetime import date, timedelta from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.test import TestCase +from django.utils import unittest class TokenGeneratorTest(TestCase): @@ -51,6 +53,7 @@ class TokenGeneratorTest(TestCase): p2 = Mocked(date.today() + timedelta(settings.PASSWORD_RESET_TIMEOUT_DAYS + 1)) self.assertFalse(p2.check_token(user, tk1)) + @unittest.skipIf(sys.version_info[:2] >= (3, 0), "Unnecessary test with Python 3") def test_date_length(self): """ Make sure we don't allow overly long dates, causing a potential DoS. diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index ccfc7a1003..f93541b4bf 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict from django.template.response import TemplateResponse +from django.utils.encoding import force_str from django.utils.http import base36_to_int from django.utils.translation import ugettext as _ from django.views.decorators.debug import sensitive_post_parameters @@ -116,10 +117,10 @@ def redirect_to_login(next, login_url=None, """ Redirects the user to the login page, passing the given 'next' page """ - if not login_url: - login_url = settings.LOGIN_URL + # urlparse chokes on lazy objects in Python 3 + login_url_as_str = force_str(login_url or settings.LOGIN_URL) - login_url_parts = list(urlparse(login_url)) + login_url_parts = list(urlparse(login_url_as_str)) if redirect_field_name: querystring = QueryDict(login_url_parts[4], mutable=True) querystring[redirect_field_name] = next @@ -200,7 +201,7 @@ def password_reset_confirm(request, uidb36=None, token=None, try: uid_int = base36_to_int(uidb36) user = User.objects.get(id=uid_int) - except (ValueError, User.DoesNotExist): + except (ValueError, OverflowError, User.DoesNotExist): user = None if user is not None and token_generator.check_token(user, token): diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index 475b3c8dea..b043b4187a 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -8,6 +8,7 @@ from django.core import urlresolvers from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.conf import settings +from django.utils.encoding import python_2_unicode_compatible COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) @@ -39,6 +40,7 @@ class BaseCommentAbstractModel(models.Model): args=(self.content_type_id, self.object_pk) ) +@python_2_unicode_compatible class Comment(BaseCommentAbstractModel): """ A user comment about some object. @@ -76,7 +78,7 @@ class Comment(BaseCommentAbstractModel): verbose_name = _('comment') verbose_name_plural = _('comments') - def __unicode__(self): + def __str__(self): return "%s: %s..." % (self.name, self.comment[:50]) def save(self, *args, **kwargs): @@ -153,6 +155,7 @@ class Comment(BaseCommentAbstractModel): } return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d +@python_2_unicode_compatible class CommentFlag(models.Model): """ Records a flag on a comment. This is intentionally flexible; right now, a @@ -182,7 +185,7 @@ class CommentFlag(models.Model): verbose_name = _('comment flag') verbose_name_plural = _('comment flags') - def __unicode__(self): + def __str__(self): return "%s flag of comment ID %s by %s" % \ (self.flag, self.comment_id, self.user.username) diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index e6d547a491..b658655bbb 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -1,6 +1,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text, force_text +from django.utils.encoding import python_2_unicode_compatible class ContentTypeManager(models.Manager): @@ -122,6 +123,7 @@ class ContentTypeManager(models.Manager): 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) app_label = models.CharField(max_length=100) @@ -135,7 +137,7 @@ class ContentType(models.Model): ordering = ('name',) unique_together = (('app_label', 'model'),) - def __unicode__(self): + def __str__(self): # self.name is deprecated in favor of using model's verbose_name, which # can be translated. Formal deprecation is delayed until we have DB # migration to be able to remove the field from the database along with diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index cfd7e6ff32..2f92a34581 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -8,6 +8,7 @@ from django.http import HttpRequest, Http404 from django.test import TestCase from django.utils.http import urlquote from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class ConcreteModel(models.Model): @@ -17,13 +18,14 @@ class ProxyModel(ConcreteModel): class Meta: proxy = True +@python_2_unicode_compatible class FooWithoutUrl(models.Model): """ Fake model not defining ``get_absolute_url`` for :meth:`ContentTypesTests.test_shortcut_view_without_get_absolute_url`""" name = models.CharField(max_length=30, unique=True) - def __unicode__(self): + def __str__(self): return self.name diff --git a/django/contrib/databrowse/datastructures.py b/django/contrib/databrowse/datastructures.py index 810e039894..5f5f46f0d1 100644 --- a/django/contrib/databrowse/datastructures.py +++ b/django/contrib/databrowse/datastructures.py @@ -7,8 +7,9 @@ from __future__ import unicode_literals from django.db import models from django.utils import formats from django.utils.text import capfirst -from django.utils.encoding import smart_text, smart_bytes, iri_to_uri +from django.utils.encoding import smart_text, force_str, iri_to_uri from django.db.models.query import QuerySet +from django.utils.encoding import python_2_unicode_compatible EMPTY_VALUE = '(None)' DISPLAY_SIZE = 100 @@ -22,7 +23,7 @@ class EasyModel(object): self.verbose_name_plural = model._meta.verbose_name_plural def __repr__(self): - return '' % smart_bytes(self.model._meta.object_name) + return force_str('' % self.model._meta.object_name) def model_databrowse(self): "Returns the ModelDatabrowse class for this model." @@ -61,7 +62,7 @@ class EasyField(object): self.model, self.field = easy_model, field def __repr__(self): - return smart_bytes('' % (self.model.model._meta.object_name, self.field.name)) + return force_str('' % (self.model.model._meta.object_name, self.field.name)) def choices(self): for value, label in self.field.choices: @@ -79,27 +80,25 @@ class EasyChoice(object): self.value, self.label = value, label def __repr__(self): - return smart_bytes('' % (self.model.model._meta.object_name, self.field.name)) + return force_str('' % (self.model.model._meta.object_name, self.field.name)) def url(self): return '%s%s/%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.field.name, iri_to_uri(self.value)) +@python_2_unicode_compatible class EasyInstance(object): def __init__(self, easy_model, instance): self.model, self.instance = easy_model, instance def __repr__(self): - return smart_bytes('' % (self.model.model._meta.object_name, self.instance._get_pk_val())) + return force_str('' % (self.model.model._meta.object_name, self.instance._get_pk_val())) - def __unicode__(self): + def __str__(self): val = smart_text(self.instance) if len(val) > DISPLAY_SIZE: return val[:DISPLAY_SIZE] + '...' return val - def __str__(self): - return self.__unicode__().encode('utf-8') - def pk(self): return self.instance._get_pk_val() @@ -136,7 +135,7 @@ class EasyInstanceField(object): self.raw_value = getattr(instance.instance, field.name) def __repr__(self): - return smart_bytes('' % (self.model.model._meta.object_name, self.field.name)) + return force_str('' % (self.model.model._meta.object_name, self.field.name)) def values(self): """ diff --git a/django/contrib/databrowse/tests.py b/django/contrib/databrowse/tests.py index 149383cf72..d649b4af67 100644 --- a/django/contrib/databrowse/tests.py +++ b/django/contrib/databrowse/tests.py @@ -1,26 +1,30 @@ from django.contrib import databrowse from django.db import models from django.test import TestCase +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class SomeModel(models.Model): some_field = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.some_field +@python_2_unicode_compatible class SomeOtherModel(models.Model): some_other_field = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.some_other_field +@python_2_unicode_compatible class YetAnotherModel(models.Model): yet_another_field = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.yet_another_field diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py index 42ec155f34..3a5b4d6135 100644 --- a/django/contrib/flatpages/models.py +++ b/django/contrib/flatpages/models.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals from django.db import models from django.contrib.sites.models import Site from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class FlatPage(models.Model): url = models.CharField(_('URL'), max_length=100, db_index=True) title = models.CharField(_('title'), max_length=200) @@ -21,7 +23,7 @@ class FlatPage(models.Model): verbose_name_plural = _('flat pages') ordering = ('url',) - def __unicode__(self): + def __str__(self): return "%s -- %s" % (self.url, self.title) def get_absolute_url(self): diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index ee93479cbd..15941332ed 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals +import datetime import os +import pickle import re import warnings @@ -16,6 +19,7 @@ from django.contrib.formtools.tests.wizard import * from django.contrib.formtools.tests.forms import * success_string = "Done was called!" +success_string_encoded = success_string.encode() class TestFormPreview(preview.FormPreview): def get_context(self, request, form): @@ -78,7 +82,7 @@ class PreviewTests(TestCase): """ # Pass strings for form submittal and add stage variable to # show we previously saw first stage of the form. - self.test_data.update({'stage': 1}) + self.test_data.update({'stage': 1, 'date1': datetime.date(2006, 10, 25)}) response = self.client.post('/preview/', self.test_data) # Check to confirm stage is set to 2 in output form. stage = self.input % 2 @@ -96,13 +100,13 @@ class PreviewTests(TestCase): """ # Pass strings for form submittal and add stage variable to # show we previously saw first stage of the form. - self.test_data.update({'stage':2}) + self.test_data.update({'stage': 2, 'date1': datetime.date(2006, 10, 25)}) response = self.client.post('/preview/', self.test_data) - self.assertNotEqual(response.content, success_string) + self.assertNotEqual(response.content, success_string_encoded) hash = self.preview.security_hash(None, TestForm(self.test_data)) self.test_data.update({'hash': hash}) response = self.client.post('/preview/', self.test_data) - self.assertEqual(response.content, success_string) + self.assertEqual(response.content, success_string_encoded) def test_bool_submit(self): """ @@ -122,7 +126,7 @@ class PreviewTests(TestCase): self.test_data.update({'hash': hash, 'bool1': 'False'}) with warnings.catch_warnings(record=True): response = self.client.post('/preview/', self.test_data) - self.assertEqual(response.content, success_string) + self.assertEqual(response.content, success_string_encoded) def test_form_submit_good_hash(self): """ @@ -133,11 +137,11 @@ class PreviewTests(TestCase): # show we previously saw first stage of the form. self.test_data.update({'stage':2}) response = self.client.post('/preview/', self.test_data) - self.assertNotEqual(response.content, success_string) + self.assertNotEqual(response.content, success_string_encoded) hash = utils.form_hmac(TestForm(self.test_data)) self.test_data.update({'hash': hash}) response = self.client.post('/preview/', self.test_data) - self.assertEqual(response.content, success_string) + self.assertEqual(response.content, success_string_encoded) def test_form_submit_bad_hash(self): @@ -150,11 +154,11 @@ class PreviewTests(TestCase): self.test_data.update({'stage':2}) response = self.client.post('/preview/', self.test_data) self.assertEqual(response.status_code, 200) - self.assertNotEqual(response.content, success_string) + self.assertNotEqual(response.content, success_string_encoded) hash = utils.form_hmac(TestForm(self.test_data)) + "bad" self.test_data.update({'hash': hash}) response = self.client.post('/previewpreview/', self.test_data) - self.assertNotEqual(response.content, success_string) + self.assertNotEqual(response.content, success_string_encoded) class FormHmacTests(unittest.TestCase): @@ -165,8 +169,8 @@ class FormHmacTests(unittest.TestCase): leading/trailing whitespace so as to be friendly to broken browsers that submit it (usually in textareas). """ - f1 = HashTestForm({'name': 'joe', 'bio': 'Nothing notable.'}) - f2 = HashTestForm({'name': ' joe', 'bio': 'Nothing notable. '}) + f1 = HashTestForm({'name': 'joe', 'bio': 'Speaking español.'}) + f2 = HashTestForm({'name': ' joe', 'bio': 'Speaking español. '}) hash1 = utils.form_hmac(f1) hash2 = utils.form_hmac(f2) self.assertEqual(hash1, hash2) @@ -270,7 +274,10 @@ class WizardTests(TestCase): """ data = {"0-field": "test", "1-field": "test2", - "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "hash_0": { + 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", + }[pickle.HIGHEST_PROTOCOL], "wizard_step": "1"} response = self.client.post('/wizard1/', data) self.assertEqual(2, response.context['step0']) @@ -295,15 +302,24 @@ class WizardTests(TestCase): wizard = WizardWithProcessStep([WizardPageOneForm]) data = {"0-field": "test", "1-field": "test2", - "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "hash_0": { + 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", + }[pickle.HIGHEST_PROTOCOL], "wizard_step": "1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) data = {"0-field": "test", "1-field": "test2", - "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", - "hash_1": "1e6f6315da42e62f33a30640ec7e007ad3fbf1a1", + "hash_0": { + 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", + }[pickle.HIGHEST_PROTOCOL], + "hash_1": { + 2: "1e6f6315da42e62f33a30640ec7e007ad3fbf1a1", + 3: "c33142ef9d01b1beae238adf22c3c6c57328f51a", + }[pickle.HIGHEST_PROTOCOL], "wizard_step": "2"} self.assertRaises(http.Http404, wizard, DummyRequest(POST=data)) @@ -325,7 +341,10 @@ class WizardTests(TestCase): WizardPageThreeForm]) data = {"0-field": "test", "1-field": "test2", - "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "hash_0": { + 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", + }[pickle.HIGHEST_PROTOCOL], "wizard_step": "1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) @@ -349,7 +368,10 @@ class WizardTests(TestCase): data = {"0-field": "test", "1-field": "test2", - "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "hash_0": { + 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", + }[pickle.HIGHEST_PROTOCOL], "wizard_step": "1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) @@ -375,7 +397,10 @@ class WizardTests(TestCase): WizardPageThreeForm]) data = {"0-field": "test", "1-field": "test2", - "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "hash_0": { + 2: "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + 3: "9355d5dff22d49dbad58e46189982cec649f9f5b", + }[pickle.HIGHEST_PROTOCOL], "wizard_step": "1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) diff --git a/django/contrib/formtools/tests/forms.py b/django/contrib/formtools/tests/forms.py index 9f6f596d3c..1ed2ab48bb 100644 --- a/django/contrib/formtools/tests/forms.py +++ b/django/contrib/formtools/tests/forms.py @@ -21,6 +21,7 @@ class TestForm(forms.Form): field1 = forms.CharField() field1_ = forms.CharField() bool1 = forms.BooleanField(required=False) + date1 = forms.DateField(required=False) class HashTestForm(forms.Form): name = forms.CharField() diff --git a/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py b/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py index a860edd9e9..7529d89a2c 100644 --- a/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py +++ b/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py @@ -122,6 +122,7 @@ class NamedWizardTests(object): self.assertEqual(response.context['wizard']['steps'].current, 'form2') post_data = self.wizard_step_data[1] + post_data['form2-file1'].close() post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post( reverse(self.wizard_urlname, @@ -149,7 +150,9 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['form_list'] - self.assertEqual(all_data[1]['file1'].read(), open(__file__, 'rb').read()) + with open(__file__, 'rb') as f: + self.assertEqual(all_data[1]['file1'].read(), f.read()) + all_data[1]['file1'].close() del all_data[1]['file1'] self.assertEqual(all_data, [ {'name': 'Pony', 'thirsty': True, 'user': self.testuser}, @@ -182,9 +185,10 @@ class NamedWizardTests(object): response = self.client.get(step2_url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form2') - self.assertEqual( - response.context['wizard']['form'].files['form2-file1'].read(), - open(__file__, 'rb').read()) + with open(__file__, 'rb') as f: + self.assertEqual( + response.context['wizard']['form'].files['form2-file1'].read(), + f.read()) response = self.client.post( reverse(self.wizard_urlname, @@ -201,7 +205,9 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['all_cleaned_data'] - self.assertEqual(all_data['file1'].read(), open(__file__, 'rb').read()) + with open(__file__, 'rb') as f: + self.assertEqual(all_data['file1'].read(), f.read()) + all_data['file1'].close() del all_data['file1'] self.assertEqual( all_data, @@ -225,6 +231,7 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] + post_data['form2-file1'].close() post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post( reverse(self.wizard_urlname, diff --git a/django/contrib/formtools/tests/wizard/wizardtests/tests.py b/django/contrib/formtools/tests/wizard/wizardtests/tests.py index a35664b322..586bd59341 100644 --- a/django/contrib/formtools/tests/wizard/wizardtests/tests.py +++ b/django/contrib/formtools/tests/wizard/wizardtests/tests.py @@ -95,7 +95,9 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['form_list'] - self.assertEqual(all_data[1]['file1'].read(), open(__file__, 'rb').read()) + with open(__file__, 'rb') as f: + self.assertEqual(all_data[1]['file1'].read(), f.read()) + all_data[1]['file1'].close() del all_data[1]['file1'] self.assertEqual(all_data, [ {'name': 'Pony', 'thirsty': True, 'user': self.testuser}, @@ -112,8 +114,9 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__, 'rb') - response = self.client.post(self.wizard_url, post_data) + with open(__file__, 'rb') as post_file: + post_data['form2-file1'] = post_file + response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) response = self.client.post(self.wizard_url, self.wizard_step_data[2]) @@ -123,7 +126,9 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['all_cleaned_data'] - self.assertEqual(all_data['file1'].read(), open(__file__, 'rb').read()) + with open(__file__, 'rb') as f: + self.assertEqual(all_data['file1'].read(), f.read()) + all_data['file1'].close() del all_data['file1'] self.assertEqual(all_data, { 'name': 'Pony', 'thirsty': True, 'user': self.testuser, @@ -140,6 +145,7 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] + post_data['form2-file1'].close() post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) @@ -167,6 +173,7 @@ class WizardTests(object): self.assertEqual(response.context['wizard']['steps'].current, 'form2') post_data = self.wizard_step_data[1] + post_data['form2-file1'].close() post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) diff --git a/django/contrib/formtools/utils.py b/django/contrib/formtools/utils.py index 8763cded07..76277c6b49 100644 --- a/django/contrib/formtools/utils.py +++ b/django/contrib/formtools/utils.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + # Do not try cPickle here (see #18340) import pickle diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py index 05c9f6f121..aafc833484 100644 --- a/django/contrib/formtools/wizard/storage/base.py +++ b/django/contrib/formtools/wizard/storage/base.py @@ -1,6 +1,5 @@ from django.core.files.uploadedfile import UploadedFile from django.utils.datastructures import MultiValueDict -from django.utils.encoding import smart_bytes from django.utils.functional import lazy_property from django.utils import six @@ -74,8 +73,7 @@ class BaseStorage(object): files = {} for field, field_dict in six.iteritems(wizard_files): - field_dict = dict((smart_bytes(k), v) - for k, v in six.iteritems(field_dict)) + field_dict = field_dict.copy() tmp_name = field_dict.pop('tmp_name') files[field] = UploadedFile( file=self.file_storage.open(tmp_name), **field_dict) diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base.py index d9f3546cff..f7af420a8d 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base.py @@ -5,6 +5,7 @@ Base/mixin classes for the spatial backend database operations and the import re from django.contrib.gis import gdal from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class BaseSpatialOperations(object): """ @@ -131,6 +132,7 @@ class BaseSpatialOperations(object): def spatial_ref_sys(self): raise NotImplementedError +@python_2_unicode_compatible class SpatialRefSysMixin(object): """ The SpatialRefSysMixin is a class used by the database-dependent @@ -325,7 +327,7 @@ class SpatialRefSysMixin(object): radius, flattening = sphere_params return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening) - def __unicode__(self): + def __str__(self): """ Returns the string representation. If GDAL is installed, it will be 'pretty' OGC WKT. diff --git a/django/contrib/gis/db/backends/oracle/models.py b/django/contrib/gis/db/backends/oracle/models.py index ed29f7bb38..b7deb3a946 100644 --- a/django/contrib/gis/db/backends/oracle/models.py +++ b/django/contrib/gis/db/backends/oracle/models.py @@ -9,7 +9,9 @@ """ from django.contrib.gis.db import models from django.contrib.gis.db.backends.base import SpatialRefSysMixin +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class GeometryColumns(models.Model): "Maps to the Oracle USER_SDO_GEOM_METADATA table." table_name = models.CharField(max_length=32) @@ -36,7 +38,7 @@ class GeometryColumns(models.Model): """ return 'column_name' - def __unicode__(self): + def __str__(self): return '%s - %s (SRID: %s)' % (self.table_name, self.column_name, self.srid) class SpatialRefSys(models.Model, SpatialRefSysMixin): diff --git a/django/contrib/gis/db/backends/postgis/models.py b/django/contrib/gis/db/backends/postgis/models.py index a38598343c..e8052594c6 100644 --- a/django/contrib/gis/db/backends/postgis/models.py +++ b/django/contrib/gis/db/backends/postgis/models.py @@ -3,7 +3,9 @@ """ from django.db import models from django.contrib.gis.db.backends.base import SpatialRefSysMixin +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class GeometryColumns(models.Model): """ The 'geometry_columns' table from the PostGIS. See the PostGIS @@ -37,7 +39,7 @@ class GeometryColumns(models.Model): """ return 'f_geometry_column' - def __unicode__(self): + def __str__(self): return "%s.%s - %dD %s field (SRID: %d)" % \ (self.f_table_name, self.f_geometry_column, self.coord_dimension, self.type, self.srid) diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py index d0a5f82033..27332b9b57 100644 --- a/django/contrib/gis/db/backends/spatialite/creation.py +++ b/django/contrib/gis/db/backends/spatialite/creation.py @@ -30,6 +30,7 @@ class SpatiaLiteCreation(DatabaseCreation): self.connection.close() self.connection.settings_dict["NAME"] = test_database_name + self.connection.ops.confirm_spatial_components_versions() # Need to load the SpatiaLite initialization SQL before running `syncdb`. self.load_spatialite_sql() diff --git a/django/contrib/gis/db/backends/spatialite/models.py b/django/contrib/gis/db/backends/spatialite/models.py index 684c5d8fc7..b281f0bc62 100644 --- a/django/contrib/gis/db/backends/spatialite/models.py +++ b/django/contrib/gis/db/backends/spatialite/models.py @@ -3,7 +3,9 @@ """ from django.db import models from django.contrib.gis.db.backends.base import SpatialRefSysMixin +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class GeometryColumns(models.Model): """ The 'geometry_columns' table from SpatiaLite. @@ -35,7 +37,7 @@ class GeometryColumns(models.Model): """ return 'f_geometry_column' - def __unicode__(self): + def __str__(self): return "%s.%s - %dD %s field (SRID: %d)" % \ (self.f_table_name, self.f_geometry_column, self.coord_dimension, self.type, self.srid) diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 60fe0a8069..80f05ef076 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -113,6 +113,12 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): def __init__(self, connection): super(DatabaseOperations, self).__init__(connection) + # Creating the GIS terms dictionary. + gis_terms = ['isnull'] + gis_terms += self.geometry_functions.keys() + self.gis_terms = dict([(term, None) for term in gis_terms]) + + def confirm_spatial_components_versions(self): # Determine the version of the SpatiaLite library. try: vtup = self.spatialite_version_tuple() @@ -129,11 +135,6 @@ class SpatiaLiteOperations(DatabaseOperations, BaseSpatialOperations): 'SQL loaded on this database?' % (self.connection.settings_dict['NAME'], msg)) - # Creating the GIS terms dictionary. - gis_terms = ['isnull'] - gis_terms += list(self.geometry_functions) - self.gis_terms = dict([(term, None) for term in gis_terms]) - if version >= (2, 4, 0): # Spatialite 2.4.0-RC4 added AsGML and AsKML, however both # RC2 (shipped in popular Debian/Ubuntu packages) and RC4 diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index cc61dfa4d2..d87e151aea 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -1,16 +1,15 @@ from django.db import connections from django.db.models.query import QuerySet, ValuesQuerySet, ValuesListQuerySet -from django.utils import six from django.contrib.gis.db.models import aggregates from django.contrib.gis.db.models.fields import get_srid_info, PointField, LineStringField from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Area, Distance -from django.utils import six from django.utils import six + class GeoQuerySet(QuerySet): "The Geographic QuerySet." diff --git a/django/contrib/gis/gdal/tests/__init__.py b/django/contrib/gis/gdal/tests/__init__.py index eb72a38775..262d294a43 100644 --- a/django/contrib/gis/gdal/tests/__init__.py +++ b/django/contrib/gis/gdal/tests/__init__.py @@ -19,7 +19,8 @@ test_suites = [test_driver.suite(), def suite(): "Builds a test suite for the GDAL tests." s = TestSuite() - map(s.addTest, test_suites) + for test_suite in test_suites: + s.addTest(test_suite) return s def run(verbosity=1): diff --git a/django/contrib/gis/maps/google/overlays.py b/django/contrib/gis/maps/google/overlays.py index 28603ac422..b82d967da6 100644 --- a/django/contrib/gis/maps/google/overlays.py +++ b/django/contrib/gis/maps/google/overlays.py @@ -2,8 +2,10 @@ from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Poly from django.utils.functional import total_ordering from django.utils.safestring import mark_safe from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class GEvent(object): """ A Python wrapper for the Google GEvent object. @@ -48,10 +50,11 @@ class GEvent(object): self.event = event self.action = action - def __unicode__(self): + def __str__(self): "Returns the parameter part of a GEvent." return mark_safe('"%s", %s' %(self.event, self.action)) +@python_2_unicode_compatible class GOverlayBase(object): def __init__(self): self.events = [] @@ -64,7 +67,7 @@ class GOverlayBase(object): "Attaches a GEvent to the overlay object." self.events.append(event) - def __unicode__(self): + def __str__(self): "The string representation is the JavaScript API call." return mark_safe('%s(%s)' % (self.__class__.__name__, self.js_params)) diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index 8bcdba1b44..36e48f7d20 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -8,7 +8,6 @@ from django.core.paginator import EmptyPage, PageNotAnInteger from django.contrib.gis.db.models.fields import GeometryField from django.db import connections, DEFAULT_DB_ALIAS from django.db.models import get_model -from django.utils.encoding import smart_bytes from django.utils import six from django.utils.translation import ugettext as _ @@ -61,7 +60,7 @@ def sitemap(request, sitemaps, section=None): raise Http404(_("Page %s empty") % page) except PageNotAnInteger: raise Http404(_("No page '%s'") % page) - xml = smart_bytes(loader.render_to_string('gis/sitemaps/geo_sitemap.xml', {'urlset': urls})) + xml = loader.render_to_string('gis/sitemaps/geo_sitemap.xml', {'urlset': urls}) return HttpResponse(xml, content_type='application/xml') def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB_ALIAS): diff --git a/django/contrib/gis/tests/distapp/models.py b/django/contrib/gis/tests/distapp/models.py index 76e7d3a03f..bf08829eae 100644 --- a/django/contrib/gis/tests/distapp/models.py +++ b/django/contrib/gis/tests/distapp/models.py @@ -1,50 +1,58 @@ from django.contrib.gis.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class SouthTexasCity(models.Model): "City model on projected coordinate system for South Texas." name = models.CharField(max_length=30) point = models.PointField(srid=32140) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class SouthTexasCityFt(models.Model): "Same City model as above, but U.S. survey feet are the units." name = models.CharField(max_length=30) point = models.PointField(srid=2278) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class AustraliaCity(models.Model): "City model for Australia, using WGS84." name = models.CharField(max_length=30) point = models.PointField() objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class CensusZipcode(models.Model): "Model for a few South Texas ZIP codes (in original Census NAD83)." name = models.CharField(max_length=5) poly = models.PolygonField(srid=4269) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class SouthTexasZipcode(models.Model): "Model for a few South Texas ZIP codes." name = models.CharField(max_length=5) poly = models.PolygonField(srid=32140, null=True) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class Interstate(models.Model): "Geodetic model for U.S. Interstates." name = models.CharField(max_length=10) path = models.LineStringField() objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class SouthTexasInterstate(models.Model): "Projected model for South Texas Interstates." name = models.CharField(max_length=10) path = models.LineStringField(srid=32140) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name diff --git a/django/contrib/gis/tests/geo3d/models.py b/django/contrib/gis/tests/geo3d/models.py index 3c4f77ee05..81e5f55f78 100644 --- a/django/contrib/gis/tests/geo3d/models.py +++ b/django/contrib/gis/tests/geo3d/models.py @@ -1,59 +1,67 @@ from django.contrib.gis.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class City3D(models.Model): name = models.CharField(max_length=30) point = models.PointField(dim=3) objects = models.GeoManager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Interstate2D(models.Model): name = models.CharField(max_length=30) line = models.LineStringField(srid=4269) objects = models.GeoManager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Interstate3D(models.Model): name = models.CharField(max_length=30) line = models.LineStringField(dim=3, srid=4269) objects = models.GeoManager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class InterstateProj2D(models.Model): name = models.CharField(max_length=30) line = models.LineStringField(srid=32140) objects = models.GeoManager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class InterstateProj3D(models.Model): name = models.CharField(max_length=30) line = models.LineStringField(dim=3, srid=32140) objects = models.GeoManager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Polygon2D(models.Model): name = models.CharField(max_length=30) poly = models.PolygonField(srid=32140) objects = models.GeoManager() - - def __unicode__(self): + + def __str__(self): return self.name +@python_2_unicode_compatible class Polygon3D(models.Model): name = models.CharField(max_length=30) poly = models.PolygonField(dim=3, srid=32140) objects = models.GeoManager() - - def __unicode__(self): + + def __str__(self): return self.name class Point2D(models.Model): diff --git a/django/contrib/gis/tests/geoadmin/models.py b/django/contrib/gis/tests/geoadmin/models.py index 51a76d1a0e..af0898823d 100644 --- a/django/contrib/gis/tests/geoadmin/models.py +++ b/django/contrib/gis/tests/geoadmin/models.py @@ -1,10 +1,12 @@ from django.contrib.gis.db import models from django.contrib.gis import admin +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class City(models.Model): name = models.CharField(max_length=30) point = models.PointField() objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name admin.site.register(City, admin.OSMGeoAdmin) diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py index 79061e1cfc..abde509c8b 100644 --- a/django/contrib/gis/tests/geoapp/models.py +++ b/django/contrib/gis/tests/geoapp/models.py @@ -1,20 +1,23 @@ from django.contrib.gis.db import models from django.contrib.gis.tests.utils import mysql, spatialite +from django.utils.encoding import python_2_unicode_compatible # MySQL spatial indices can't handle NULL geometries. null_flag = not mysql +@python_2_unicode_compatible class Country(models.Model): name = models.CharField(max_length=30) mpoly = models.MultiPolygonField() # SRID, by default, is 4326 objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class City(models.Model): name = models.CharField(max_length=30) point = models.PointField() objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name # This is an inherited model from City class PennsylvaniaCity(City): @@ -22,28 +25,31 @@ class PennsylvaniaCity(City): founded = models.DateTimeField(null=True) objects = models.GeoManager() # TODO: This should be implicitly inherited. +@python_2_unicode_compatible class State(models.Model): name = models.CharField(max_length=30) poly = models.PolygonField(null=null_flag) # Allowing NULL geometries here. objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class Track(models.Model): name = models.CharField(max_length=30) line = models.LineStringField() objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name class Truth(models.Model): val = models.BooleanField() objects = models.GeoManager() if not spatialite: + @python_2_unicode_compatible class Feature(models.Model): name = models.CharField(max_length=20) geom = models.GeometryField() objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name class MinusOneSRID(models.Model): geom = models.PointField(srid=-1) # Minus one SRID. diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index b06d6b5e1b..fbe30e8841 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -186,6 +186,15 @@ class GeoModelTest(TestCase): self.assertEqual(1, qs.count()) for pc in qs: self.assertEqual(32128, pc.point.srid) + def test_raw_sql_query(self): + "Testing raw SQL query." + cities1 = City.objects.all() + # Only PostGIS would support a 'select *' query because of its recognized + # HEXEWKB format for geometry fields + cities2 = City.objects.raw('select id, name, asText(point) from geoapp_city') + self.assertEqual(len(cities1), len(list(cities2))) + self.assertTrue(isinstance(cities2[0].point, Point)) + class GeoLookupTest(TestCase): diff --git a/django/contrib/gis/tests/geogapp/models.py b/django/contrib/gis/tests/geogapp/models.py index 3696ba2ff4..7e802f9321 100644 --- a/django/contrib/gis/tests/geogapp/models.py +++ b/django/contrib/gis/tests/geogapp/models.py @@ -1,20 +1,24 @@ from django.contrib.gis.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class City(models.Model): name = models.CharField(max_length=30) point = models.PointField(geography=True) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name +@python_2_unicode_compatible class Zipcode(models.Model): code = models.CharField(max_length=10) poly = models.PolygonField(geography=True) objects = models.GeoManager() - def __unicode__(self): return self.code + def __str__(self): return self.code +@python_2_unicode_compatible class County(models.Model): name = models.CharField(max_length=25) state = models.CharField(max_length=20) mpoly = models.MultiPolygonField(geography=True) objects = models.GeoManager() - def __unicode__(self): return ' County, '.join([self.name, self.state]) + def __str__(self): return ' County, '.join([self.name, self.state]) diff --git a/django/contrib/gis/tests/relatedapp/models.py b/django/contrib/gis/tests/relatedapp/models.py index aec4e15749..659fef7a93 100644 --- a/django/contrib/gis/tests/relatedapp/models.py +++ b/django/contrib/gis/tests/relatedapp/models.py @@ -1,37 +1,41 @@ from django.contrib.gis.db import models from django.contrib.localflavor.us.models import USStateField +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Location(models.Model): point = models.PointField() objects = models.GeoManager() - def __unicode__(self): return self.point.wkt + def __str__(self): return self.point.wkt +@python_2_unicode_compatible class City(models.Model): name = models.CharField(max_length=50) state = USStateField() location = models.ForeignKey(Location) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name class AugmentedLocation(Location): extra_text = models.TextField(blank=True) objects = models.GeoManager() - + class DirectoryEntry(models.Model): listing_text = models.CharField(max_length=50) location = models.ForeignKey(AugmentedLocation) objects = models.GeoManager() +@python_2_unicode_compatible class Parcel(models.Model): name = models.CharField(max_length=30) city = models.ForeignKey(City) center1 = models.PointField() # Throwing a curveball w/`db_column` here. - center2 = models.PointField(srid=2276, db_column='mycenter') + center2 = models.PointField(srid=2276, db_column='mycenter') border1 = models.PolygonField() border2 = models.PolygonField(srid=2276) objects = models.GeoManager() - def __unicode__(self): return self.name + def __str__(self): return self.name # These use the GeoManager but do not have any geographic fields. class Author(models.Model): diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py index f8977059d9..4266ee4b4c 100644 --- a/django/contrib/gis/utils/ogrinspect.py +++ b/django/contrib/gis/utils/ogrinspect.py @@ -223,4 +223,4 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non if name_field: yield '' - yield ' def __unicode__(self): return self.%s' % name_field + yield ' def __str__(self): return self.%s' % name_field diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index af9c842f42..18b7475ca7 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -13,7 +13,7 @@ markup syntaxes to HTML; currently there is support for: from django import template from django.conf import settings -from django.utils.encoding import smart_bytes, force_text +from django.utils.encoding import force_bytes, force_text from django.utils.safestring import mark_safe register = template.Library() @@ -27,7 +27,7 @@ def textile(value): raise template.TemplateSyntaxError("Error in 'textile' filter: The Python textile library isn't installed.") return force_text(value) else: - return mark_safe(force_text(textile.textile(smart_bytes(value), encoding='utf-8', output='utf-8'))) + return mark_safe(force_text(textile.textile(force_bytes(value), encoding='utf-8', output='utf-8'))) @register.filter(is_safe=True) def markdown(value, arg=''): @@ -80,5 +80,5 @@ def restructuredtext(value): return force_text(value) else: docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) - parts = publish_parts(source=smart_bytes(value), writer_name="html4css1", settings_overrides=docutils_settings) + parts = publish_parts(source=force_bytes(value), writer_name="html4css1", settings_overrides=docutils_settings) return mark_safe(force_text(parts["fragment"])) diff --git a/django/contrib/messages/storage/base.py b/django/contrib/messages/storage/base.py index 5433bbff28..7fe8a077ed 100644 --- a/django/contrib/messages/storage/base.py +++ b/django/contrib/messages/storage/base.py @@ -1,14 +1,15 @@ from __future__ import unicode_literals from django.conf import settings -from django.utils.encoding import force_text, StrAndUnicode +from django.utils.encoding import force_text, python_2_unicode_compatible from django.contrib.messages import constants, utils LEVEL_TAGS = utils.get_level_tags() -class Message(StrAndUnicode): +@python_2_unicode_compatible +class Message(object): """ Represents an actual message that can be stored in any of the supported storage classes (typically session- or cookie-based) and rendered in a view @@ -35,7 +36,7 @@ class Message(StrAndUnicode): return isinstance(other, Message) and self.level == other.level and \ self.message == other.message - def __unicode__(self): + def __str__(self): return force_text(self.message) def _get_tags(self): diff --git a/django/contrib/redirects/models.py b/django/contrib/redirects/models.py index 4233d55793..a0376b5578 100644 --- a/django/contrib/redirects/models.py +++ b/django/contrib/redirects/models.py @@ -1,7 +1,9 @@ from django.db import models from django.contrib.sites.models import Site from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Redirect(models.Model): site = models.ForeignKey(Site) old_path = models.CharField(_('redirect from'), max_length=200, db_index=True, @@ -15,6 +17,6 @@ class Redirect(models.Model): db_table = 'django_redirect' unique_together=(('site', 'old_path'),) ordering = ('old_path',) - - def __unicode__(self): + + def __str__(self): return "%s ---> %s" % (self.old_path, self.new_path) diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index 153cde9830..c8393f23c6 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import base64 import time from datetime import datetime, timedelta @@ -12,6 +14,7 @@ from django.utils.crypto import constant_time_compare from django.utils.crypto import get_random_string from django.utils.crypto import salted_hmac from django.utils import timezone +from django.utils.encoding import force_bytes class CreateError(Exception): """ @@ -78,15 +81,15 @@ class SessionBase(object): "Returns the given session dictionary pickled and encoded as a string." pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL) hash = self._hash(pickled) - return base64.encodestring(hash + ":" + pickled) + return base64.b64encode(hash.encode() + b":" + pickled).decode('ascii') def decode(self, session_data): - encoded_data = base64.decodestring(session_data) + encoded_data = base64.b64decode(force_bytes(session_data)) try: # could produce ValueError if there is no ':' - hash, pickled = encoded_data.split(':', 1) + hash, pickled = encoded_data.split(b':', 1) expected_hash = self._hash(pickled) - if not constant_time_compare(hash, expected_hash): + if not constant_time_compare(hash.decode(), expected_hash): raise SuspiciousOperation("Session data corrupted") else: return pickle.loads(pickled) diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py index 0cc17b44c3..babdb72c27 100644 --- a/django/contrib/sessions/backends/db.py +++ b/django/contrib/sessions/backends/db.py @@ -1,7 +1,6 @@ from django.contrib.sessions.backends.base import SessionBase, CreateError from django.core.exceptions import SuspiciousOperation from django.db import IntegrityError, transaction, router -from django.utils.encoding import force_text from django.utils import timezone @@ -18,7 +17,7 @@ class SessionStore(SessionBase): session_key = self.session_key, expire_date__gt=timezone.now() ) - return self.decode(force_text(s.session_data)) + return self.decode(s.session_data) except (Session.DoesNotExist, SuspiciousOperation): self.create() return {} diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py index 0f869088ac..20ac2c2087 100644 --- a/django/contrib/sessions/backends/file.py +++ b/django/contrib/sessions/backends/file.py @@ -115,7 +115,7 @@ class SessionStore(SessionBase): renamed = False try: try: - os.write(output_file_fd, self.encode(session_data)) + os.write(output_file_fd, self.encode(session_data).encode()) finally: os.close(output_file_fd) os.rename(output_file_name, session_file_name) diff --git a/django/contrib/sitemaps/tests/generic.py b/django/contrib/sitemaps/tests/generic.py index e392cbf909..e0b0a827a6 100644 --- a/django/contrib/sitemaps/tests/generic.py +++ b/django/contrib/sitemaps/tests/generic.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib.auth.models import User from django.test.utils import override_settings @@ -12,8 +14,9 @@ class GenericViewsSitemapTests(SitemapTestsBase): expected = '' for username in User.objects.values_list("username", flat=True): expected += "%s/users/%s/" % (self.base_url, username) - self.assertEqual(response.content, """ + expected_content = """ %s -""" % expected) +""" % expected + self.assertEqual(response.content, expected_content.encode('utf-8')) diff --git a/django/contrib/sitemaps/tests/http.py b/django/contrib/sitemaps/tests/http.py index d71907e09a..8da971876f 100644 --- a/django/contrib/sitemaps/tests/http.py +++ b/django/contrib/sitemaps/tests/http.py @@ -21,11 +21,12 @@ class HTTPSitemapTests(SitemapTestsBase): def test_simple_sitemap_index(self): "A simple sitemap index can be rendered" response = self.client.get('/simple/index.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/simple/sitemap-simple.xml -""" % self.base_url) +""" % self.base_url + self.assertEqual(response.content, expected_content.encode('utf-8')) @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) @@ -33,30 +34,34 @@ class HTTPSitemapTests(SitemapTestsBase): def test_simple_sitemap_custom_index(self): "A simple sitemap index can be rendered with a custom template" response = self.client.get('/simple/custom-index.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/simple/sitemap-simple.xml -""" % self.base_url) +""" % self.base_url + self.assertEqual(response.content, expected_content.encode('utf-8')) + def test_simple_sitemap_section(self): "A simple sitemap section can be rendered" response = self.client.get('/simple/sitemap-simple.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/location/%snever0.5 -""" % (self.base_url, date.today())) +""" % (self.base_url, date.today()) + self.assertEqual(response.content, expected_content.encode('utf-8')) def test_simple_sitemap(self): "A simple sitemap can be rendered" response = self.client.get('/simple/sitemap.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/location/%snever0.5 -""" % (self.base_url, date.today())) +""" % (self.base_url, date.today()) + self.assertEqual(response.content, expected_content.encode('utf-8')) @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) @@ -64,12 +69,13 @@ class HTTPSitemapTests(SitemapTestsBase): def test_simple_custom_sitemap(self): "A simple sitemap can be rendered with a custom template" response = self.client.get('/simple/custom-sitemap.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/location/%snever0.5 -""" % (self.base_url, date.today())) +""" % (self.base_url, date.today()) + self.assertEqual(response.content, expected_content.encode('utf-8')) @skipUnless(settings.USE_I18N, "Internationalization is not enabled") @override_settings(USE_L10N=True) @@ -90,11 +96,12 @@ class HTTPSitemapTests(SitemapTestsBase): # installed doesn't raise an exception Site._meta.installed = False response = self.client.get('/simple/sitemap.xml') - self.assertEqual(response.content, """ + expected_content = """ http://testserver/location/%snever0.5 -""" % date.today()) +""" % date.today() + self.assertEqual(response.content, expected_content.encode('utf-8')) @skipUnless("django.contrib.sites" in settings.INSTALLED_APPS, "django.contrib.sites app not installed.") @@ -131,8 +138,9 @@ class HTTPSitemapTests(SitemapTestsBase): Check that a cached sitemap index can be rendered (#2713). """ response = self.client.get('/cached/index.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/cached/sitemap-simple.xml -""" % self.base_url) +""" % self.base_url + self.assertEqual(response.content, expected_content.encode('utf-8')) diff --git a/django/contrib/sitemaps/tests/https.py b/django/contrib/sitemaps/tests/https.py index d4f9053fc8..26241eb30b 100644 --- a/django/contrib/sitemaps/tests/https.py +++ b/django/contrib/sitemaps/tests/https.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from datetime import date from django.test.utils import override_settings @@ -11,20 +13,22 @@ class HTTPSSitemapTests(SitemapTestsBase): def test_secure_sitemap_index(self): "A secure sitemap index can be rendered" response = self.client.get('/secure/index.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/secure/sitemap-simple.xml -""" % self.base_url) +""" % self.base_url + self.assertEqual(response.content, expected_content.encode('utf-8')) def test_secure_sitemap_section(self): "A secure sitemap section can be rendered" response = self.client.get('/secure/sitemap-simple.xml') - self.assertEqual(response.content, """ + expected_content = """ %s/location/%snever0.5 -""" % (self.base_url, date.today())) +""" % (self.base_url, date.today()) + self.assertEqual(response.content, expected_content.encode('utf-8')) @override_settings(SECURE_PROXY_SSL_HEADER=False) @@ -34,17 +38,19 @@ class HTTPSDetectionSitemapTests(SitemapTestsBase): def test_sitemap_index_with_https_request(self): "A sitemap index requested in HTTPS is rendered with HTTPS links" response = self.client.get('/simple/index.xml', **self.extra) - self.assertEqual(response.content, """ + expected_content = """ %s/simple/sitemap-simple.xml -""" % self.base_url.replace('http://', 'https://')) +""" % self.base_url.replace('http://', 'https://') + self.assertEqual(response.content, expected_content.encode('utf-8')) def test_sitemap_section_with_https_request(self): "A sitemap section requested in HTTPS is rendered with HTTPS links" response = self.client.get('/simple/sitemap-simple.xml', **self.extra) - self.assertEqual(response.content, """ + expected_content = """ %s/location/%snever0.5 -""" % (self.base_url.replace('http://', 'https://'), date.today())) +""" % (self.base_url.replace('http://', 'https://'), date.today()) + self.assertEqual(response.content, expected_content.encode('utf-8')) diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py index fecbff79d8..8590740658 100644 --- a/django/contrib/sites/models.py +++ b/django/contrib/sites/models.py @@ -1,5 +1,6 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import python_2_unicode_compatible SITE_CACHE = {} @@ -32,6 +33,7 @@ class SiteManager(models.Manager): SITE_CACHE = {} +@python_2_unicode_compatible class Site(models.Model): domain = models.CharField(_('domain name'), max_length=100) @@ -44,7 +46,7 @@ class Site(models.Model): verbose_name_plural = _('sites') ordering = ('domain',) - def __unicode__(self): + def __str__(self): return self.domain def save(self, *args, **kwargs): @@ -62,6 +64,7 @@ class Site(models.Model): pass +@python_2_unicode_compatible class RequestSite(object): """ A class that shares the primary interface of Site (i.e., it has @@ -73,7 +76,7 @@ class RequestSite(object): def __init__(self, request): self.domain = self.name = request.get_host() - def __unicode__(self): + def __str__(self): return self.domain def save(self, force_insert=False, force_update=False): diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 7dac0ffb4c..0e16e9d649 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -192,7 +192,7 @@ Type 'yes' to continue, or 'no' to cancel: """ def clear_dir(self, path): """ - Deletes the given relative path using the destinatin storage backend. + Deletes the given relative path using the destination storage backend. """ dirs, files = self.storage.listdir(path) for f in files: diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index 2ca54dde71..9691b7849d 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -16,7 +16,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage, get_storage_class from django.utils.datastructures import SortedDict -from django.utils.encoding import force_text, smart_bytes +from django.utils.encoding import force_bytes, force_text from django.utils.functional import LazyObject from django.utils.importlib import import_module @@ -51,8 +51,8 @@ class CachedFilesMixin(object): default_template = """url("%s")""" patterns = ( ("*.css", ( - br"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""", - (br"""(@import\s*["']\s*(.*?)["'])""", """@import url("%s")"""), + r"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""", + (r"""(@import\s*["']\s*(.*?)["'])""", """@import url("%s")"""), )), ) @@ -87,6 +87,7 @@ class CachedFilesMixin(object): def hashed_name(self, name, content=None): parsed_name = urlsplit(unquote(name)) clean_name = parsed_name.path.strip() + opened = False if content is None: if not self.exists(clean_name): raise ValueError("The file '%s' could not be found with %r." % @@ -96,9 +97,14 @@ class CachedFilesMixin(object): except IOError: # Handle directory paths and fragments return name + opened = True + try: + file_hash = self.file_hash(clean_name, content) + finally: + if opened: + content.close() path, filename = os.path.split(clean_name) root, ext = os.path.splitext(filename) - file_hash = self.file_hash(clean_name, content) if file_hash is not None: file_hash = ".%s" % file_hash hashed_name = os.path.join(path, "%s%s%s" % @@ -112,7 +118,7 @@ class CachedFilesMixin(object): return urlunsplit(unparsed_name) def cache_key(self, name): - return 'staticfiles:%s' % hashlib.md5(smart_bytes(name)).hexdigest() + return 'staticfiles:%s' % hashlib.md5(force_bytes(name)).hexdigest() def url(self, name, force=False): """ @@ -248,7 +254,7 @@ class CachedFilesMixin(object): if hashed_file_exists: self.delete(hashed_name) # then save the processed result - content_file = ContentFile(smart_bytes(content)) + content_file = ContentFile(force_bytes(content)) saved_name = self._save(hashed_name, content_file) hashed_name = force_text(saved_name.replace('\\', '/')) processed = True @@ -261,7 +267,7 @@ class CachedFilesMixin(object): hashed_name = force_text(saved_name.replace('\\', '/')) # and then set the cache accordingly - hashed_paths[self.cache_key(name)] = hashed_name + hashed_paths[self.cache_key(name.replace('\\', '/'))] = hashed_name yield name, hashed_name, processed # Finally set the cache diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index bce7ef7cfb..4815ce5567 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -106,7 +106,7 @@ class Feed(object): subtitle = self.__get_dynamic_attr('subtitle', obj), link = link, description = self.__get_dynamic_attr('description', obj), - language = settings.LANGUAGE_CODE.decode(), + language = settings.LANGUAGE_CODE, feed_url = add_domain( current_site.domain, self.__get_dynamic_attr('feed_url', obj) or request.path, diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index d527e44d8b..06e8952bfb 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -1,9 +1,9 @@ "Base Cache class." +from __future__ import unicode_literals import warnings from django.core.exceptions import ImproperlyConfigured, DjangoRuntimeWarning -from django.utils.encoding import smart_bytes from django.utils.importlib import import_module class InvalidCacheBackendError(ImproperlyConfigured): @@ -23,7 +23,7 @@ def default_key_func(key, key_prefix, version): the `key_prefix'. KEY_FUNCTION can be used to specify an alternate function with custom key making behavior. """ - return ':'.join([key_prefix, str(version), smart_bytes(key)]) + return ':'.join([key_prefix, str(version), key]) def get_key_func(key_func): """ @@ -62,7 +62,7 @@ class BaseCache(object): except (ValueError, TypeError): self._cull_frequency = 3 - self.key_prefix = smart_bytes(params.get('KEY_PREFIX', '')) + self.key_prefix = params.get('KEY_PREFIX', '') self.version = params.get('VERSION', 1) self.key_func = get_key_func(params.get('KEY_FUNCTION', None)) diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index f60b4e0cd1..348b03f733 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -12,6 +12,7 @@ from django.conf import settings from django.core.cache.backends.base import BaseCache from django.db import connections, router, transaction, DatabaseError from django.utils import timezone +from django.utils.encoding import force_bytes class Options(object): @@ -72,7 +73,7 @@ class DatabaseCache(BaseDatabaseCache): transaction.commit_unless_managed(using=db) return default value = connections[db].ops.process_clob(row[1]) - return pickle.loads(base64.decodestring(value)) + return pickle.loads(base64.b64decode(force_bytes(value))) def set(self, key, value, timeout=None, version=None): key = self.make_key(key, version=version) @@ -103,7 +104,7 @@ class DatabaseCache(BaseDatabaseCache): if num > self._max_entries: self._cull(db, cursor, now) pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) - encoded = base64.encodestring(pickled).strip() + encoded = base64.b64encode(pickled).strip() cursor.execute("SELECT cache_key, expires FROM %s " "WHERE cache_key = %%s" % table, [key]) try: @@ -166,7 +167,7 @@ class DatabaseCache(BaseDatabaseCache): cursor.execute("SELECT COUNT(*) FROM %s" % table) num = cursor.fetchone()[0] if num > self._max_entries: - cull_num = num / self._cull_frequency + cull_num = num // self._cull_frequency cursor.execute( connections[db].ops.cache_key_culling_sql() % table, [cull_num]) diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index 1170996a76..96194d458f 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -10,6 +10,7 @@ except ImportError: import pickle from django.core.cache.backends.base import BaseCache +from django.utils.encoding import force_bytes class FileBasedCache(BaseCache): def __init__(self, dir, params): @@ -136,7 +137,7 @@ class FileBasedCache(BaseCache): Thus, a cache key of "foo" gets turnned into a file named ``{cache-dir}ac/bd/18db4cc2f85cedef654fccc4a4d8``. """ - path = hashlib.md5(key).hexdigest() + path = hashlib.md5(force_bytes(key)).hexdigest() path = os.path.join(path[:2], path[2:4], path[4:]) return os.path.join(self._dir, path) diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index e7724f1b91..426a0a15c0 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -6,6 +6,7 @@ from threading import local from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError from django.utils import six +from django.utils.encoding import force_str class BaseMemcachedCache(BaseCache): def __init__(self, server, params, library, value_not_found_exception): @@ -50,6 +51,10 @@ class BaseMemcachedCache(BaseCache): timeout += int(time.time()) return int(timeout) + def make_key(self, key, version=None): + # 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): key = self.make_key(key, version=version) return self._cache.add(key, value, self._get_memcache_timeout(timeout)) diff --git a/django/core/context_processors.py b/django/core/context_processors.py index a503270cf4..ca1ac68f55 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -6,12 +6,15 @@ and returns a dictionary to add to the context. These are referenced from the setting TEMPLATE_CONTEXT_PROCESSORS and used by RequestContext. """ +from __future__ import unicode_literals from django.conf import settings from django.middleware.csrf import get_token -from django.utils.encoding import smart_bytes +from django.utils import six +from django.utils.encoding import smart_text from django.utils.functional import lazy + def csrf(request): """ Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if @@ -23,10 +26,10 @@ def csrf(request): # In order to be able to provide debugging info in the # case of misconfiguration, we use a sentinel value # instead of returning an empty dict. - return b'NOTPROVIDED' + return 'NOTPROVIDED' else: - return smart_bytes(token) - _get_val = lazy(_get_val, str) + return smart_text(token) + _get_val = lazy(_get_val, six.text_type) return {'csrf_token': _get_val() } diff --git a/django/core/files/base.py b/django/core/files/base.py index d0b25250a5..b81e180292 100644 --- a/django/core/files/base.py +++ b/django/core/files/base.py @@ -1,11 +1,14 @@ from __future__ import unicode_literals import os -from io import BytesIO +from io import BytesIO, StringIO, UnsupportedOperation -from django.utils.encoding import smart_bytes, smart_text +from django.utils.encoding import smart_text from django.core.files.utils import FileProxyMixin +from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class File(FileProxyMixin): DEFAULT_CHUNK_SIZE = 64 * 2**10 @@ -18,9 +21,6 @@ class File(FileProxyMixin): self.mode = file.mode def __str__(self): - return smart_bytes(self.name or '') - - def __unicode__(self): return smart_text(self.name or '') def __repr__(self): @@ -65,8 +65,10 @@ class File(FileProxyMixin): if not chunk_size: chunk_size = self.DEFAULT_CHUNK_SIZE - if hasattr(self, 'seek'): + try: self.seek(0) + except (AttributeError, UnsupportedOperation): + pass while True: data = self.read(chunk_size) @@ -124,13 +126,15 @@ class File(FileProxyMixin): def close(self): self.file.close() +@python_2_unicode_compatible class ContentFile(File): """ A File-like object that takes just raw content, rather than an actual file. """ def __init__(self, content, name=None): content = content or b'' - super(ContentFile, self).__init__(BytesIO(content), name=name) + stream_class = StringIO if isinstance(content, six.text_type) else BytesIO + super(ContentFile, self).__init__(stream_class(content), name=name) self.size = len(content) def __str__(self): diff --git a/django/core/files/move.py b/django/core/files/move.py index f9060fd3d8..3af02634fe 100644 --- a/django/core/files/move.py +++ b/django/core/files/move.py @@ -66,7 +66,7 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove try: locks.lock(fd, locks.LOCK_EX) current_chunk = None - while current_chunk != '': + while current_chunk != b'': current_chunk = old_file.read(chunk_size) os.write(fd, current_chunk) finally: diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 7542dcda46..0b300cd31e 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -195,11 +195,18 @@ class FileSystemStorage(Storage): fd = os.open(full_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, 'O_BINARY', 0)) try: locks.lock(fd, locks.LOCK_EX) + _file = None for chunk in content.chunks(): - os.write(fd, chunk) + if _file is None: + mode = 'wb' if isinstance(chunk, bytes) else 'wt' + _file = os.fdopen(fd, mode) + _file.write(chunk) finally: locks.unlock(fd) - os.close(fd) + if _file is not None: + _file.close() + else: + os.close(fd) except OSError as e: if e.errno == errno.EEXIST: # Ooops, the file exists. We need a new file name. diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py index 3a6c632975..39b99ff78f 100644 --- a/django/core/files/uploadedfile.py +++ b/django/core/files/uploadedfile.py @@ -8,7 +8,7 @@ from io import BytesIO from django.conf import settings from django.core.files.base import File from django.core.files import temp as tempfile -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_str __all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile', 'SimpleUploadedFile') @@ -30,7 +30,7 @@ class UploadedFile(File): self.charset = charset def __repr__(self): - return smart_bytes("<%s: %s (%s)>" % ( + return force_str("<%s: %s (%s)>" % ( self.__class__.__name__, self.name, self.content_type)) def _get_name(self): diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py index 68d540e595..c422945d6f 100644 --- a/django/core/files/uploadhandler.py +++ b/django/core/files/uploadhandler.py @@ -10,6 +10,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.files.uploadedfile import TemporaryUploadedFile, InMemoryUploadedFile from django.utils import importlib +from django.utils.encoding import python_2_unicode_compatible __all__ = ['UploadFileException','StopUpload', 'SkipFile', 'FileUploadHandler', 'TemporaryFileUploadHandler', 'MemoryFileUploadHandler', @@ -21,6 +22,7 @@ class UploadFileException(Exception): """ pass +@python_2_unicode_compatible class StopUpload(UploadFileException): """ This exception is raised when an upload must abort. @@ -33,7 +35,7 @@ class StopUpload(UploadFileException): """ self.connection_reset = connection_reset - def __unicode__(self): + def __str__(self): if self.connection_reset: return 'StopUpload: Halt current upload.' else: diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 5a6825f0a7..791382bac0 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import sys +import types from django import http from django.core import signals @@ -125,10 +126,10 @@ class BaseHandler(object): # Complain if the view returned None (a common error). if response is None: - try: - view_name = callback.func_name # If it's a function - except AttributeError: - view_name = callback.__class__.__name__ + '.__call__' # If it's a class + if isinstance(callback, types.FunctionType): # FBV + view_name = callback.__name__ + else: # CBV + view_name = callback.__class__.__name__ + '.__call__' raise ValueError("The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name)) # If the response supports deferred rendering, apply template @@ -152,10 +153,8 @@ class BaseHandler(object): callback, param_dict = resolver.resolve404() response = callback(request, **param_dict) except: - try: - response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) - finally: - signals.got_request_exception.send(sender=self.__class__, request=request) + signals.got_request_exception.send(sender=self.__class__, request=request) + response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) except exceptions.PermissionDenied: logger.warning( 'Forbidden (Permission denied): %s', request.path, @@ -167,12 +166,10 @@ class BaseHandler(object): callback, param_dict = resolver.resolve403() response = callback(request, **param_dict) except: - try: - response = self.handle_uncaught_exception(request, - resolver, sys.exc_info()) - finally: - signals.got_request_exception.send( + signals.got_request_exception.send( sender=self.__class__, request=request) + response = self.handle_uncaught_exception(request, + resolver, sys.exc_info()) except SystemExit: # Allow sys.exit() to actually exit. See tickets #1023 and #4701 raise @@ -225,7 +222,7 @@ class BaseHandler(object): # If Http500 handler is not installed, re-raise last exception if resolver.urlconf_module is None: - six.reraise(exc_info[1], None, exc_info[2]) + six.reraise(*exc_info) # Return an HttpResponse that displays a friendly error message. callback, param_dict = resolver.resolve500() return callback(request, **param_dict) diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 70b23f8515..e445d07a61 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -9,7 +9,7 @@ from django.core import signals from django.core.handlers import base from django.core.urlresolvers import set_script_prefix from django.utils import datastructures -from django.utils.encoding import force_text, smart_bytes, iri_to_uri +from django.utils.encoding import force_str, force_text, iri_to_uri from django.utils.log import getLogger logger = getLogger('django.request') @@ -246,5 +246,5 @@ class WSGIHandler(base.BaseHandler): 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='')))) - start_response(smart_bytes(status), response_headers) + start_response(force_str(status), response_headers) return response diff --git a/django/core/mail/message.py b/django/core/mail/message.py index b332ffba04..db9023a0bb 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -99,7 +99,12 @@ def sanitize_address(addr, encoding): if isinstance(addr, six.string_types): addr = parseaddr(force_text(addr)) nm, addr = addr - nm = Header(nm, encoding).encode() + # This try-except clause is needed on Python 3 < 3.2.4 + # http://bugs.python.org/issue14291 + try: + nm = Header(nm, encoding).encode() + except UnicodeEncodeError: + nm = Header(nm, 'utf-8').encode() try: addr.encode('ascii') except UnicodeEncodeError: # IDN diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 268cd63c38..98f75e0310 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -51,14 +51,19 @@ def find_management_module(app_name): # module, we need look for the case where the project name is part # of the app_name but the project directory itself isn't on the path. try: - f, path, descr = imp.find_module(part,path) + f, path, descr = imp.find_module(part, path) except ImportError as e: if os.path.basename(os.getcwd()) != part: raise e + else: + if f: + f.close() while parts: part = parts.pop() f, path, descr = imp.find_module(part, path and [path] or None) + if f: + f.close() return path def load_command_class(app_name, name): diff --git a/django/core/management/base.py b/django/core/management/base.py index 5e630d5207..895753ea12 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -12,7 +12,7 @@ import traceback import django from django.core.exceptions import ImproperlyConfigured from django.core.management.color import color_style -from django.utils.encoding import smart_str +from django.utils.encoding import force_str from django.utils.six import StringIO @@ -65,7 +65,7 @@ class OutputWrapper(object): msg += ending style_func = [f for f in (style_func, self.style_func, lambda x:x) if f is not None][0] - self._out.write(smart_str(style_func(msg))) + self._out.write(force_str(style_func(msg))) class BaseCommand(object): diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index 7c868e4b60..936d5e89ab 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -1,4 +1,7 @@ +from __future__ import unicode_literals + import keyword +import re from optparse import make_option from django.core.management.base import NoArgsCommand, CommandError @@ -31,6 +34,7 @@ class Command(NoArgsCommand): table_name_filter = options.get('table_name_filter') table2model = lambda table_name: table_name.title().replace('_', '').replace(' ', '').replace('-', '') + strip_prefix = lambda s: s.startswith("u'") and s[1:] or s cursor = connection.cursor() yield "# This is an auto-generated Django model module." @@ -41,6 +45,7 @@ class Command(NoArgsCommand): yield "#" yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'" yield "# into your database." + yield "from __future__ import unicode_literals" yield '' yield 'from %s import models' % self.db_module yield '' @@ -59,16 +64,19 @@ class Command(NoArgsCommand): indexes = connection.introspection.get_indexes(cursor, table_name) except NotImplementedError: indexes = {} + used_column_names = [] # Holds column names used in the table so far for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)): - column_name = row[0] - att_name = column_name.lower() comment_notes = [] # Holds Field notes, to be displayed in a Python comment. extra_params = {} # Holds Field parameters such as 'db_column'. + column_name = row[0] + is_relation = i in relations - # If the column name can't be used verbatim as a Python - # attribute, set the "db_column" for this Field. - if ' ' in att_name or '-' in att_name or keyword.iskeyword(att_name) or column_name != att_name: - extra_params['db_column'] = column_name + att_name, params, notes = self.normalize_col_name( + column_name, used_column_names, is_relation) + extra_params.update(params) + comment_notes.extend(notes) + + used_column_names.append(att_name) # Add primary_key and unique, if necessary. if column_name in indexes: @@ -77,30 +85,12 @@ class Command(NoArgsCommand): elif indexes[column_name]['unique']: extra_params['unique'] = True - # Modify the field name to make it Python-compatible. - if ' ' in att_name: - att_name = att_name.replace(' ', '_') - comment_notes.append('Field renamed to remove spaces.') - - if '-' in att_name: - att_name = att_name.replace('-', '_') - comment_notes.append('Field renamed to remove dashes.') - - if column_name != att_name: - comment_notes.append('Field name made lowercase.') - - if i in relations: + if is_relation: rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1]) - if rel_to in known_models: field_type = 'ForeignKey(%s' % rel_to else: field_type = "ForeignKey('%s'" % rel_to - - if att_name.endswith('_id'): - att_name = att_name[:-3] - else: - extra_params['db_column'] = column_name else: # Calling `get_field_type` to get the field type string and any # additional paramters and notes. @@ -110,16 +100,6 @@ class Command(NoArgsCommand): field_type += '(' - if keyword.iskeyword(att_name): - att_name += '_field' - comment_notes.append('Field renamed because it was a Python reserved word.') - - if att_name[0].isdigit(): - att_name = 'number_%s' % att_name - extra_params['db_column'] = six.text_type(column_name) - comment_notes.append("Field renamed because it wasn't a " - "valid Python identifier.") - # Don't output 'id = meta.AutoField(primary_key=True)', because # that's assumed if it doesn't exist. if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}: @@ -136,7 +116,9 @@ class Command(NoArgsCommand): if extra_params: if not field_desc.endswith('('): field_desc += ', ' - field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()]) + field_desc += ', '.join([ + '%s=%s' % (k, strip_prefix(repr(v))) + for k, v in extra_params.items()]) field_desc += ')' if comment_notes: field_desc += ' # ' + ' '.join(comment_notes) @@ -144,6 +126,62 @@ class Command(NoArgsCommand): for meta_line in self.get_meta(table_name): yield meta_line + def normalize_col_name(self, col_name, used_column_names, is_relation): + """ + Modify the column name to make it Python-compatible as a field name + """ + field_params = {} + field_notes = [] + + new_name = col_name.lower() + if new_name != col_name: + field_notes.append('Field name made lowercase.') + + if is_relation: + if new_name.endswith('_id'): + new_name = new_name[:-3] + else: + field_params['db_column'] = col_name + + new_name, num_repl = re.subn(r'\W', '_', new_name) + if num_repl > 0: + field_notes.append('Field renamed to remove unsuitable characters.') + + if new_name.find('__') >= 0: + while new_name.find('__') >= 0: + new_name = new_name.replace('__', '_') + if col_name.lower().find('__') >= 0: + # Only add the comment if the double underscore was in the original name + field_notes.append("Field renamed because it contained more than one '_' in a row.") + + if new_name.startswith('_'): + new_name = 'field%s' % new_name + field_notes.append("Field renamed because it started with '_'.") + + if new_name.endswith('_'): + new_name = '%sfield' % new_name + field_notes.append("Field renamed because it ended with '_'.") + + if keyword.iskeyword(new_name): + new_name += '_field' + field_notes.append('Field renamed because it was a Python reserved word.') + + if new_name[0].isdigit(): + new_name = 'number_%s' % new_name + field_notes.append("Field renamed because it wasn't a valid Python identifier.") + + if new_name in used_column_names: + num = 0 + while '%s_%d' % (new_name, num) in used_column_names: + num += 1 + new_name = '%s_%d' % (new_name, num) + field_notes.append('Field renamed because of name conflict.') + + if col_name != new_name and field_notes: + field_params['db_column'] = col_name + + return new_name, field_params, field_notes + def get_field_type(self, connection, table_name, row): """ Given the database connection, the table name, and the cursor row @@ -181,6 +219,6 @@ class Command(NoArgsCommand): to construct the inner Meta class for the model corresponding to the given database table name. """ - return [' class Meta:', - ' db_table = %r' % table_name, - ''] + return [" class Meta:", + " db_table = '%s'" % table_name, + ""] diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 1896e53cee..30cf740cdf 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -196,6 +196,10 @@ class Command(BaseCommand): loaded_object_count += loaded_objects_in_fixture fixture_object_count += objects_in_fixture label_found = True + except Exception as e: + if not isinstance(e, CommandError): + e.args = ("Problem installing fixture '%s': %s" % (full_path, e),) + raise finally: fixture.close() @@ -209,7 +213,11 @@ class Command(BaseCommand): # Since we disabled constraint checks, we must manually check for # any invalid keys that might have been added table_names = [model._meta.db_table for model in models] - connection.check_constraints(table_names=table_names) + try: + connection.check_constraints(table_names=table_names) + except Exception as e: + e.args = ("Problem installing fixtures: %s" % e,) + raise except (SystemExit, KeyboardInterrupt): raise @@ -217,8 +225,6 @@ class Command(BaseCommand): if commit: transaction.rollback(using=using) transaction.leave_transaction_management(using=using) - if not isinstance(e, CommandError): - e.args = ("Problem installing fixture '%s': %s" % (full_path, e),) raise # If we found even one object in a fixture, we need to reset the diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index d8e8f6cff7..81c4fdf8cc 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -47,32 +47,27 @@ def _popen(cmd): output, errors = p.communicate() return output, errors, p.returncode -def walk(root, topdown=True, onerror=None, followlinks=False, - ignore_patterns=None, verbosity=0, stdout=sys.stdout): +def find_files(root, ignore_patterns, verbosity, stdout=sys.stdout, symlinks=False): """ - A version of os.walk that can follow symlinks for Python < 2.6 + Helper function to get all files in the given root. """ - if ignore_patterns is None: - ignore_patterns = [] dir_suffix = '%s*' % os.sep - norm_patterns = map(lambda p: p.endswith(dir_suffix) - and p[:-len(dir_suffix)] or p, ignore_patterns) - for dirpath, dirnames, filenames in os.walk(root, topdown, onerror): - remove_dirs = [] - for dirname in dirnames: + norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in ignore_patterns] + all_files = [] + for dirpath, dirnames, filenames in os.walk(root, topdown=True, followlinks=symlinks): + for dirname in dirnames[:]: if is_ignored(os.path.normpath(os.path.join(dirpath, dirname)), norm_patterns): - remove_dirs.append(dirname) - for dirname in remove_dirs: - dirnames.remove(dirname) - if verbosity > 1: - stdout.write('ignoring directory %s\n' % dirname) - yield (dirpath, dirnames, filenames) - if followlinks: - for d in dirnames: - p = os.path.join(dirpath, d) - if os.path.islink(p): - for link_dirpath, link_dirnames, link_filenames in walk(p): - yield (link_dirpath, link_dirnames, link_filenames) + dirnames.remove(dirname) + if verbosity > 1: + stdout.write('ignoring directory %s\n' % dirname) + for filename in filenames: + if is_ignored(os.path.normpath(os.path.join(dirpath, filename)), ignore_patterns): + if verbosity > 1: + stdout.write('ignoring file %s in %s\n' % (filename, dirpath)) + else: + all_files.extend([(dirpath, filename)]) + all_files.sort() + return all_files def is_ignored(path, ignore_patterns): """ @@ -83,23 +78,6 @@ def is_ignored(path, ignore_patterns): return True return False -def find_files(root, ignore_patterns, verbosity, stdout=sys.stdout, symlinks=False): - """ - Helper function to get all files in the given root. - """ - all_files = [] - for (dirpath, dirnames, filenames) in walk(root, followlinks=symlinks, - ignore_patterns=ignore_patterns, verbosity=verbosity, stdout=stdout): - for filename in filenames: - norm_filepath = os.path.normpath(os.path.join(dirpath, filename)) - if is_ignored(norm_filepath, ignore_patterns): - if verbosity > 1: - stdout.write('ignoring file %s in %s\n' % (filename, dirpath)) - else: - all_files.extend([(dirpath, filename)]) - all_files.sort() - return all_files - def copy_plural_forms(msgs, locale, domain, verbosity, stdout=sys.stdout): """ Copies plural forms header contents from a Django catalog of locale to @@ -144,7 +122,7 @@ def write_pot_file(potfile, msgs, file, work_file, is_templatized): msgs = '\n'.join(dropwhile(len, msgs.split('\n'))) else: msgs = msgs.replace('charset=CHARSET', 'charset=UTF-8') - with open(potfile, 'ab') as fp: + with open(potfile, 'a') as fp: fp.write(msgs) def process_file(file, dirpath, potfile, domain, verbosity, @@ -252,7 +230,7 @@ def write_po_file(pofile, potfile, domain, locale, verbosity, stdout, msgs = copy_plural_forms(msgs, locale, domain, verbosity, stdout) msgs = msgs.replace( "#. #-#-#-#-# %s.pot (PACKAGE VERSION) #-#-#-#-#\n" % domain, "") - with open(pofile, 'wb') as fp: + with open(pofile, 'w') as fp: fp.write(msgs) os.unlink(potfile) if no_obsolete: diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 4e7d1dbbf4..52a8cab7e4 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -80,14 +80,14 @@ class Command(NoArgsCommand): readline.parse_and_bind("tab:complete") # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system - # conventions and get $PYTHONSTARTUP first then import user. + # conventions and get $PYTHONSTARTUP first then .pythonrc.py. if not use_plain: - pythonrc = os.environ.get("PYTHONSTARTUP") - if pythonrc and os.path.isfile(pythonrc): - try: - execfile(pythonrc) - except NameError: - pass - # This will import .pythonrc.py as a side-effect - import user + for pythonrc in (os.environ.get("PYTHONSTARTUP"), + os.path.expanduser('~/.pythonrc.py')): + if pythonrc and os.path.isfile(pythonrc): + try: + with open(pythonrc) as handle: + exec(compile(handle.read(), pythonrc, 'exec')) + except NameError: + pass code.interact(local=imported_objects) diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index cf26e754ae..cceec07be8 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -75,7 +75,7 @@ class Command(NoArgsCommand): (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables)) manifest = SortedDict( - (app_name, filter(model_installed, model_list)) + (app_name, list(filter(model_installed, model_list))) for app_name, model_list in all_models ) diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 7579cbe8ab..b02a548314 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import codecs import os import re @@ -168,10 +169,10 @@ def custom_sql_for_model(model, style, connection): os.path.join(app_dir, "%s.sql" % opts.object_name.lower())] for sql_file in sql_files: if os.path.exists(sql_file): - with open(sql_file, 'U') as fp: + with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp: # Some backends can't execute more than one SQL statement at a time, # so split into separate statements. - output.extend(_split_statements(fp.read().decode(settings.FILE_CHARSET))) + output.extend(_split_statements(fp.read())) return output diff --git a/django/core/management/templates.py b/django/core/management/templates.py index 52d0e5c89d..aa65593e9c 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -8,6 +8,8 @@ import shutil import stat import sys import tempfile +import codecs + try: from urllib.request import urlretrieve except ImportError: # Python 2 @@ -154,12 +156,12 @@ class TemplateCommand(BaseCommand): # Only render the Python files, as we don't want to # accidentally render Django templates files - with open(old_path, 'r') as template_file: + with codecs.open(old_path, 'r', 'utf-8') as template_file: content = template_file.read() if filename.endswith(extensions) or filename in extra_files: template = Template(content) content = template.render(context) - with open(new_path, 'w') as new_file: + with codecs.open(new_path, 'w', 'utf-8') as new_file: new_file.write(content) if self.verbosity >= 2: diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 274f98ee79..6cd66f3a6a 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -1,7 +1,7 @@ import sys from django.core.management.color import color_style -from django.utils.encoding import smart_str +from django.utils.encoding import force_str from django.utils.itercompat import is_iterable from django.utils import six @@ -13,7 +13,7 @@ class ModelErrorCollection: def add(self, context, error): self.errors.append((context, error)) - self.outfile.write(self.style.ERROR(smart_str("%s: %s\n" % (context, error)))) + self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error)))) def get_validation_errors(outfile, app=None): """ diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index bdb43db9f3..276f9a4738 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -2,8 +2,6 @@ Module for abstract serializer/unserializer base classes. """ -from io import BytesIO - from django.db import models from django.utils.encoding import smart_text from django.utils import six @@ -35,7 +33,7 @@ class Serializer(object): """ self.options = options - self.stream = options.pop("stream", BytesIO()) + self.stream = options.pop("stream", six.StringIO()) self.selected_fields = options.pop("fields", None) self.use_natural_keys = options.pop("use_natural_keys", False) @@ -125,7 +123,7 @@ class Deserializer(object): """ self.options = options if isinstance(stream_or_string, six.string_types): - self.stream = BytesIO(stream_or_string) + self.stream = six.StringIO(stream_or_string) else: self.stream = stream_or_string # hack to make sure that the models have all been loaded before diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index 3bac24d33a..ed65f2922c 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -12,7 +12,6 @@ import json from django.core.serializers.base import DeserializationError from django.core.serializers.python import Serializer as PythonSerializer from django.core.serializers.python import Deserializer as PythonDeserializer -from django.utils.encoding import smart_bytes from django.utils import six from django.utils.timezone import is_aware @@ -61,13 +60,12 @@ def Deserializer(stream_or_string, **options): """ Deserialize a stream or string of JSON data. """ + if not isinstance(stream_or_string, (bytes, six.string_types)): + stream_or_string = stream_or_string.read() if isinstance(stream_or_string, bytes): stream_or_string = stream_or_string.decode('utf-8') try: - if isinstance(stream_or_string, six.string_types): - objects = json.loads(stream_or_string) - else: - objects = json.load(stream_or_string) + objects = json.loads(stream_or_string) for obj in PythonDeserializer(objects, **options): yield obj except GeneratorExit: diff --git a/django/core/serializers/pyyaml.py b/django/core/serializers/pyyaml.py index 9be1ea4492..4c11626bad 100644 --- a/django/core/serializers/pyyaml.py +++ b/django/core/serializers/pyyaml.py @@ -12,7 +12,6 @@ from django.db import models from django.core.serializers.base import DeserializationError from django.core.serializers.python import Serializer as PythonSerializer from django.core.serializers.python import Deserializer as PythonDeserializer -from django.utils.encoding import smart_bytes from django.utils import six diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 2db4e5ef6a..19b287af87 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -7,6 +7,8 @@ This is a simple server for use in testing or debugging Django apps. It hasn't been reviewed for security issues. DON'T USE IT FOR PRODUCTION USE! """ +from __future__ import unicode_literals + import os import socket import sys @@ -71,12 +73,12 @@ class WSGIServerException(Exception): class ServerHandler(simple_server.ServerHandler, object): - error_status = "500 INTERNAL SERVER ERROR" + error_status = str("500 INTERNAL SERVER ERROR") def write(self, data): - """'write()' callable as specified by PEP 333""" + """'write()' callable as specified by PEP 3333""" - assert isinstance(data, str), "write() argument must be string" + assert isinstance(data, bytes), "write() argument must be bytestring" if not self.status: raise AssertionError("write() before start_response()") @@ -200,7 +202,7 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler, object): def run(addr, port, wsgi_handler, ipv6=False, threading=False): server_address = (addr, port) if threading: - httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, WSGIServer), {}) + httpd_cls = type(str('WSGIServer'), (socketserver.ThreadingMixIn, WSGIServer), {}) else: httpd_cls = WSGIServer httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) diff --git a/django/core/signing.py b/django/core/signing.py index 9ab8c5b8b0..147e54780c 100644 --- a/django/core/signing.py +++ b/django/core/signing.py @@ -32,6 +32,9 @@ start of the base64 JSON. There are 65 url-safe characters: the 64 used by url-safe base64 and the ':'. These functions make use of all of them. """ + +from __future__ import unicode_literals + import base64 import json import time @@ -41,7 +44,7 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils import baseconv from django.utils.crypto import constant_time_compare, salted_hmac -from django.utils.encoding import force_text, smart_bytes +from django.utils.encoding import force_bytes, force_str, force_text from django.utils.importlib import import_module @@ -60,11 +63,11 @@ class SignatureExpired(BadSignature): def b64_encode(s): - return base64.urlsafe_b64encode(s).strip('=') + return base64.urlsafe_b64encode(s).strip(b'=') def b64_decode(s): - pad = '=' * (-len(s) % 4) + pad = b'=' * (-len(s) % 4) return base64.urlsafe_b64decode(s + pad) @@ -114,7 +117,7 @@ def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, value or re-using a salt value across different parts of your application without good cause is a security risk. """ - data = serializer().dumps(obj) + data = force_bytes(serializer().dumps(obj)) # Flag for if it's been compressed or not is_compressed = False @@ -127,7 +130,7 @@ def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, is_compressed = True base64d = b64_encode(data) if is_compressed: - base64d = '.' + base64d + base64d = b'.' + base64d return TimestampSigner(key, salt=salt).sign(base64d) @@ -135,35 +138,40 @@ def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, ma """ Reverse of dumps(), raises BadSignature if signature fails """ - base64d = smart_bytes( - TimestampSigner(key, salt=salt).unsign(s, max_age=max_age)) + # TimestampSigner.unsign always returns unicode but base64 and zlib + # compression operate on bytes. + base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age)) decompress = False - if base64d[0] == '.': + if base64d[0] == b'.': # It's compressed; uncompress it first base64d = base64d[1:] decompress = True data = b64_decode(base64d) if decompress: data = zlib.decompress(data) - return serializer().loads(data) + return serializer().loads(force_str(data)) class Signer(object): + def __init__(self, key=None, sep=':', salt=None): - self.sep = sep - self.key = key or settings.SECRET_KEY - self.salt = salt or ('%s.%s' % - (self.__class__.__module__, self.__class__.__name__)) + # Use of native strings in all versions of Python + self.sep = str(sep) + self.key = str(key or settings.SECRET_KEY) + self.salt = str(salt or + '%s.%s' % (self.__class__.__module__, self.__class__.__name__)) def signature(self, value): - return base64_hmac(self.salt + 'signer', value, self.key) + signature = base64_hmac(self.salt + 'signer', value, self.key) + # Convert the signature from bytes to str only on Python 3 + return force_str(signature) def sign(self, value): - value = smart_bytes(value) - return '%s%s%s' % (value, self.sep, self.signature(value)) + value = force_str(value) + return str('%s%s%s') % (value, self.sep, self.signature(value)) def unsign(self, signed_value): - signed_value = smart_bytes(signed_value) + signed_value = force_str(signed_value) if not self.sep in signed_value: raise BadSignature('No "%s" found in value' % self.sep) value, sig = signed_value.rsplit(self.sep, 1) @@ -178,8 +186,9 @@ class TimestampSigner(Signer): return baseconv.base62.encode(int(time.time())) def sign(self, value): - value = smart_bytes('%s%s%s' % (value, self.sep, self.timestamp())) - return '%s%s%s' % (value, self.sep, self.signature(value)) + value = force_str(value) + value = str('%s%s%s') % (value, self.sep, self.timestamp()) + return super(TimestampSigner, self).sign(value) def unsign(self, value, max_age=None): result = super(TimestampSigner, self).unsign(value) diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 2fe744e8eb..af3df83d0a 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -14,7 +14,7 @@ from threading import local from django.http import Http404 from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.utils.datastructures import MultiValueDict -from django.utils.encoding import iri_to_uri, force_text, smart_bytes +from django.utils.encoding import force_str, force_text, iri_to_uri from django.utils.functional import memoize, lazy from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule @@ -89,18 +89,11 @@ def get_callable(lookup_view, can_fail=False): """ if not callable(lookup_view): mod_name, func_name = get_mod_func(lookup_view) + if func_name == '': + return lookup_view + try: - if func_name != '': - lookup_view = getattr(import_module(mod_name), func_name) - if not callable(lookup_view): - raise ViewDoesNotExist( - "Could not import %s.%s. View is not callable." % - (mod_name, func_name)) - except AttributeError: - if not can_fail: - raise ViewDoesNotExist( - "Could not import %s. View does not exist in module %s." % - (lookup_view, mod_name)) + mod = import_module(mod_name) except ImportError: parentmod, submod = get_mod_func(mod_name) if (not can_fail and submod != '' and @@ -110,6 +103,18 @@ def get_callable(lookup_view, can_fail=False): (lookup_view, mod_name)) if not can_fail: raise + else: + try: + lookup_view = getattr(mod, func_name) + if not callable(lookup_view): + raise ViewDoesNotExist( + "Could not import %s.%s. View is not callable." % + (mod_name, func_name)) + except AttributeError: + if not can_fail: + raise ViewDoesNotExist( + "Could not import %s. View does not exist in module %s." % + (lookup_view, mod_name)) return lookup_view get_callable = memoize(get_callable, _callable_cache, 1) @@ -190,7 +195,7 @@ class RegexURLPattern(LocaleRegexProvider): self.name = name def __repr__(self): - return smart_bytes('<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)) + return force_str('<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)) def add_prefix(self, prefix): """ @@ -240,7 +245,14 @@ class RegexURLResolver(LocaleRegexProvider): self._app_dict = {} def __repr__(self): - return smart_bytes('<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)) + if isinstance(self.urlconf_name, list) and len(self.urlconf_name): + # Don't bother to output the whole list, it can be huge + urlconf_repr = '<%s list>' % self.urlconf_name[0].__class__.__name__ + else: + urlconf_repr = repr(self.urlconf_name) + return force_str('<%s %s (%s:%s) %s>' % ( + self.__class__.__name__, urlconf_repr, self.app_name, + self.namespace, self.regex.pattern)) def _populate(self): lookups = MultiValueDict() diff --git a/django/core/validators.py b/django/core/validators.py index 456fa3cec5..317e3880bf 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -8,7 +8,7 @@ except ImportError: # Python 2 from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ -from django.utils.encoding import smart_text +from django.utils.encoding import force_text from django.utils.ipv6 import is_valid_ipv6_address from django.utils import six @@ -36,7 +36,7 @@ class RegexValidator(object): """ Validates that the input matches the regular expression. """ - if not self.regex.search(smart_text(value)): + if not self.regex.search(force_text(value)): raise ValidationError(self.message, code=self.code) class URLValidator(RegexValidator): @@ -44,7 +44,8 @@ class URLValidator(RegexValidator): r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) @@ -54,10 +55,10 @@ class URLValidator(RegexValidator): except ValidationError as e: # Trivial case failed. Try for possible IDN domain if value: - value = smart_text(value) + value = force_text(value) scheme, netloc, path, query, fragment = urlsplit(value) try: - netloc = netloc.encode('idna') # IDN -> ACE + netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE except UnicodeError: # invalid domain part raise e url = urlunsplit((scheme, netloc, path, query, fragment)) @@ -84,7 +85,7 @@ class EmailValidator(RegexValidator): if value and '@' in value: parts = value.split('@') try: - parts[-1] = parts[-1].encode('idna') + parts[-1] = parts[-1].encode('idna').decode('ascii') except UnicodeError: raise e super(EmailValidator, self).__call__('@'.join(parts)) @@ -99,7 +100,7 @@ email_re = re.compile( r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) validate_email = EmailValidator(email_re, _('Enter a valid e-mail address.'), 'invalid') -slug_re = re.compile(r'^[-\w]+$') +slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 5719f51445..efea9f802e 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1,7 +1,7 @@ from django.db.utils import DatabaseError try: - import thread + from django.utils.six.moves import _thread as thread except ImportError: from django.utils.six.moves import _dummy_thread as thread from contextlib import contextmanager @@ -47,6 +47,8 @@ class BaseDatabaseWrapper(object): def __ne__(self, other): return not self == other + __hash__ = object.__hash__ + def _commit(self): if self.connection is not None: return self.connection.commit() @@ -621,7 +623,7 @@ class BaseDatabaseOperations(object): exists for database backends to provide a better implementation according to their own quoting schemes. """ - from django.utils.encoding import smart_text, force_text + from django.utils.encoding import force_text # Convert params to contain Unicode values. to_unicode = lambda s: force_text(s, strings_only=True, errors='replace') @@ -630,7 +632,7 @@ class BaseDatabaseOperations(object): else: u_params = dict([(to_unicode(k), to_unicode(v)) for k, v in params.items()]) - return smart_text(sql) % u_params + return force_text(sql) % u_params def last_insert_id(self, cursor, table_name, pk_name): """ @@ -814,8 +816,8 @@ class BaseDatabaseOperations(object): def prep_for_like_query(self, x): """Prepares a value for use in a LIKE query.""" - from django.utils.encoding import smart_text - return smart_text(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_") + from django.utils.encoding import force_text + return force_text(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_") # Same as prep_for_like_query(), but called for "iexact" matches, which # need not necessarily be implemented using "LIKE" in the backend. @@ -892,19 +894,21 @@ class BaseDatabaseOperations(object): return self.year_lookup_bounds(value) def convert_values(self, value, field): - """Coerce the value returned by the database backend into a consistent type that - is compatible with the field type. + """ + Coerce the value returned by the database backend into a consistent type + that is compatible with the field type. """ internal_type = field.get_internal_type() if internal_type == 'DecimalField': return value - elif internal_type and internal_type.endswith('IntegerField') or internal_type == 'AutoField': + elif internal_type == 'FloatField': + return float(value) + elif (internal_type and (internal_type.endswith('IntegerField') + or internal_type == 'AutoField')): return int(value) elif internal_type in ('DateField', 'DateTimeField', 'TimeField'): return value - # No field, or the field isn't known to be a decimal or integer - # Default to a float - return float(value) + return value def check_aggregate_support(self, aggregate_func): """Check that the backend supports the provided aggregate @@ -1003,7 +1007,7 @@ class BaseDatabaseIntrospection(object): for model in models.get_models(app): if router.allow_syncdb(self.connection.alias, model): all_models.append(model) - tables = map(self.table_name_converter, tables) + tables = list(map(self.table_name_converter, tables)) return set([ m for m in all_models if self.table_name_converter(m._meta.db_table) in tables diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index b3e09edc57..10649b64b9 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -39,7 +39,7 @@ from django.db.backends.mysql.introspection import DatabaseIntrospection from django.db.backends.mysql.validation import DatabaseValidation from django.db.backends.mysql.schema import DatabaseSchemaEditor from django.utils.functional import cached_property -from django.utils.safestring import SafeString, SafeUnicode +from django.utils.safestring import SafeBytes, SafeText from django.utils import six from django.utils import timezone @@ -76,7 +76,7 @@ def adapt_datetime_with_timezone_support(value, conv): # MySQLdb-1.2.1 returns TIME columns as timedelta -- they are more like # timedelta in terms of actual behavior as they are signed and include days -- # and Django expects time, so we still need to override that. We also need to -# add special handling for SafeUnicode and SafeString as MySQLdb's type +# add special handling for SafeText and SafeBytes as MySQLdb's type # checking is too tight to catch those (see Django ticket #6052). # Finally, MySQLdb always returns naive datetime objects. However, when # timezone support is active, Django expects timezone-aware datetime objects. @@ -403,8 +403,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): kwargs['client_flag'] = CLIENT.FOUND_ROWS kwargs.update(settings_dict['OPTIONS']) self.connection = Database.connect(**kwargs) - self.connection.encoders[SafeUnicode] = self.connection.encoders[six.text_type] - self.connection.encoders[SafeString] = self.connection.encoders[bytes] + self.connection.encoders[SafeText] = self.connection.encoders[six.text_type] + self.connection.encoders[SafeBytes] = self.connection.encoders[bytes] connection_created.send(sender=self.__class__, connection=self) cursor = self.connection.cursor() if new_connection: diff --git a/django/db/backends/mysql/compiler.py b/django/db/backends/mysql/compiler.py index bd4105ff8e..d8e9b3a202 100644 --- a/django/db/backends/mysql/compiler.py +++ b/django/db/backends/mysql/compiler.py @@ -1,10 +1,16 @@ +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + from django.db.models.sql import compiler + class SQLCompiler(compiler.SQLCompiler): def resolve_columns(self, row, fields=()): values = [] - index_extra_select = len(self.query.extra_select.keys()) - for value, field in map(None, row[index_extra_select:], fields): + index_extra_select = len(self.query.extra_select) + for value, field in zip_longest(row[index_extra_select:], fields): if (field and field.get_internal_type() in ("BooleanField", "NullBooleanField") and value in (0, 1)): value = bool(value) diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index 310f270a0b..1fc55115a2 100644 --- a/django/db/backends/mysql/introspection.py +++ b/django/db/backends/mysql/introspection.py @@ -1,8 +1,9 @@ +import re +from .base import FIELD_TYPE + from django.db.backends import BaseDatabaseIntrospection from django.utils import six -from MySQLdb import ProgrammingError, OperationalError -from MySQLdb.constants import FIELD_TYPE -import re + foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") @@ -35,9 +36,20 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): return [row[0] for row in cursor.fetchall()] def get_table_description(self, cursor, table_name): - "Returns a description of the table, with the DB-API cursor.description interface." + """ + Returns a description of the table, with the DB-API cursor.description interface." + """ + # varchar length returned by cursor.description is an internal length, + # not visible length (#5725), use information_schema database to fix this + cursor.execute(""" + SELECT column_name, character_maximum_length FROM information_schema.columns + WHERE table_name = %s AND table_schema = DATABASE() + AND character_maximum_length IS NOT NULL""", [table_name]) + length_map = dict(cursor.fetchall()) + cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return cursor.description + return [line[:3] + (length_map.get(line[0], line[3]),) + line[4:] + for line in cursor.description] def _name_to_index(self, cursor, table_name): """ diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 89cad12b6c..6bf2e815a7 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -10,8 +10,6 @@ import decimal import sys import warnings -from django.utils import six - def _setup_environment(environ): import platform # Cygwin requires some special voodoo to set the environment variables @@ -53,7 +51,7 @@ from django.db.backends.signals import connection_created from django.db.backends.oracle.client import DatabaseClient from django.db.backends.oracle.creation import DatabaseCreation from django.db.backends.oracle.introspection import DatabaseIntrospection -from django.utils.encoding import smart_bytes, force_text +from django.utils.encoding import force_bytes, force_text from django.utils import six from django.utils import timezone @@ -66,7 +64,7 @@ IntegrityError = Database.IntegrityError if int(Database.version.split('.', 1)[0]) >= 5 and not hasattr(Database, 'UNICODE'): convert_unicode = force_text else: - convert_unicode = smart_bytes + convert_unicode = force_bytes class DatabaseFeatures(BaseDatabaseFeatures): @@ -221,7 +219,10 @@ WHEN (new.%(col_name)s IS NULL) def last_executed_query(self, cursor, sql, params): # http://cx-oracle.sourceforge.net/html/cursor.html#Cursor.statement # The DB API definition does not define this attribute. - return cursor.statement.decode("utf-8") + if six.PY3: + return cursor.statement + else: + return cursor.statement.decode("utf-8") def last_insert_id(self, cursor, table_name, pk_name): sq_name = self._get_sequence_name(table_name) @@ -594,10 +595,16 @@ class OracleParam(object): param = timezone.make_aware(param, default_timezone) param = param.astimezone(timezone.utc).replace(tzinfo=None) + # Oracle doesn't recognize True and False correctly in Python 3. + # The conversion done below works both in 2 and 3. + if param is True: + param = "1" + elif param is False: + param = "0" if hasattr(param, 'bind_parameter'): - self.smart_bytes = param.bind_parameter(cursor) + self.force_bytes = param.bind_parameter(cursor) else: - self.smart_bytes = convert_unicode(param, cursor.charset, + self.force_bytes = convert_unicode(param, cursor.charset, strings_only) if hasattr(param, 'input_size'): # If parameter has `input_size` attribute, use that. @@ -676,7 +683,7 @@ class FormatStylePlaceholderCursor(object): self.setinputsizes(*sizes) def _param_generator(self, params): - return [p.smart_bytes for p in params] + return [p.force_bytes for p in params] def execute(self, query, params=None): if params is None: diff --git a/django/db/backends/oracle/compiler.py b/django/db/backends/oracle/compiler.py index c7b10429c2..24030cdffc 100644 --- a/django/db/backends/oracle/compiler.py +++ b/django/db/backends/oracle/compiler.py @@ -1,4 +1,9 @@ from django.db.models.sql import compiler +# The izip_longest was renamed to zip_longest in py3 +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest class SQLCompiler(compiler.SQLCompiler): @@ -10,10 +15,10 @@ class SQLCompiler(compiler.SQLCompiler): rn_offset = 1 else: rn_offset = 0 - index_start = rn_offset + len(self.query.extra_select.keys()) + index_start = rn_offset + len(self.query.extra_select) values = [self.query.convert_values(v, None, connection=self.connection) for v in row[rn_offset:index_start]] - for value, field in map(None, row[index_start:], fields): + for value, field in zip_longest(row[index_start:], fields): values.append(self.query.convert_values(value, field, connection=self.connection)) return tuple(values) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 005c0a04b1..4cad6e06a7 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -15,7 +15,7 @@ from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor from django.utils.log import getLogger -from django.utils.safestring import SafeUnicode, SafeString +from django.utils.safestring import SafeText, SafeBytes from django.utils import six from django.utils.timezone import utc @@ -30,8 +30,8 @@ DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) -psycopg2.extensions.register_adapter(SafeString, psycopg2.extensions.QuotedString) -psycopg2.extensions.register_adapter(SafeUnicode, psycopg2.extensions.QuotedString) +psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString) +psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) logger = getLogger('django.db.backends') diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py index 1a08984bd2..916073c09f 100644 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ b/django/db/backends/postgresql_psycopg2/introspection.py @@ -45,8 +45,8 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): WHERE table_name = %s""", [table_name]) null_map = dict(cursor.fetchall()) cursor.execute("SELECT * FROM %s LIMIT 1" % self.connection.ops.quote_name(table_name)) - return [tuple([item for item in line[:6]] + [null_map[line[0]]=='YES']) - for line in cursor.description] + return [line[:6] + (null_map[line[0]]=='YES',) + for line in cursor.description] def get_relations(self, cursor, table_name): """ diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 7918d5d3ef..d0a6fda78e 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -21,7 +21,7 @@ from django.db.backends.sqlite3.introspection import DatabaseIntrospection from django.db.backends.sqlite3.schema import DatabaseSchemaEditor from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.functional import cached_property -from django.utils.safestring import SafeString +from django.utils.safestring import SafeBytes from django.utils import six from django.utils import timezone @@ -57,13 +57,21 @@ def adapt_datetime_with_timezone_support(value): value = value.astimezone(timezone.utc).replace(tzinfo=None) return value.isoformat(str(" ")) -Database.register_converter(str("bool"), lambda s: str(s) == '1') -Database.register_converter(str("time"), parse_time) -Database.register_converter(str("date"), parse_date) -Database.register_converter(str("datetime"), parse_datetime_with_timezone_support) -Database.register_converter(str("timestamp"), parse_datetime_with_timezone_support) -Database.register_converter(str("TIMESTAMP"), parse_datetime_with_timezone_support) -Database.register_converter(str("decimal"), util.typecast_decimal) +def decoder(conv_func): + """ The Python sqlite3 interface returns always byte strings. + This function converts the received value to a regular string before + passing it to the receiver function. + """ + return lambda s: conv_func(s.decode('utf-8')) + +Database.register_converter(str("bool"), decoder(lambda s: s == '1')) +Database.register_converter(str("time"), decoder(parse_time)) +Database.register_converter(str("date"), decoder(parse_date)) +Database.register_converter(str("datetime"), decoder(parse_datetime_with_timezone_support)) +Database.register_converter(str("timestamp"), decoder(parse_datetime_with_timezone_support)) +Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime_with_timezone_support)) +Database.register_converter(str("decimal"), decoder(util.typecast_decimal)) + Database.register_adapter(datetime.datetime, adapt_datetime_with_timezone_support) Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal) if Database.version_info >= (2, 4, 1): @@ -73,7 +81,7 @@ if Database.version_info >= (2, 4, 1): # slow-down, this adapter is only registered for sqlite3 versions # needing it (Python 2.6 and up). Database.register_adapter(str, lambda s: s.decode('utf-8')) - Database.register_adapter(SafeString, lambda s: s.decode('utf-8')) + Database.register_adapter(SafeBytes, lambda s: s.decode('utf-8')) class DatabaseFeatures(BaseDatabaseFeatures): # SQLite cannot handle us only partially reading from a cursor's result set diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index 8135f3548c..1df4c18c1c 100644 --- a/django/db/backends/sqlite3/introspection.py +++ b/django/db/backends/sqlite3/introspection.py @@ -1,6 +1,14 @@ import re from django.db.backends import BaseDatabaseIntrospection +field_size_re = re.compile(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$') + +def get_field_size(name): + """ Extract the size number from a "varchar(11)" type name """ + m = field_size_re.search(name) + return int(m.group(1)) if m else None + + # This light wrapper "fakes" a dictionary interface, because some SQLite data # types include variables in them -- e.g. "varchar(30)" -- and can't be matched # as a simple dictionary lookup. @@ -32,10 +40,9 @@ class FlexibleFieldLookupDict(object): try: return self.base_data_types_reverse[key] except KeyError: - import re - m = re.search(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$', key) - if m: - return ('CharField', {'max_length': int(m.group(1))}) + size = get_field_size(key) + if size is not None: + return ('CharField', {'max_length': size}) raise KeyError class DatabaseIntrospection(BaseDatabaseIntrospection): @@ -53,7 +60,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): def get_table_description(self, cursor, table_name): "Returns a description of the table, with the DB-API cursor.description interface." - return [(info['name'], info['type'], None, None, None, None, + return [(info['name'], info['type'], None, info['size'], None, None, info['null_ok']) for info in self._table_info(cursor, table_name)] def get_relations(self, cursor, table_name): @@ -171,6 +178,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): # cid, name, type, notnull, dflt_value, pk return [{'name': field[1], 'type': field[2], + 'size': get_field_size(field[2]), 'null_ok': not field[3], 'pk': field[5] # undocumented } for field in cursor.fetchall()] diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 775bb3e182..75d4d07a66 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -6,6 +6,7 @@ import hashlib from time import time from django.conf import settings +from django.utils.encoding import force_bytes from django.utils.log import getLogger from django.utils.timezone import utc @@ -137,7 +138,7 @@ def truncate_name(name, length=None, hash_len=4): if length is None or len(name) <= length: return name - hsh = hashlib.md5(name).hexdigest()[:hash_len] + hsh = hashlib.md5(force_bytes(name)).hexdigest()[:hash_len] return '%s%s' % (name[:length-hash_len], hsh) def format_number(value, max_digits, decimal_places): diff --git a/django/db/models/base.py b/django/db/models/base.py index 569b8e876c..62024c8ee4 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -23,7 +23,7 @@ from django.db.models import signals from django.db.models.loading import register_models, get_model from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry -from django.utils.encoding import smart_bytes, force_text +from django.utils.encoding import force_str, force_text from django.utils import six from django.utils.text import get_text_list, capfirst @@ -404,10 +404,10 @@ class Model(six.with_metaclass(ModelBase, object)): u = six.text_type(self) except (UnicodeEncodeError, UnicodeDecodeError): u = '[Bad Unicode data]' - return smart_bytes('<%s: %s>' % (self.__class__.__name__, u)) + return force_str('<%s: %s>' % (self.__class__.__name__, u)) def __str__(self): - if hasattr(self, '__unicode__'): + if not six.PY3 and hasattr(self, '__unicode__'): return force_text(self).encode('utf-8') return '%s object' % self.__class__.__name__ @@ -475,6 +475,7 @@ class Model(six.with_metaclass(ModelBase, object)): that the "save" must be an SQL insert or update (or equivalent for non-SQL backends), respectively. Normally, they should not be set. """ + using = using or router.db_for_write(self.__class__, instance=self) if force_insert and (force_update or update_fields): raise ValueError("Cannot force both insert and updating in model saving.") @@ -502,6 +503,23 @@ class Model(six.with_metaclass(ModelBase, object)): "model or are m2m fields: %s" % ', '.join(non_model_fields)) + # If saving to the same database, and this model is deferred, then + # automatically do a "update_fields" save on the loaded fields. + elif not force_insert and self._deferred and using == self._state.db: + field_names = set() + for field in self._meta.fields: + if not field.primary_key and not hasattr(field, 'through'): + field_names.add(field.attname) + deferred_fields = [ + f.attname for f in self._meta.fields + if f.attname not in self.__dict__ + and isinstance(self.__class__.__dict__[f.attname], + DeferredAttribute)] + + loaded_fields = field_names.difference(deferred_fields) + if loaded_fields: + update_fields = frozenset(loaded_fields) + self.save_base(using=using, force_insert=force_insert, force_update=force_update, update_fields=update_fields) save.alters_data = True @@ -636,7 +654,7 @@ class Model(six.with_metaclass(ModelBase, object)): raise ValueError("get_next/get_previous cannot be used on unsaved objects.") op = is_next and 'gt' or 'lt' order = not is_next and '-' or '' - param = smart_bytes(getattr(self, field.attname)) + param = force_text(getattr(self, field.attname)) q = Q(**{'%s__%s' % (field.name, op): param}) q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk}) qs = self.__class__._default_manager.using(self._state.db).filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order) @@ -761,7 +779,7 @@ class Model(six.with_metaclass(ModelBase, object)): lookup_kwargs[str(field_name)] = lookup_value # some fields were skipped, no reason to do the check - if len(unique_check) != len(lookup_kwargs.keys()): + if len(unique_check) != len(lookup_kwargs): continue qs = model_class._default_manager.filter(**lookup_kwargs) @@ -837,7 +855,7 @@ class Model(six.with_metaclass(ModelBase, object)): } # unique_together else: - field_labels = map(lambda f: capfirst(opts.get_field(f).verbose_name), unique_check) + field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check] field_labels = get_text_list(field_labels, _('and')) return _("%(model_name)s with this %(field_label)s already exists.") % { 'model_name': six.text_type(model_name), diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index a71f4a33ef..639ef6ee10 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -58,8 +58,9 @@ class ExpressionNode(tree.Node): def __mul__(self, other): return self._combine(other, self.MUL, False) - def __div__(self, other): + def __truediv__(self, other): return self._combine(other, self.DIV, False) + __div__ = __truediv__ # Python 2 compatibility def __mod__(self, other): return self._combine(other, self.MOD, False) @@ -79,8 +80,9 @@ class ExpressionNode(tree.Node): def __rmul__(self, other): return self._combine(other, self.MUL, True) - def __rdiv__(self, other): + def __rtruediv__(self, other): return self._combine(other, self.DIV, True) + __rdiv__ = __rtruediv__ # Python 2 compatibility def __rmod__(self, other): return self._combine(other, self.MOD, True) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index d07851bbf5..58ae3413f3 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1047,13 +1047,14 @@ class GenericIPAddressField(Field): description = _("IP address") default_error_messages = {} - def __init__(self, protocol='both', unpack_ipv4=False, *args, **kwargs): + def __init__(self, verbose_name=None, name=None, protocol='both', + unpack_ipv4=False, *args, **kwargs): self.unpack_ipv4 = unpack_ipv4 self.default_validators, invalid_error_message = \ validators.ip_address_validators(protocol, unpack_ipv4) self.default_error_messages['invalid'] = invalid_error_message kwargs['max_length'] = 39 - Field.__init__(self, *args, **kwargs) + Field.__init__(self, verbose_name, name, *args, **kwargs) def get_internal_type(self): return "GenericIPAddressField" diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index ad4c36ca0d..f16632ef25 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -8,7 +8,7 @@ from django.core.files.base import File from django.core.files.storage import default_storage from django.core.files.images import ImageFile from django.db.models import signals -from django.utils.encoding import force_text, smart_bytes +from django.utils.encoding import force_str, force_text from django.utils import six from django.utils.translation import ugettext_lazy as _ @@ -280,7 +280,7 @@ class FileField(Field): setattr(cls, self.name, self.descriptor_class(self)) def get_directory_name(self): - return os.path.normpath(force_text(datetime.datetime.now().strftime(smart_bytes(self.upload_to)))) + return os.path.normpath(force_text(datetime.datetime.now().strftime(force_str(self.upload_to)))) def get_filename(self, filename): return os.path.normpath(self.storage.get_valid_name(os.path.basename(filename))) diff --git a/django/db/models/fields/subclassing.py b/django/db/models/fields/subclassing.py index d4d7d75222..e6153aefe0 100644 --- a/django/db/models/fields/subclassing.py +++ b/django/db/models/fields/subclassing.py @@ -2,8 +2,9 @@ Convenience routines for creating non-trivial Field subclasses, as well as backwards compatibility utilities. -Add SubfieldBase as the __metaclass__ for your Field subclass, implement -to_python() and the other necessary methods and everything will work seamlessly. +Add SubfieldBase as the metaclass for your Field subclass, implement +to_python() and the other necessary methods and everything will work +seamlessly. """ class SubfieldBase(type): diff --git a/django/db/models/options.py b/django/db/models/options.py index 2e8ccb49ce..6814ce27ff 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import re from bisect import bisect @@ -8,9 +10,10 @@ from django.db.models.fields import AutoField, FieldDoesNotExist from django.db.models.fields.proxy import OrderWrt from django.db.models.loading import get_models, app_cache_ready from django.utils.translation import activate, deactivate_all, get_language, string_concat -from django.utils.encoding import force_text, smart_bytes +from django.utils.encoding import force_text, smart_text from django.utils.datastructures import SortedDict from django.utils import six +from django.utils.encoding import python_2_unicode_compatible # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip() @@ -20,6 +23,7 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'order_with_respect_to', 'app_label', 'db_tablespace', 'abstract', 'managed', 'proxy', 'auto_created') +@python_2_unicode_compatible class Options(object): def __init__(self, meta, app_label=None): self.local_fields, self.local_many_to_many = [], [] @@ -199,7 +203,7 @@ class Options(object): return '' % self.object_name def __str__(self): - return "%s.%s" % (smart_bytes(self.app_label), smart_bytes(self.module_name)) + return "%s.%s" % (smart_text(self.app_label), smart_text(self.module_name)) def verbose_name_raw(self): """ @@ -383,7 +387,7 @@ class Options(object): predicates.append(lambda k, v: not k.field.rel.is_hidden()) cache = (self._related_objects_proxy_cache if include_proxy_eq else self._related_objects_cache) - return filter(lambda t: all([p(*t) for p in predicates]), cache.items()) + return [t for t in cache.items() if all(p(*t) for p in predicates)] def _fill_related_objects_cache(self): cache = SortedDict() diff --git a/django/db/models/query.py b/django/db/models/query.py index e8d6ae2a7b..05c049b31f 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -472,7 +472,7 @@ class QuerySet(object): return self.get(**lookup), False except self.model.DoesNotExist: # Re-raise the IntegrityError with its original traceback. - six.reraise(exc_info[1], None, exc_info[2]) + six.reraise(*exc_info) def latest(self, field_name=None): """ @@ -498,9 +498,7 @@ class QuerySet(object): "Cannot use 'limit' or 'offset' with in_bulk" if not id_list: return {} - qs = self._clone() - qs.query.add_filter(('pk__in', id_list)) - qs.query.clear_ordering(force_empty=True) + qs = self.filter(pk__in=id_list).order_by() return dict([(obj._get_pk_val(), obj) for obj in qs]) def delete(self): @@ -1107,7 +1105,7 @@ class ValuesListQuerySet(ValuesQuerySet): # If a field list has been specified, use it. Otherwise, use the # full list of fields, including extras and aggregates. if self._fields: - fields = list(self._fields) + filter(lambda f: f not in self._fields, aggregate_names) + fields = list(self._fields) + [f for f in aggregate_names if f not in self._fields] else: fields = names diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 1311ea546c..cf7cad29bd 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -470,9 +470,7 @@ class SQLCompiler(object): # Must use left outer joins for nullable fields and their relations. # Ordering or distinct must not affect the returned set, and INNER # JOINS for nullable fields could do this. - if joins_to_promote: - self.query.promote_alias_chain(joins_to_promote, - self.query.alias_map[joins_to_promote[0]].join_type == self.query.LOUTER) + self.query.promote_joins(joins_to_promote) return field, col, alias, joins, opts def _final_join_removal(self, col, alias): @@ -645,8 +643,6 @@ class SQLCompiler(object): alias_chain.append(alias) for (dupe_opts, dupe_col) in dupe_set: self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias) - if self.query.alias_map[root_alias].join_type == self.query.LOUTER: - self.query.promote_alias_chain(alias_chain, True) else: alias = root_alias @@ -663,8 +659,6 @@ class SQLCompiler(object): columns, aliases = self.get_default_columns(start_alias=alias, opts=f.rel.to._meta, as_pairs=True) self.query.related_select_cols.extend(columns) - if self.query.alias_map[alias].join_type == self.query.LOUTER: - self.query.promote_alias_chain(aliases, True) self.query.related_select_fields.extend(f.rel.to._meta.fields) if restricted: next = requested.get(f.name, {}) @@ -738,7 +732,9 @@ class SQLCompiler(object): self.query.related_select_fields.extend(model._meta.fields) next = requested.get(f.related_query_name(), {}) - new_nullable = f.null or None + # Use True here because we are looking at the _reverse_ side of + # the relation, which is always nullable. + new_nullable = True self.fill_related_selections(model._meta, table, cur_depth+1, used, next, restricted, new_nullable) @@ -786,7 +782,7 @@ class SQLCompiler(object): row = self.resolve_columns(row, fields) if has_aggregate_select: - aggregate_start = len(self.query.extra_select.keys()) + len(self.query.select) + aggregate_start = len(self.query.extra_select) + len(self.query.select) aggregate_end = aggregate_start + len(self.query.aggregate_select) row = tuple(row[:aggregate_start]) + tuple([ self.query.resolve_aggregate(value, aggregate, self.connection) diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index 612755a012..b9cf2c96fd 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -1,7 +1,7 @@ from collections import namedtuple import re -# Valid query types (a dictionary is used for speedy lookups). +# Valid query types (a set is used for speedy lookups). QUERY_TERMS = set([ 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index be257a5410..1915efa7b9 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -505,7 +505,7 @@ class Query(object): # Again, some of the tables won't have aliases due to # the trimming of unnecessary tables. if self.alias_refcount.get(alias) or rhs.alias_refcount.get(alias): - self.promote_alias(alias, True) + self.promote_joins([alias], True) # Now relabel a copy of the rhs where-clause and add it to the current # one. @@ -682,32 +682,38 @@ class Query(object): """ Decreases the reference count for this alias. """ self.alias_refcount[alias] -= amount - def promote_alias(self, alias, unconditional=False): + def promote_joins(self, aliases, unconditional=False): """ - Promotes the join type of an alias to an outer join if it's possible - for the join to contain NULL values on the left. If 'unconditional' is - False, the join is only promoted if it is nullable, otherwise it is - always promoted. + Promotes recursively the join type of given aliases and its children to + an outer join. If 'unconditional' is False, the join is only promoted if + it is nullable or the parent join is an outer join. - Returns True if the join was promoted by this call. + Note about join promotion: When promoting any alias, we make sure all + joins which start from that alias are promoted, too. When adding a join + in join(), we make sure any join added to already existing LOUTER join + is generated as LOUTER. This ensures we don't ever have broken join + chains which contain first a LOUTER join, then an INNER JOIN, that is + this kind of join should never be generated: a LOUTER b INNER c. The + reason for avoiding this type of join chain is that the INNER after + the LOUTER will effectively remove any effect the LOUTER had. """ - if ((unconditional or self.alias_map[alias].nullable) and - self.alias_map[alias].join_type != self.LOUTER): - data = self.alias_map[alias] - data = data._replace(join_type=self.LOUTER) - self.alias_map[alias] = data - return True - return False - - def promote_alias_chain(self, chain, must_promote=False): - """ - Walks along a chain of aliases, promoting the first nullable join and - any joins following that. If 'must_promote' is True, all the aliases in - the chain are promoted. - """ - for alias in chain: - if self.promote_alias(alias, must_promote): - must_promote = True + aliases = list(aliases) + while aliases: + alias = aliases.pop(0) + parent_alias = self.alias_map[alias].lhs_alias + parent_louter = (parent_alias + and self.alias_map[parent_alias].join_type == self.LOUTER) + already_louter = self.alias_map[alias].join_type == self.LOUTER + if ((unconditional or self.alias_map[alias].nullable + or parent_louter) and not already_louter): + data = self.alias_map[alias]._replace(join_type=self.LOUTER) + self.alias_map[alias] = data + # Join type of 'alias' changed, so re-examine all aliases that + # refer to this one. + aliases.extend( + join for join in self.alias_map.keys() + if (self.alias_map[join].lhs_alias == alias + and join not in aliases)) def reset_refcounts(self, to_counts): """ @@ -726,19 +732,10 @@ class Query(object): then and which ones haven't been used and promotes all of those aliases, plus any children of theirs in the alias tree, to outer joins. """ - # FIXME: There's some (a lot of!) overlap with the similar OR promotion - # in add_filter(). It's not quite identical, but is very similar. So - # pulling out the common bits is something for later. - considered = {} for alias in self.tables: - if alias not in used_aliases: - continue - if (alias not in initial_refcounts or + if alias in used_aliases and (alias not in initial_refcounts or self.alias_refcount[alias] == initial_refcounts[alias]): - parent = self.alias_map[alias].lhs_alias - must_promote = considered.get(parent, False) - promoted = self.promote_alias(alias, must_promote) - considered[alias] = must_promote or promoted + self.promote_joins([alias]) def change_aliases(self, change_map): """ @@ -875,6 +872,9 @@ class Query(object): LOUTER join type. This is used when joining certain types of querysets and Q-objects together. + A join is always created as LOUTER if the lhs alias is LOUTER to make + sure we do not generate chains like a LOUTER b INNER c. + If 'nullable' is True, the join can potentially involve NULL values and is a candidate for promotion (to "left outer") when combining querysets. """ @@ -900,8 +900,8 @@ class Query(object): if self.alias_map[alias].lhs_alias != lhs: continue self.ref_alias(alias) - if promote: - self.promote_alias(alias) + if promote or (lhs and self.alias_map[lhs].join_type == self.LOUTER): + self.promote_joins([alias]) return alias # No reuse is possible, so we need a new alias. @@ -910,7 +910,12 @@ class Query(object): # Not all tables need to be joined to anything. No join type # means the later columns are ignored. join_type = None - elif promote or outer_if_first: + elif (promote or outer_if_first + or self.alias_map[lhs].join_type == self.LOUTER): + # We need to use LOUTER join if asked by promote or outer_if_first, + # or if the LHS table is left-joined in the query. Adding inner join + # to an existing outer join effectively cancels the effect of the + # outer join. join_type = self.LOUTER else: join_type = self.INNER @@ -1004,8 +1009,7 @@ class Query(object): # If the aggregate references a model or field that requires a join, # those joins must be LEFT OUTER - empty join rows must be returned # in order for zeros to be returned for those aggregates. - for column_alias in join_list: - self.promote_alias(column_alias, unconditional=True) + self.promote_joins(join_list, True) col = (join_list[-1], col) else: @@ -1124,7 +1128,7 @@ class Query(object): # If the comparison is against NULL, we may need to use some left # outer joins when creating the join chain. This is only done when # needed, as it's less efficient at the database level. - self.promote_alias_chain(join_list) + self.promote_joins(join_list) join_promote = True # Process the join list to see if we can remove any inner joins from @@ -1155,16 +1159,16 @@ class Query(object): # This means that we are dealing with two different query # subtrees, so we don't need to do any join promotion. continue - join_promote = join_promote or self.promote_alias(join, unconditional) + join_promote = join_promote or self.promote_joins([join], unconditional) if table != join: - table_promote = self.promote_alias(table) + table_promote = self.promote_joins([table]) # We only get here if we have found a table that exists # in the join list, but isn't on the original tables list. # This means we've reached the point where we only have # new tables, so we can break out of this promotion loop. break - self.promote_alias_chain(join_it, join_promote) - self.promote_alias_chain(table_it, table_promote or join_promote) + self.promote_joins(join_it, join_promote) + self.promote_joins(table_it, table_promote or join_promote) if having_clause or force_having: if (alias, col) not in self.group_by: @@ -1176,7 +1180,7 @@ class Query(object): connector) if negate: - self.promote_alias_chain(join_list) + self.promote_joins(join_list) if lookup_type != 'isnull': if len(join_list) > 1: for alias in join_list: @@ -1550,7 +1554,7 @@ class Query(object): N-to-many relation field. """ query = Query(self.model) - query.add_filter(filter_expr, can_reuse=can_reuse) + query.add_filter(filter_expr) query.bump_prefix() query.clear_ordering(True) query.set_start(prefix) @@ -1650,7 +1654,7 @@ class Query(object): final_alias = join.lhs_alias col = join.lhs_join_col joins = joins[:-1] - self.promote_alias_chain(joins[1:]) + self.promote_joins(joins[1:]) self.select.append((final_alias, col)) self.select_fields.append(field) except MultiJoin: diff --git a/django/dispatch/saferef.py b/django/dispatch/saferef.py index d8728e219a..84d1b2183c 100644 --- a/django/dispatch/saferef.py +++ b/django/dispatch/saferef.py @@ -149,9 +149,11 @@ class BoundMethodWeakref(object): self.selfName, self.funcName, ) - + __repr__ = __str__ - + + __hash__ = object.__hash__ + def __bool__( self ): """Whether we are still a valid reference""" return self() is not None diff --git a/django/forms/forms.py b/django/forms/forms.py index 45b758202a..3299c2becc 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -12,7 +12,7 @@ from django.forms.util import flatatt, ErrorDict, ErrorList from django.forms.widgets import Media, media_property, TextInput, Textarea from django.utils.datastructures import SortedDict from django.utils.html import conditional_escape, format_html -from django.utils.encoding import StrAndUnicode, smart_text, force_text +from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe from django.utils import six @@ -68,7 +68,8 @@ class DeclarativeFieldsMetaclass(type): new_class.media = media_property(new_class) return new_class -class BaseForm(StrAndUnicode): +@python_2_unicode_compatible +class BaseForm(object): # This is the main implementation of all the Form logic. Note that this # class is different than Form. See the comments by the Form class for more # information. Any improvements to the form API should be made to *this* @@ -95,7 +96,7 @@ class BaseForm(StrAndUnicode): # self.base_fields. self.fields = copy.deepcopy(self.base_fields) - def __unicode__(self): + def __str__(self): return self.as_table() def __iter__(self): @@ -387,7 +388,8 @@ class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)): # to define a form using declarative syntax. # BaseForm itself has no way of designating self.fields. -class BoundField(StrAndUnicode): +@python_2_unicode_compatible +class BoundField(object): "A Field plus data" def __init__(self, form, field, name): self.form = form @@ -402,7 +404,7 @@ class BoundField(StrAndUnicode): self.label = self.field.label self.help_text = field.help_text or '' - def __unicode__(self): + def __str__(self): """Renders this field as an HTML widget.""" if self.field.show_hidden_initial: return self.as_widget() + self.as_hidden(only_initial=True) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 4ea8dc4ca9..bdcef92dec 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -5,7 +5,7 @@ from django.forms import Form from django.forms.fields import IntegerField, BooleanField from django.forms.util import ErrorList from django.forms.widgets import Media, HiddenInput -from django.utils.encoding import StrAndUnicode +from django.utils.encoding import python_2_unicode_compatible from django.utils.safestring import mark_safe from django.utils import six from django.utils.six.moves import xrange @@ -33,7 +33,8 @@ class ManagementForm(Form): self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput) super(ManagementForm, self).__init__(*args, **kwargs) -class BaseFormSet(StrAndUnicode): +@python_2_unicode_compatible +class BaseFormSet(object): """ A collection of instances of the same Form class. """ @@ -51,7 +52,7 @@ class BaseFormSet(StrAndUnicode): # construct the forms in the formset self._construct_forms() - def __unicode__(self): + def __str__(self): return self.as_table() def __iter__(self): @@ -94,10 +95,11 @@ class BaseFormSet(StrAndUnicode): total_forms = initial_forms + self.extra # Allow all existing related objects/inlines to be displayed, # but don't allow extra beyond max_num. - if initial_forms > self.max_num >= 0: - total_forms = initial_forms - elif total_forms > self.max_num >= 0: - total_forms = self.max_num + if self.max_num is not None: + if initial_forms > self.max_num >= 0: + total_forms = initial_forms + elif total_forms > self.max_num >= 0: + total_forms = self.max_num return total_forms def initial_form_count(self): @@ -107,7 +109,7 @@ class BaseFormSet(StrAndUnicode): else: # Use the length of the inital data if it's there, 0 otherwise. initial_forms = self.initial and len(self.initial) or 0 - if initial_forms > self.max_num >= 0: + if self.max_num is not None and (initial_forms > self.max_num >= 0): initial_forms = self.max_num return initial_forms @@ -252,13 +254,10 @@ class BaseFormSet(StrAndUnicode): errors = property(_get_errors) def _should_delete_form(self, form): - # The way we lookup the value of the deletion field here takes - # more code than we'd like, but the form's cleaned_data will - # not exist if the form is invalid. - field = form.fields[DELETION_FIELD_NAME] - raw_value = form._raw_value(DELETION_FIELD_NAME) - should_delete = field.clean(raw_value) - return should_delete + """ + Returns whether or not the form was marked for deletion. + """ + return form.cleaned_data.get(DELETION_FIELD_NAME, False) def is_valid(self): """ diff --git a/django/forms/models.py b/django/forms/models.py index 0b07d31d9a..1aa49eaaec 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -18,7 +18,6 @@ from django.utils.datastructures import SortedDict from django.utils import six from django.utils.text import get_text_list, capfirst from django.utils.translation import ugettext_lazy as _, ugettext -from django.utils import six __all__ = ( @@ -401,13 +400,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 'formfield_callback': formfield_callback } - form_metaclass = ModelFormMetaclass - - # TODO: this doesn't work under Python 3. - if issubclass(form, BaseModelForm) and hasattr(form, '__metaclass__'): - form_metaclass = form.__metaclass__ - - return form_metaclass(class_name, (form,), form_class_attrs) + # Instatiate type(form) in order to use the same metaclass as form. + return type(form)(class_name, (form,), form_class_attrs) # ModelFormSets ############################################################## @@ -597,6 +591,10 @@ class BaseModelFormSet(BaseFormSet): return [] saved_instances = [] + try: + forms_to_delete = self.deleted_forms + except AttributeError: + forms_to_delete = [] for form in self.initial_forms: pk_name = self._pk_field.name raw_pk_value = form._raw_value(pk_name) @@ -607,7 +605,7 @@ class BaseModelFormSet(BaseFormSet): pk_value = getattr(pk_value, 'pk', pk_value) obj = self._existing_object(pk_value) - if self.can_delete and self._should_delete_form(form): + if form in forms_to_delete: self.deleted_objects.append(obj) obj.delete() continue diff --git a/django/forms/util.py b/django/forms/util.py index cd6b52df6f..9b1bcebe33 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf import settings from django.utils.html import format_html, format_html_join -from django.utils.encoding import StrAndUnicode, force_text +from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -22,13 +22,14 @@ def flatatt(attrs): """ return format_html_join('', ' {0}="{1}"', attrs.items()) -class ErrorDict(dict, StrAndUnicode): +@python_2_unicode_compatible +class ErrorDict(dict): """ A collection of errors that knows how to display itself in various formats. The dictionary keys are the field names, and the values are the errors. """ - def __unicode__(self): + def __str__(self): return self.as_ul() def as_ul(self): @@ -42,11 +43,12 @@ class ErrorDict(dict, StrAndUnicode): def as_text(self): return '\n'.join(['* %s\n%s' % (k, '\n'.join([' * %s' % force_text(i) for i in v])) for k, v in self.items()]) -class ErrorList(list, StrAndUnicode): +@python_2_unicode_compatible +class ErrorList(list): """ A collection of errors that knows how to display itself in various formats. """ - def __unicode__(self): + def __str__(self): return self.as_ul() def as_ul(self): diff --git a/django/forms/widgets.py b/django/forms/widgets.py index be9ac8eb8f..fdb9f50688 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -17,11 +17,9 @@ from django.forms.util import flatatt, to_current_timezone from django.utils.datastructures import MultiValueDict, MergeDict from django.utils.html import conditional_escape, format_html, format_html_join from django.utils.translation import ugettext, ugettext_lazy -from django.utils.encoding import StrAndUnicode, force_text +from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe -from django.utils import six -from django.utils import datetime_safe, formats -from django.utils import six +from django.utils import datetime_safe, formats, six __all__ = ( 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput', @@ -34,7 +32,8 @@ __all__ = ( MEDIA_TYPES = ('css','js') -class Media(StrAndUnicode): +@python_2_unicode_compatible +class Media(object): def __init__(self, media=None, **kwargs): if media: media_attrs = media.__dict__ @@ -51,7 +50,7 @@ class Media(StrAndUnicode): # if media_attrs != {}: # raise TypeError("'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())) - def __unicode__(self): + def __str__(self): return self.render() def render(self): @@ -142,7 +141,8 @@ class MediaDefiningClass(type): new_class.media = media_property(new_class) return new_class -class SubWidget(StrAndUnicode): +@python_2_unicode_compatible +class SubWidget(object): """ Some widgets are made of multiple HTML elements -- namely, RadioSelect. This is a class that represents the "inner" HTML element of a widget. @@ -152,7 +152,7 @@ class SubWidget(StrAndUnicode): self.name, self.value = name, value self.attrs, self.choices = attrs, choices - def __unicode__(self): + def __str__(self): args = [self.name, self.value, self.attrs] if self.choices: args.append(self.choices) @@ -647,6 +647,7 @@ class SelectMultiple(Select): data_set = set([force_text(value) for value in data]) return data_set != initial_set +@python_2_unicode_compatible class RadioInput(SubWidget): """ An object used by RadioFieldRenderer that represents a single @@ -660,7 +661,7 @@ class RadioInput(SubWidget): self.choice_label = force_text(choice[1]) self.index = index - def __unicode__(self): + def __str__(self): return self.render() def render(self, name=None, value=None, attrs=None, choices=()): @@ -685,7 +686,8 @@ class RadioInput(SubWidget): final_attrs['checked'] = 'checked' return format_html('', flatatt(final_attrs)) -class RadioFieldRenderer(StrAndUnicode): +@python_2_unicode_compatible +class RadioFieldRenderer(object): """ An object used by RadioSelect to enable customization of radio widgets. """ @@ -702,7 +704,7 @@ class RadioFieldRenderer(StrAndUnicode): choice = self.choices[idx] # Let the IndexError propogate return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx) - def __unicode__(self): + def __str__(self): return self.render() def render(self): diff --git a/django/http/__init__.py b/django/http/__init__.py index 05824ad1d9..2198f38cbb 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -61,14 +61,14 @@ else: if not _cookie_allows_colon_in_names: def load(self, rawdata): self.bad_cookies = set() - super(SimpleCookie, self).load(smart_bytes(rawdata)) + super(SimpleCookie, self).load(force_str(rawdata)) for key in self.bad_cookies: del self[key] # override private __set() method: # (needed for using our Morsel, and for laxness with CookieError def _BaseCookie__set(self, key, real_value, coded_value): - key = smart_bytes(key) + key = force_str(key) try: M = self.get(key, Morsel()) M.set(key, real_value, coded_value) @@ -85,7 +85,7 @@ from django.core.files import uploadhandler from django.http.multipartparser import MultiPartParser from django.http.utils import * from django.utils.datastructures import MultiValueDict, ImmutableList -from django.utils.encoding import smart_bytes, iri_to_uri, force_text +from django.utils.encoding import force_bytes, force_str, force_text, iri_to_uri from django.utils.http import cookie_date from django.utils import six from django.utils import timezone @@ -113,7 +113,7 @@ def build_request_repr(request, path_override=None, GET_override=None, get = (pformat(GET_override) if GET_override is not None else pformat(request.GET)) - except: + except Exception: get = '' if request._post_parse_error: post = '' @@ -122,22 +122,22 @@ def build_request_repr(request, path_override=None, GET_override=None, post = (pformat(POST_override) if POST_override is not None else pformat(request.POST)) - except: + except Exception: post = '' try: cookies = (pformat(COOKIES_override) if COOKIES_override is not None else pformat(request.COOKIES)) - except: + except Exception: cookies = '' try: meta = (pformat(META_override) if META_override is not None else pformat(request.META)) - except: + except Exception: meta = '' path = path_override if path_override is not None else request.path - return smart_bytes('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % + return force_str('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % (request.__class__.__name__, path, six.text_type(get), @@ -177,14 +177,14 @@ class HttpRequest(object): # Reconstruct the host using the algorithm from PEP 333. host = self.META['SERVER_NAME'] server_port = str(self.META['SERVER_PORT']) - if server_port != (self.is_secure() and '443' or '80'): + if server_port != ('443' if self.is_secure() else '80'): host = '%s:%s' % (host, server_port) return host def get_full_path(self): # RFC 3986 requires query string arguments to be in the ASCII range. # Rather than crash if this doesn't happen, we encode defensively. - return '%s%s' % (self.path, self.META.get('QUERY_STRING', '') and ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) or '') + return '%s%s' % (self.path, ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) if self.META.get('QUERY_STRING', '') else '') def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None): """ @@ -193,7 +193,7 @@ class HttpRequest(object): default argument in which case that value will be returned instead. """ try: - cookie_value = self.COOKIES[key].encode('utf-8') + cookie_value = self.COOKIES[key] except KeyError: if default is not RAISE_ERROR: return default @@ -218,7 +218,7 @@ class HttpRequest(object): if not location: location = self.get_full_path() if not absolute_http_url_re.match(location): - current_uri = '%s://%s%s' % (self.is_secure() and 'https' or 'http', + current_uri = '%s://%s%s' % ('https' if self.is_secure() else 'http', self.get_host(), self.path) location = urljoin(current_uri, location) return iri_to_uri(location) @@ -243,7 +243,12 @@ class HttpRequest(object): def is_ajax(self): return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' - def _set_encoding(self, val): + @property + def encoding(self): + return self._encoding + + @encoding.setter + def encoding(self, val): """ Sets the encoding used for GET/POST accesses. If the GET or POST dictionary has already been created, it is removed and recreated on the @@ -255,33 +260,28 @@ class HttpRequest(object): if hasattr(self, '_post'): del self._post - def _get_encoding(self): - return self._encoding - - encoding = property(_get_encoding, _set_encoding) - def _initialize_handlers(self): self._upload_handlers = [uploadhandler.load_handler(handler, self) for handler in settings.FILE_UPLOAD_HANDLERS] - def _set_upload_handlers(self, upload_handlers): - if hasattr(self, '_files'): - raise AttributeError("You cannot set the upload handlers after the upload has been processed.") - self._upload_handlers = upload_handlers - - def _get_upload_handlers(self): + @property + def upload_handlers(self): if not self._upload_handlers: # If there are no upload handlers defined, initialize them from settings. self._initialize_handlers() return self._upload_handlers - upload_handlers = property(_get_upload_handlers, _set_upload_handlers) + @upload_handlers.setter + def upload_handlers(self, upload_handlers): + if hasattr(self, '_files'): + raise AttributeError("You cannot set the upload handlers after the upload has been processed.") + self._upload_handlers = upload_handlers def parse_file_upload(self, META, post_data): """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" self.upload_handlers = ImmutableList( self.upload_handlers, - warning = "You cannot alter upload handlers after the upload has been processed." + warning="You cannot alter upload handlers after the upload has been processed." ) parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) return parser.parse() @@ -294,7 +294,7 @@ class HttpRequest(object): try: self._body = self.read() except IOError as e: - six.reraise(UnreadablePostError, e, sys.exc_traceback) + six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) self._stream = BytesIO(self._body) return self._body @@ -360,6 +360,7 @@ class HttpRequest(object): if not buf: break yield buf + __iter__ = xreadlines def readlines(self): @@ -384,29 +385,36 @@ class QueryDict(MultiValueDict): if not encoding: encoding = settings.DEFAULT_CHARSET self.encoding = encoding - for key, value in parse_qsl((query_string or ''), True): # keep_blank_values=True - self.appendlist(force_text(key, encoding, errors='replace'), - force_text(value, encoding, errors='replace')) + if six.PY3: + for key, value in parse_qsl(query_string or '', + keep_blank_values=True, + encoding=encoding): + self.appendlist(key, value) + else: + for key, value in parse_qsl(query_string or '', + keep_blank_values=True): + self.appendlist(force_text(key, encoding, errors='replace'), + force_text(value, encoding, errors='replace')) self._mutable = mutable - def _get_encoding(self): + @property + def encoding(self): if self._encoding is None: self._encoding = settings.DEFAULT_CHARSET return self._encoding - def _set_encoding(self, value): + @encoding.setter + def encoding(self, value): self._encoding = value - encoding = property(_get_encoding, _set_encoding) - def _assert_mutable(self): if not self._mutable: raise AttributeError("This QueryDict instance is immutable") def __setitem__(self, key, value): self._assert_mutable() - key = str_to_unicode(key, self.encoding) - value = str_to_unicode(value, self.encoding) + key = bytes_to_text(key, self.encoding) + value = bytes_to_text(value, self.encoding) super(QueryDict, self).__setitem__(key, value) def __delitem__(self, key): @@ -415,21 +423,21 @@ class QueryDict(MultiValueDict): def __copy__(self): result = self.__class__('', mutable=True, encoding=self.encoding) - for key, value in self.iterlists(): + for key, value in six.iterlists(self): result.setlist(key, value) return result def __deepcopy__(self, memo): result = self.__class__('', mutable=True, encoding=self.encoding) memo[id(self)] = result - for key, value in self.iterlists(): + for key, value in six.iterlists(self): result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo)) return result def setlist(self, key, list_): self._assert_mutable() - key = str_to_unicode(key, self.encoding) - list_ = [str_to_unicode(elt, self.encoding) for elt in list_] + key = bytes_to_text(key, self.encoding) + list_ = [bytes_to_text(elt, self.encoding) for elt in list_] super(QueryDict, self).setlist(key, list_) def setlistdefault(self, key, default_list=None): @@ -438,8 +446,8 @@ class QueryDict(MultiValueDict): def appendlist(self, key, value): self._assert_mutable() - key = str_to_unicode(key, self.encoding) - value = str_to_unicode(value, self.encoding) + key = bytes_to_text(key, self.encoding) + value = bytes_to_text(value, self.encoding) super(QueryDict, self).appendlist(key, value) def pop(self, key, *args): @@ -456,8 +464,8 @@ class QueryDict(MultiValueDict): def setdefault(self, key, default=None): self._assert_mutable() - key = str_to_unicode(key, self.encoding) - default = str_to_unicode(default, self.encoding) + key = bytes_to_text(key, self.encoding) + default = bytes_to_text(default, self.encoding) return super(QueryDict, self).setdefault(key, default) def copy(self): @@ -481,13 +489,13 @@ class QueryDict(MultiValueDict): """ output = [] if safe: - safe = smart_bytes(safe, self.encoding) + safe = force_bytes(safe, self.encoding) encode = lambda k, v: '%s=%s' % ((quote(k, safe), quote(v, safe))) else: encode = lambda k, v: urlencode({k: v}) for k, list_ in self.lists(): - k = smart_bytes(k, self.encoding) - output.extend([encode(k, smart_bytes(v, self.encoding)) + k = force_bytes(k, self.encoding) + output.extend([encode(k, force_bytes(v, self.encoding)) for v in list_]) return '&'.join(output) @@ -531,6 +539,7 @@ class HttpResponse(object): if not content_type: content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, self._charset) + # content is a bytestring. See the content property methods. self.content = content self.cookies = SimpleCookie() if status: @@ -538,30 +547,40 @@ class HttpResponse(object): self['Content-Type'] = content_type - def __str__(self): - """Full HTTP message, including headers.""" - return '\n'.join(['%s: %s' % (key, value) - for key, value in self._headers.values()]) \ - + '\n\n' + self.content + def serialize(self): + """Full HTTP message, including headers, as a bytestring.""" + headers = [ + ('%s: %s' % (key, value)).encode('us-ascii') + for key, value in self._headers.values() + ] + return b'\r\n'.join(headers) + b'\r\n\r\n' + self.content + + if six.PY3: + __bytes__ = serialize + else: + __str__ = serialize def _convert_to_ascii(self, *values): """Converts all values to ascii strings.""" for value in values: - if isinstance(value, six.text_type): - try: - if not six.PY3: - value = value.encode('us-ascii') - else: - # In Python 3, use a string in headers, - # but ensure in only contains ASCII characters. - value.encode('us-ascii') - except UnicodeError as e: - e.reason += ', HTTP response headers must be in US-ASCII format' - raise - else: + if not isinstance(value, six.string_types): value = str(value) + try: + if six.PY3: + # Ensure string only contains ASCII + value.encode('us-ascii') + else: + if isinstance(value, str): + # Ensure string only contains ASCII + value.decode('us-ascii') + else: + # Convert unicode to an ASCII string + value = value.encode('us-ascii') + except UnicodeError as e: + e.reason += ', HTTP response headers must be in US-ASCII format' + raise if '\n' in value or '\r' in value: - raise BadHeaderError("Header values can't contain newlines (got %r)" % (value)) + raise BadHeaderError("Header values can't contain newlines (got %r)" % value) yield value def __setitem__(self, header, value): @@ -650,30 +669,40 @@ class HttpResponse(object): self.set_cookie(key, max_age=0, path=path, domain=domain, expires='Thu, 01-Jan-1970 00:00:00 GMT') - def _get_content(self): + @property + def content(self): if self.has_header('Content-Encoding'): - return b''.join([str(e) for e in self._container]) - return b''.join([smart_bytes(e, self._charset) for e in self._container]) + def make_bytes(value): + if isinstance(value, int): + value = six.text_type(value) + if isinstance(value, six.text_type): + value = value.encode('ascii') + # force conversion to bytes in case chunk is a subclass + return bytes(value) + return b''.join(make_bytes(e) for e in self._container) + return b''.join(force_bytes(e, self._charset) for e in self._container) - def _set_content(self, value): - if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.text_type)): + @content.setter + def content(self, value): + if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): self._container = value self._base_content_is_iter = True else: self._container = [value] self._base_content_is_iter = False - content = property(_get_content, _set_content) - def __iter__(self): self._iterator = iter(self._container) return self def __next__(self): chunk = next(self._iterator) + if isinstance(chunk, int): + chunk = six.text_type(chunk) if isinstance(chunk, six.text_type): chunk = chunk.encode(self._charset) - return str(chunk) + # force conversion to bytes in case chunk is a subclass + return bytes(chunk) next = __next__ # Python 2 compatibility @@ -699,11 +728,11 @@ class HttpResponse(object): class HttpResponseRedirectBase(HttpResponse): allowed_schemes = ['http', 'https', 'ftp'] - def __init__(self, redirect_to): + def __init__(self, redirect_to, *args, **kwargs): parsed = urlparse(redirect_to) if parsed.scheme and parsed.scheme not in self.allowed_schemes: raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) - super(HttpResponseRedirectBase, self).__init__() + super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) self['Location'] = iri_to_uri(redirect_to) class HttpResponseRedirect(HttpResponseRedirectBase): @@ -715,6 +744,16 @@ class HttpResponsePermanentRedirect(HttpResponseRedirectBase): class HttpResponseNotModified(HttpResponse): status_code = 304 + def __init__(self, *args, **kwargs): + super(HttpResponseNotModified, self).__init__(*args, **kwargs) + del self['content-type'] + + @HttpResponse.content.setter + def content(self, value): + if value: + raise AttributeError("You cannot set content to a 304 (Not Modified) response") + self._container = [] + class HttpResponseBadRequest(HttpResponse): status_code = 400 @@ -727,8 +766,8 @@ class HttpResponseForbidden(HttpResponse): class HttpResponseNotAllowed(HttpResponse): status_code = 405 - def __init__(self, permitted_methods): - super(HttpResponseNotAllowed, self).__init__() + def __init__(self, permitted_methods, *args, **kwargs): + super(HttpResponseNotAllowed, self).__init__(*args, **kwargs) self['Allow'] = ', '.join(permitted_methods) class HttpResponseGone(HttpResponse): @@ -743,8 +782,8 @@ def get_host(request): # It's neither necessary nor appropriate to use # django.utils.encoding.smart_text for parsing URLs and form inputs. Thus, -# this slightly more restricted function. -def str_to_unicode(s, encoding): +# this slightly more restricted function, used by QueryDict. +def bytes_to_text(s, encoding): """ Converts basestring objects to unicode, using the given encoding. Illegally encoded input characters are replaced with Unicode "unknown" codepoint diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index 9c66827cbb..e76a4f4208 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -6,7 +6,9 @@ file upload handlers for processing. """ from __future__ import unicode_literals +import base64 import cgi + from django.conf import settings from django.core.exceptions import SuspiciousOperation from django.utils.datastructures import MultiValueDict @@ -199,7 +201,7 @@ class MultiPartParser(object): if transfer_encoding == 'base64': # We only special-case base64 transfer encoding try: - chunk = str(chunk).decode('base64') + chunk = base64.b64decode(chunk) except Exception as e: # Since this is only a chunk, any error is an unfixable error. raise MultiPartParserError("Could not decode base64 data: %r" % e) @@ -507,9 +509,11 @@ class BoundaryIter(object): end = index next = index + len(self._boundary) # backup over CRLF - if data[max(0,end-1)] == b'\n': + last = max(0, end-1) + if data[last:last+1] == b'\n': end -= 1 - if data[max(0,end-1)] == b'\r': + last = max(0, end-1) + if data[last:last+1] == b'\r': end -= 1 return end, next @@ -613,7 +617,7 @@ def parse_header(line): if i >= 0: name = p[:i].strip().lower().decode('ascii') value = p[i+1:].strip() - if len(value) >= 2 and value[0] == value[-1] == b'"': + if len(value) >= 2 and value[:1] == value[-1:] == b'"': value = value[1:-1] value = value.replace(b'\\\\', b'\\').replace(b'\\"', b'"') pdict[name] = value diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index fd8ff30303..305b20e1c4 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -4,6 +4,7 @@ Cross Site Request Forgery Middleware. This module provides a middleware that implements protection against request forgeries from other sites. """ +from __future__ import unicode_literals import hashlib import re @@ -12,6 +13,7 @@ import random from django.conf import settings from django.core.urlresolvers import get_callable from django.utils.cache import patch_vary_headers +from django.utils.encoding import force_text from django.utils.http import same_origin from django.utils.log import getLogger from django.utils.crypto import constant_time_compare, get_random_string @@ -51,11 +53,10 @@ def get_token(request): def _sanitize_token(token): - # Allow only alphanum, and ensure we return a 'str' for the sake - # of the post processing middleware. + # Allow only alphanum if len(token) > CSRF_KEY_LENGTH: return _get_new_csrf_key() - token = re.sub('[^a-zA-Z0-9]+', '', str(token.decode('ascii', 'ignore'))) + token = re.sub('[^a-zA-Z0-9]+', '', force_text(token)) if token == "": # In case the cookie has been truncated to nothing at some point. return _get_new_csrf_key() diff --git a/django/template/base.py b/django/template/base.py index 661d8c092a..0a2b2c9437 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -11,7 +11,7 @@ from django.utils.importlib import import_module from django.utils.itercompat import is_iterable from django.utils.text import (smart_split, unescape_string_literal, get_text_list) -from django.utils.encoding import smart_text, force_text, smart_bytes +from django.utils.encoding import force_str, force_text from django.utils.translation import ugettext_lazy, pgettext_lazy from django.utils.safestring import (SafeData, EscapeData, mark_safe, mark_for_escaping) @@ -20,6 +20,7 @@ from django.utils.html import escape from django.utils.module_loading import module_has_submodule from django.utils import six from django.utils.timezone import template_localtime +from django.utils.encoding import python_2_unicode_compatible TOKEN_TEXT = 0 @@ -79,6 +80,7 @@ class TemplateDoesNotExist(Exception): class TemplateEncodingError(Exception): pass +@python_2_unicode_compatible class VariableDoesNotExist(Exception): def __init__(self, msg, params=()): @@ -86,9 +88,6 @@ class VariableDoesNotExist(Exception): self.params = params def __str__(self): - return six.text_type(self).encode('utf-8') - - def __unicode__(self): return self.msg % tuple([force_text(p, errors='replace') for p in self.params]) @@ -117,7 +116,7 @@ class Template(object): def __init__(self, template_string, origin=None, name=''): try: - template_string = smart_text(template_string) + template_string = force_text(template_string) except UnicodeDecodeError: raise TemplateEncodingError("Templates can only be constructed " "from unicode or UTF-8 strings.") @@ -849,7 +848,7 @@ class TextNode(Node): self.s = s def __repr__(self): - return "" % smart_bytes(self.s[:25], 'ascii', + return force_str("" % self.s[:25], 'ascii', errors='replace') def render(self, context): diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 16801fd573..1bfb627023 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -231,12 +231,12 @@ def make_list(value): @stringfilter def slugify(value): """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. + Converts to lowercase, removes non-word characters (alphanumerics and + underscores) and converts spaces to hyphens. Also strips leading and + trailing whitespace. """ - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = six.text_type(re.sub('[^\w\s-]', '', value).strip().lower()) - return mark_safe(re.sub('[-\s]+', '-', value)) + from django.utils.text import slugify + return slugify(value) @register.filter(is_safe=True) def stringformat(value, arg): @@ -468,13 +468,8 @@ def safeseq(value): @stringfilter def removetags(value, tags): """Removes a space separated list of [X]HTML tags from the output.""" - tags = [re.escape(tag) for tag in tags.split()] - tags_re = '(%s)' % '|'.join(tags) - starttag_re = re.compile(r'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U) - endtag_re = re.compile('' % tags_re) - value = starttag_re.sub('', value) - value = endtag_re.sub('', value) - return value + from django.utils.html import remove_tags + return remove_tags(value, tags) @register.filter(is_safe=True) @stringfilter diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 14391f08e1..cb2ecd26d8 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -448,7 +448,7 @@ class WidthRatioNode(Node): max_width = int(self.max_width.resolve(context)) except VariableDoesNotExist: return '' - except ValueError: + except (ValueError, TypeError): raise TemplateSyntaxError("widthratio final argument must be an number") try: value = float(value) @@ -456,7 +456,7 @@ class WidthRatioNode(Node): ratio = (value / max_value) * max_width except ZeroDivisionError: return '0' - except ValueError: + except (ValueError, TypeError): return '' return str(int(round(ratio))) diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py index 42f87a459e..7e35b34ec4 100644 --- a/django/template/loaders/eggs.py +++ b/django/template/loaders/eggs.py @@ -1,13 +1,15 @@ # Wrapper for loading templates from eggs via pkg_resources.resource_string. +from __future__ import unicode_literals try: from pkg_resources import resource_string except ImportError: resource_string = None +from django.conf import settings from django.template.base import TemplateDoesNotExist from django.template.loader import BaseLoader -from django.conf import settings +from django.utils import six class Loader(BaseLoader): is_usable = resource_string is not None @@ -22,9 +24,12 @@ class Loader(BaseLoader): pkg_name = 'templates/' + template_name for app in settings.INSTALLED_APPS: try: - return (resource_string(app, pkg_name).decode(settings.FILE_CHARSET), 'egg:%s:%s' % (app, pkg_name)) - except: - pass + resource = resource_string(app, pkg_name) + except Exception: + continue + if not six.PY3: + resource = resource.decode(settings.FILE_CHARSET) + return (resource, 'egg:%s:%s' % (app, pkg_name)) raise TemplateDoesNotExist(template_name) _loader = Loader() diff --git a/django/template/response.py b/django/template/response.py index 800e060c74..2cb44d127d 100644 --- a/django/template/response.py +++ b/django/template/response.py @@ -102,7 +102,7 @@ class SimpleTemplateResponse(HttpResponse): """ retval = self if not self._is_rendered: - self._set_content(self.rendered_content) + self.content = self.rendered_content for post_callback in self._post_render_callbacks: newretval = post_callback(retval) if newretval is not None: @@ -119,20 +119,20 @@ class SimpleTemplateResponse(HttpResponse): 'rendered before it can be iterated over.') return super(SimpleTemplateResponse, self).__iter__() - def _get_content(self): + @property + def content(self): if not self._is_rendered: raise ContentNotRenderedError('The response content must be ' 'rendered before it can be accessed.') - return super(SimpleTemplateResponse, self)._get_content() + return super(SimpleTemplateResponse, self).content - def _set_content(self, value): + @content.setter + def content(self, value): """Sets the content for the response """ - super(SimpleTemplateResponse, self)._set_content(value) + HttpResponse.content.fset(self, value) self._is_rendered = True - content = property(_get_content, _set_content) - class TemplateResponse(SimpleTemplateResponse): rendering_attrs = SimpleTemplateResponse.rendering_attrs + \ diff --git a/django/templatetags/cache.py b/django/templatetags/cache.py index 056eadc7de..36db4807c7 100644 --- a/django/templatetags/cache.py +++ b/django/templatetags/cache.py @@ -4,6 +4,7 @@ import hashlib from django.template import Library, Node, TemplateSyntaxError, Variable, VariableDoesNotExist from django.template import resolve_variable from django.core.cache import cache +from django.utils.encoding import force_bytes from django.utils.http import urlquote register = Library() @@ -24,8 +25,9 @@ class CacheNode(Node): expire_time = int(expire_time) except (ValueError, TypeError): raise TemplateSyntaxError('"cache" tag got a non-integer timeout value: %r' % expire_time) - # Build a unicode key for this fragment and all vary-on's. - args = hashlib.md5(':'.join([urlquote(resolve_variable(var, context)) for var in self.vary_on])) + # Build a key for this fragment and all vary-on's. + key = ':'.join([urlquote(resolve_variable(var, context)) for var in self.vary_on]) + args = hashlib.md5(force_bytes(key)) cache_key = 'template.cache.%s.%s' % (self.fragment_name, args.hexdigest()) value = cache.get(cache_key) if value is None: diff --git a/django/test/client.py b/django/test/client.py index a9ae7f5db1..8fd765ec9a 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import sys import os import re @@ -19,7 +21,7 @@ from django.http import SimpleCookie, HttpRequest, QueryDict from django.template import TemplateDoesNotExist from django.test import signals from django.utils.functional import curry -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_bytes from django.utils.http import urlencode from django.utils.importlib import import_module from django.utils.itercompat import is_iterable @@ -108,7 +110,7 @@ def encode_multipart(boundary, data): as an application/octet-stream; otherwise, str(value) will be sent. """ lines = [] - to_str = lambda s: smart_bytes(s, settings.DEFAULT_CHARSET) + to_bytes = lambda s: force_bytes(s, settings.DEFAULT_CHARSET) # Not by any means perfect, but good enough for our purposes. is_file = lambda thing: hasattr(thing, "read") and callable(thing.read) @@ -124,37 +126,37 @@ def encode_multipart(boundary, data): if is_file(item): lines.extend(encode_file(boundary, key, item)) else: - lines.extend([ - '--' + boundary, - 'Content-Disposition: form-data; name="%s"' % to_str(key), + lines.extend([to_bytes(val) for val in [ + '--%s' % boundary, + 'Content-Disposition: form-data; name="%s"' % key, '', - to_str(item) - ]) + item + ]]) else: - lines.extend([ - '--' + boundary, - 'Content-Disposition: form-data; name="%s"' % to_str(key), + lines.extend([to_bytes(val) for val in [ + '--%s' % boundary, + 'Content-Disposition: form-data; name="%s"' % key, '', - to_str(value) - ]) + value + ]]) lines.extend([ - '--' + boundary + '--', - '', + to_bytes('--%s--' % boundary), + b'', ]) - return '\r\n'.join(lines) + return b'\r\n'.join(lines) def encode_file(boundary, key, file): - to_str = lambda s: smart_bytes(s, settings.DEFAULT_CHARSET) + to_bytes = lambda s: force_bytes(s, settings.DEFAULT_CHARSET) content_type = mimetypes.guess_type(file.name)[0] if content_type is None: content_type = 'application/octet-stream' return [ - '--' + to_str(boundary), - 'Content-Disposition: form-data; name="%s"; filename="%s"' \ - % (to_str(key), to_str(os.path.basename(file.name))), - 'Content-Type: %s' % content_type, - '', + to_bytes('--%s' % boundary), + to_bytes('Content-Disposition: form-data; name="%s"; filename="%s"' \ + % (key, os.path.basename(file.name))), + to_bytes('Content-Type: %s' % content_type), + b'', file.read() ] @@ -220,7 +222,7 @@ class RequestFactory(object): charset = match.group(1) else: charset = settings.DEFAULT_CHARSET - return smart_bytes(data, encoding=charset) + return force_bytes(data, encoding=charset) def _get_path(self, parsed): # If there are parameters, add them @@ -291,7 +293,7 @@ class RequestFactory(object): def generic(self, method, path, data='', content_type='application/octet-stream', **extra): parsed = urlparse(path) - data = smart_bytes(data, settings.DEFAULT_CHARSET) + data = force_bytes(data, settings.DEFAULT_CHARSET) r = { 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': parsed[4], @@ -385,7 +387,7 @@ class Client(RequestFactory): if self.exc_info: exc_info = self.exc_info self.exc_info = None - six.reraise(exc_info[1], None, exc_info[2]) + six.reraise(*exc_info) # Save the client and request that stimulated the response. response.client = self diff --git a/django/test/html.py b/django/test/html.py index acdb4ffd14..274810cab4 100644 --- a/django/test/html.py +++ b/django/test/html.py @@ -8,6 +8,7 @@ import re from django.utils.encoding import force_text from django.utils.html_parser import HTMLParser, HTMLParseError from django.utils import six +from django.utils.encoding import python_2_unicode_compatible WHITESPACE = re.compile('\s+') @@ -17,6 +18,7 @@ def normalize_whitespace(string): return WHITESPACE.sub(' ', string) +@python_2_unicode_compatible class Element(object): def __init__(self, name, attributes): self.name = name @@ -117,7 +119,7 @@ class Element(object): def __getitem__(self, key): return self.children[key] - def __unicode__(self): + def __str__(self): output = '<%s' % self.name for key, value in self.attributes: if value: @@ -136,11 +138,12 @@ class Element(object): return six.text_type(self) +@python_2_unicode_compatible class RootElement(Element): def __init__(self): super(RootElement, self).__init__(None, ()) - def __unicode__(self): + def __str__(self): return ''.join(six.text_type(c) for c in self.children) diff --git a/django/test/signals.py b/django/test/signals.py index 052b7dfa5c..5b0a9a19ca 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -4,7 +4,6 @@ import time from django.conf import settings from django.db import connections from django.dispatch import receiver, Signal -from django.template import context from django.utils import timezone template_rendered = Signal(providing_args=["template", "context"]) @@ -48,9 +47,17 @@ def update_connections_time_zone(**kwargs): @receiver(setting_changed) def clear_context_processors_cache(**kwargs): if kwargs['setting'] == 'TEMPLATE_CONTEXT_PROCESSORS': + from django.template import context context._standard_context_processors = None +@receiver(setting_changed) +def clear_serializers_cache(**kwargs): + if kwargs['setting'] == 'SERIALIZATION_MODULES': + from django.core import serializers + serializers._serializers = {} + + @receiver(setting_changed) def language_changed(**kwargs): if kwargs['setting'] in ('LOCALE_PATHS', 'LANGUAGE_CODE'): diff --git a/django/test/testcases.py b/django/test/testcases.py index 56ba56caf1..f12c431d3a 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -41,7 +41,7 @@ from django.test.utils import (get_warnings_state, restore_warnings_state, override_settings) from django.test.utils import ContextList from django.utils import unittest as ut2 -from django.utils.encoding import smart_bytes, force_text +from django.utils.encoding import force_text from django.utils import six from django.utils.unittest.util import safe_repr from django.views.static import serve @@ -647,14 +647,13 @@ class TransactionTestCase(SimpleTestCase): self.assertEqual(response.status_code, status_code, msg_prefix + "Couldn't retrieve content: Response code was %d" " (expected %d)" % (response.status_code, status_code)) - enc_text = smart_bytes(text, response._charset) - content = response.content + content = response.content.decode(response._charset) if html: content = assert_and_parse_html(self, content, None, "Response's content is not valid HTML:") - enc_text = assert_and_parse_html(self, enc_text, None, + text = assert_and_parse_html(self, text, None, "Second argument is not valid HTML:") - real_count = content.count(enc_text) + real_count = content.count(text) if count is not None: self.assertEqual(real_count, count, msg_prefix + "Found %d instances of '%s' in response" @@ -683,14 +682,13 @@ class TransactionTestCase(SimpleTestCase): self.assertEqual(response.status_code, status_code, msg_prefix + "Couldn't retrieve content: Response code was %d" " (expected %d)" % (response.status_code, status_code)) - enc_text = smart_bytes(text, response._charset) - content = response.content + content = response.content.decode(response._charset) if html: content = assert_and_parse_html(self, content, None, 'Response\'s content is not valid HTML:') - enc_text = assert_and_parse_html(self, enc_text, None, + text = assert_and_parse_html(self, text, None, 'Second argument is not valid HTML:') - self.assertEqual(content.count(enc_text), 0, + self.assertEqual(content.count(text), 0, msg_prefix + "Response should not contain '%s'" % text) def assertFormError(self, response, form, field, errors, msg_prefix=''): @@ -919,23 +917,26 @@ class QuietWSGIRequestHandler(WSGIRequestHandler): pass -class _ImprovedEvent(threading._Event): - """ - Does the same as `threading.Event` except it overrides the wait() method - with some code borrowed from Python 2.7 to return the set state of the - event (see: http://hg.python.org/cpython/rev/b5aa8aa78c0f/). This allows - to know whether the wait() method exited normally or because of the - timeout. This class can be removed when Django supports only Python >= 2.7. - """ +if sys.version_info >= (2, 7, 0): + _ImprovedEvent = threading._Event +else: + class _ImprovedEvent(threading._Event): + """ + Does the same as `threading.Event` except it overrides the wait() method + with some code borrowed from Python 2.7 to return the set state of the + event (see: http://hg.python.org/cpython/rev/b5aa8aa78c0f/). This allows + to know whether the wait() method exited normally or because of the + timeout. This class can be removed when Django supports only Python >= 2.7. + """ - def wait(self, timeout=None): - self._Event__cond.acquire() - try: - if not self._Event__flag: - self._Event__cond.wait(timeout) - return self._Event__flag - finally: - self._Event__cond.release() + def wait(self, timeout=None): + self._Event__cond.acquire() + try: + if not self._Event__flag: + self._Event__cond.wait(timeout) + return self._Event__flag + finally: + self._Event__cond.release() class StoppableWSGIServer(WSGIServer): @@ -1136,7 +1137,7 @@ class LiveServerTestCase(TransactionTestCase): host, port_ranges = specified_address.split(':') for port_range in port_ranges.split(','): # A port range can be of either form: '8000' or '8000-8010'. - extremes = map(int, port_range.split('-')) + extremes = list(map(int, port_range.split('-'))) assert len(extremes) in [1, 2] if len(extremes) == 1: # Port range of the form '8000' diff --git a/django/test/utils.py b/django/test/utils.py index 4b121bdfb0..e2c439fd80 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -221,4 +221,4 @@ class override_settings(object): setting=key, value=new_value) def str_prefix(s): - return s % {'_': 'u'} + return s % {'_': '' if six.PY3 else 'u'} diff --git a/django/utils/2to3_fixes/__init__.py b/django/utils/2to3_fixes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/utils/2to3_fixes/fix_unicode.py b/django/utils/2to3_fixes/fix_unicode.py new file mode 100644 index 0000000000..613734ca86 --- /dev/null +++ b/django/utils/2to3_fixes/fix_unicode.py @@ -0,0 +1,36 @@ +"""Fixer for __unicode__ methods. + +Uses the django.utils.encoding.python_2_unicode_compatible decorator. +""" + +from __future__ import unicode_literals + +from lib2to3 import fixer_base +from lib2to3.fixer_util import find_indentation, Name, syms, touch_import +from lib2to3.pgen2 import token +from lib2to3.pytree import Leaf, Node + + +class FixUnicode(fixer_base.BaseFix): + + BM_compatible = True + PATTERN = """ + classdef< 'class' any+ ':' + suite< any* + funcdef< 'def' unifunc='__unicode__' + parameters< '(' NAME ')' > any+ > + any* > > + """ + + def transform(self, node, results): + unifunc = results["unifunc"] + strfunc = Name("__str__", prefix=unifunc.prefix) + unifunc.replace(strfunc) + + klass = node.clone() + klass.prefix = '\n' + find_indentation(node) + decorator = Node(syms.decorator, [Leaf(token.AT, "@"), Name('python_2_unicode_compatible')]) + decorated = Node(syms.decorated, [decorator, klass], prefix=node.prefix) + node.replace(decorated) + + touch_import('django.utils.encoding', 'python_2_unicode_compatible', decorated) diff --git a/django/utils/archive.py b/django/utils/archive.py index 829b55dd28..0faf1fa781 100644 --- a/django/utils/archive.py +++ b/django/utils/archive.py @@ -46,7 +46,8 @@ def extract(path, to_path=''): Unpack the tar or zip file at the specified path to the directory specified by to_path. """ - Archive(path).extract(to_path) + with Archive(path) as archive: + archive.extract(to_path) class Archive(object): @@ -77,12 +78,21 @@ class Archive(object): "Path not a recognized archive format: %s" % filename) return cls + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + def extract(self, to_path=''): self._archive.extract(to_path) def list(self): self._archive.list() + def close(self): + self._archive.close() + class BaseArchive(object): """ @@ -161,6 +171,9 @@ class TarArchive(BaseArchive): if extracted: extracted.close() + def close(self): + self._archive.close() + class ZipArchive(BaseArchive): @@ -189,6 +202,9 @@ class ZipArchive(BaseArchive): with open(filename, 'wb') as outfile: outfile.write(data) + def close(self): + self._archive.close() + extension_map = { '.tar': TarArchive, '.tar.bz2': TarArchive, diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index b6c055383c..2daafedd85 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -31,7 +31,7 @@ import os, sys, time, signal try: - import thread + from django.utils.six.moves import _thread as thread except ImportError: from django.utils.six.moves import _dummy_thread as thread @@ -54,7 +54,8 @@ _win = (sys.platform == "win32") def code_changed(): global _mtimes, _win - for filename in filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values())): + filenames = [getattr(m, "__file__", None) for m in sys.modules.values()] + for filename in filter(None, filenames): if filename.endswith(".pyc") or filename.endswith(".pyo"): filename = filename[:-1] if filename.endswith("$py.class"): diff --git a/django/utils/cache.py b/django/utils/cache.py index 42b0de4ce6..91c4796988 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -16,6 +16,7 @@ cache keys to prevent delivery of wrong content. An example: i18n middleware would need to distinguish caches by the "Accept-language" header. """ +from __future__ import unicode_literals import hashlib import re @@ -23,7 +24,7 @@ import time from django.conf import settings from django.core.cache import get_cache -from django.utils.encoding import iri_to_uri, force_text +from django.utils.encoding import iri_to_uri, force_bytes, force_text from django.utils.http import http_date from django.utils.timezone import get_current_timezone_name from django.utils.translation import get_language @@ -65,7 +66,7 @@ def patch_cache_control(response, **kwargs): # max-age, use the minimum of the two ages. In practice this happens when # a decorator and a piece of middleware both operate on a given view. if 'max-age' in cc and 'max_age' in kwargs: - kwargs['max_age'] = min(cc['max-age'], kwargs['max_age']) + kwargs['max_age'] = min(int(cc['max-age']), kwargs['max_age']) # Allow overriding private caching and vice versa if 'private' in cc and 'public' in kwargs: @@ -170,7 +171,7 @@ def _i18n_cache_key_suffix(request, cache_key): # User-defined tzinfo classes may return absolutely anything. # Hence this paranoid conversion to create a valid cache key. tz_name = force_text(get_current_timezone_name(), errors='ignore') - cache_key += '.%s' % tz_name.encode('ascii', 'ignore').replace(' ', '_') + cache_key += '.%s' % tz_name.encode('ascii', 'ignore').decode('ascii').replace(' ', '_') return cache_key def _generate_cache_key(request, method, headerlist, key_prefix): @@ -180,14 +181,14 @@ def _generate_cache_key(request, method, headerlist, key_prefix): value = request.META.get(header, None) if value is not None: ctx.update(value) - path = hashlib.md5(iri_to_uri(request.get_full_path())) + path = hashlib.md5(force_bytes(iri_to_uri(request.get_full_path()))) cache_key = 'views.decorators.cache.cache_page.%s.%s.%s.%s' % ( key_prefix, method, path.hexdigest(), ctx.hexdigest()) return _i18n_cache_key_suffix(request, cache_key) def _generate_cache_header_key(key_prefix, request): """Returns a cache key for the header cache.""" - path = hashlib.md5(iri_to_uri(request.get_full_path())) + path = hashlib.md5(force_bytes(iri_to_uri(request.get_full_path()))) cache_key = 'views.decorators.cache.cache_header.%s.%s' % ( key_prefix, path.hexdigest()) return _i18n_cache_key_suffix(request, cache_key) diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 70a07e7fde..57bc60dc4f 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -23,7 +23,8 @@ except NotImplementedError: using_sysrandom = False from django.conf import settings -from django.utils.encoding import smart_bytes +from django.utils.encoding import force_bytes +from django.utils import six from django.utils.six.moves import xrange @@ -50,7 +51,7 @@ def salted_hmac(key_salt, value, secret=None): # line is redundant and could be replaced by key = key_salt + secret, since # the hmac module does the same thing for keys longer than the block size. # However, we need to ensure that we *always* do this. - return hmac.new(key, msg=smart_bytes(value), digestmod=hashlib.sha1) + return hmac.new(key, msg=force_bytes(value), digestmod=hashlib.sha1) def get_random_string(length=12, @@ -88,8 +89,12 @@ def constant_time_compare(val1, val2): if len(val1) != len(val2): return False result = 0 - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) + if six.PY3 and isinstance(val1, bytes) and isinstance(val2, bytes): + for x, y in zip(val1, val2): + result |= x ^ y + else: + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) return result == 0 @@ -142,8 +147,8 @@ def pbkdf2(password, salt, iterations, dklen=0, digest=None): assert iterations > 0 if not digest: digest = hashlib.sha256 - password = smart_bytes(password) - salt = smart_bytes(salt) + password = force_bytes(password) + salt = force_bytes(salt) hlen = digest().digest_size if not dklen: dklen = hlen diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py index 032eb493b6..b4bd559a66 100644 --- a/django/utils/dateparse.py +++ b/django/utils/dateparse.py @@ -15,16 +15,16 @@ date_re = re.compile( r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})$' ) +time_re = re.compile( + r'(?P\d{1,2}):(?P\d{1,2})' + r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' +) + datetime_re = re.compile( r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})' r'[T ](?P\d{1,2}):(?P\d{1,2})' r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' - r'(?PZ|[+-]\d{1,2}:\d{1,2})?$' -) - -time_re = re.compile( - r'(?P\d{1,2}):(?P\d{1,2})' - r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?' + r'(?PZ|[+-]\d{2}:?\d{2})?$' ) def parse_date(value): @@ -43,8 +43,6 @@ def parse_time(value): This function doesn't support time zone offsets. - Sub-microsecond precision is accepted, but ignored. - Raises ValueError if the input is well formatted but not a valid time. Returns None if the input isn't well formatted, in particular if it contains an offset. @@ -63,8 +61,6 @@ def parse_datetime(value): This function supports time zone offsets. When the input contains one, the output uses an instance of FixedOffset as tzinfo. - Sub-microsecond precision is accepted, but ignored. - Raises ValueError if the input is well formatted but not a valid datetime. Returns None if the input isn't well formatted. """ @@ -77,7 +73,7 @@ def parse_datetime(value): if tzinfo == 'Z': tzinfo = utc elif tzinfo is not None: - offset = 60 * int(tzinfo[1:3]) + int(tzinfo[4:6]) + offset = 60 * int(tzinfo[1:3]) + int(tzinfo[-2:]) if tzinfo[0] == '-': offset = -offset tzinfo = FixedOffset(offset) diff --git a/django/utils/encoding.py b/django/utils/encoding.py index eb60cfde8b..3b284f3ed0 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -8,6 +8,7 @@ try: from urllib.parse import quote except ImportError: # Python 2 from urllib import quote +import warnings from django.utils.functional import Promise from django.utils import six @@ -32,6 +33,12 @@ class StrAndUnicode(object): Useful as a mix-in. If you support Python 2 and 3 with a single code base, you can inherit this mix-in and just define __unicode__. """ + def __init__(self, *args, **kwargs): + warnings.warn("StrAndUnicode is deprecated. Define a __str__ method " + "and apply the @python_2_unicode_compatible decorator " + "instead.", PendingDeprecationWarning, stacklevel=2) + super(StrAndUnicode, self).__init__(*args, **kwargs) + if six.PY3: def __str__(self): return self.__unicode__() @@ -39,6 +46,19 @@ class StrAndUnicode(object): def __str__(self): return self.__unicode__().encode('utf-8') +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if not six.PY3: + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + def smart_text(s, encoding='utf-8', strings_only=False, errors='strict'): """ Returns a text object representing 's' -- unicode on Python 2 and str on @@ -99,8 +119,8 @@ def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): errors) for arg in s]) else: # Note: We use .decode() here, instead of six.text_type(s, encoding, - # errors), so that if s is a SafeString, it ends up being a - # SafeUnicode at the end. + # errors), so that if s is a SafeBytes, it ends up being a + # SafeText at the end. s = s.decode(encoding, errors) except UnicodeDecodeError as e: if not isinstance(s, Exception): @@ -119,6 +139,19 @@ def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): """ Returns a bytestring version of 's', encoded as specified in 'encoding'. + If strings_only is True, don't convert (some) non-string-like objects. + """ + if isinstance(s, Promise): + # The input is the result of a gettext_lazy() call. + return s + return force_bytes(s, encoding, strings_only, errors) + + +def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Similar to smart_bytes, except that lazy instances are resolved to + strings, rather than kept as lazy objects. + If strings_only is True, don't convert (some) non-string-like objects. """ if isinstance(s, bytes): @@ -141,7 +174,7 @@ def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): # An Exception subclass containing non-ASCII data that doesn't # know how to print itself properly. We shouldn't raise a # further exception. - return ' '.join([smart_bytes(arg, encoding, strings_only, + return b' '.join([force_bytes(arg, encoding, strings_only, errors) for arg in s]) return six.text_type(s).encode(encoding, errors) else: @@ -149,8 +182,10 @@ def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): if six.PY3: smart_str = smart_text + force_str = force_text else: smart_str = smart_bytes + force_str = force_bytes # backwards compatibility for Python 2 smart_unicode = smart_text force_unicode = force_text @@ -161,6 +196,10 @@ Apply smart_text in Python 3 and smart_bytes in Python 2. This is suitable for writing to sys.stdout (for instance). """ +force_str.__doc__ = """\ +Apply force_text in Python 3 and force_bytes in Python 2. +""" + def iri_to_uri(iri): """ Convert an Internationalized Resource Identifier (IRI) portion to a URI @@ -186,7 +225,7 @@ def iri_to_uri(iri): # converted. if iri is None: return iri - return quote(smart_bytes(iri), safe=b"/#%[]=:;$&()+,!?*@'~") + return quote(force_bytes(iri), safe=b"/#%[]=:;$&()+,!?*@'~") def filepath_to_uri(path): """Convert an file system path to a URI portion that is suitable for @@ -205,7 +244,7 @@ def filepath_to_uri(path): return path # I know about `os.sep` and `os.altsep` but I want to leave # some flexibility for hardcoding separators. - return quote(smart_bytes(path).replace("\\", "/"), safe=b"/~!*()'") + return quote(force_bytes(path.replace("\\", "/")), safe=b"/~!*()'") # The encoding of the default system locale but falls back to the # given fallback encoding if the encoding is unsupported by python or could diff --git a/django/utils/formats.py b/django/utils/formats.py index 2e54d792fa..555982eede 100644 --- a/django/utils/formats.py +++ b/django/utils/formats.py @@ -4,7 +4,7 @@ import datetime from django.conf import settings from django.utils import dateformat, numberformat, datetime_safe from django.utils.importlib import import_module -from django.utils.encoding import smart_str +from django.utils.encoding import force_str from django.utils.functional import lazy from django.utils.safestring import mark_safe from django.utils import six @@ -66,7 +66,7 @@ def get_format(format_type, lang=None, use_l10n=None): If use_l10n is provided and is not None, that will force the value to be localized (or not), overriding the value of settings.USE_L10N. """ - format_type = smart_str(format_type) + format_type = force_str(format_type) if use_l10n or (use_l10n is None and settings.USE_L10N): if lang is None: lang = get_language() @@ -160,14 +160,14 @@ def localize_input(value, default=None): return number_format(value) elif isinstance(value, datetime.datetime): value = datetime_safe.new_datetime(value) - format = smart_str(default or get_format('DATETIME_INPUT_FORMATS')[0]) + format = force_str(default or get_format('DATETIME_INPUT_FORMATS')[0]) return value.strftime(format) elif isinstance(value, datetime.date): value = datetime_safe.new_date(value) - format = smart_str(default or get_format('DATE_INPUT_FORMATS')[0]) + format = force_str(default or get_format('DATE_INPUT_FORMATS')[0]) return value.strftime(format) elif isinstance(value, datetime.time): - format = smart_str(default or get_format('TIME_INPUT_FORMATS')[0]) + format = force_str(default or get_format('TIME_INPUT_FORMATS')[0]) return value.strftime(format) return value diff --git a/django/utils/functional.py b/django/utils/functional.py index 085ec40b63..085a8fce59 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -238,7 +238,6 @@ class LazyObject(object): raise NotImplementedError # introspection support: - __members__ = property(lambda self: self.__dir__()) __dir__ = new_method_proxy(dir) diff --git a/django/utils/html.py b/django/utils/html.py index 7dd53a1353..2b669cc8ec 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -11,7 +11,7 @@ except ImportError: # Python 2 from urlparse import urlsplit, urlunsplit from django.utils.safestring import SafeData, mark_safe -from django.utils.encoding import smart_bytes, force_text +from django.utils.encoding import force_bytes, force_text from django.utils.functional import allow_lazy from django.utils import six from django.utils.text import normalize_newlines @@ -123,6 +123,17 @@ def strip_tags(value): return re.sub(r'<[^>]*?>', '', force_text(value)) strip_tags = allow_lazy(strip_tags) +def remove_tags(html, tags): + """Returns the given HTML with given tags removed.""" + tags = [re.escape(tag) for tag in tags.split()] + tags_re = '(%s)' % '|'.join(tags) + starttag_re = re.compile(r'<%s(/?>|(\s+[^>]*>))' % tags_re, re.U) + endtag_re = re.compile('' % tags_re) + html = starttag_re.sub('', html) + html = endtag_re.sub('', html) + return html +remove_tags = allow_lazy(remove_tags, six.text_type) + def strip_spaces_between_tags(value): """Returns the given HTML with spaces between tags removed.""" return re.sub(r'>\s+<', '><', force_text(value)) @@ -143,7 +154,7 @@ def smart_urlquote(url): # Handle IDN before quoting. scheme, netloc, path, query, fragment = urlsplit(url) try: - netloc = netloc.encode('idna') # IDN -> ACE + netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE except UnicodeError: # invalid domain part pass else: @@ -153,7 +164,7 @@ def smart_urlquote(url): # contains a % not followed by two hexadecimal digits. See #9655. if '%' not in url or unquoted_percents_re.search(url): # See http://bugs.python.org/issue2637 - url = quote(smart_bytes(url), safe=b'!*\'();:@&=+$,/?#[]~') + url = quote(force_bytes(url), safe=b'!*\'();:@&=+$,/?#[]~') return force_text(url) @@ -206,7 +217,7 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): elif not ':' in middle and simple_email_re.match(middle): local, domain = middle.rsplit('@', 1) try: - domain = domain.encode('idna') + domain = domain.encode('idna').decode('ascii') except UnicodeError: continue url = 'mailto:%s@%s' % (local, domain) diff --git a/django/utils/html_parser.py b/django/utils/html_parser.py index ee56c01aec..d7311f253b 100644 --- a/django/utils/html_parser.py +++ b/django/utils/html_parser.py @@ -1,102 +1,114 @@ from django.utils.six.moves import html_parser as _html_parser import re +import sys -tagfind = re.compile('([a-zA-Z][-.a-zA-Z0-9:_]*)(?:\s|/(?!>))*') +current_version = sys.version_info + +use_workaround = ( + (current_version < (2, 6, 8)) or + (current_version >= (2, 7) and current_version < (2, 7, 3)) or + (current_version >= (3, 0) and current_version < (3, 2, 3)) +) HTMLParseError = _html_parser.HTMLParseError -class HTMLParser(_html_parser.HTMLParser): - """ - Patched version of stdlib's HTMLParser with patch from: - http://bugs.python.org/issue670664 - """ - def __init__(self): - _html_parser.HTMLParser.__init__(self) - self.cdata_tag = None +if not use_workaround: + HTMLParser = _html_parser.HTMLParser +else: + tagfind = re.compile('([a-zA-Z][-.a-zA-Z0-9:_]*)(?:\s|/(?!>))*') - def set_cdata_mode(self, tag): - try: - self.interesting = _html_parser.interesting_cdata - except AttributeError: - self.interesting = re.compile(r'' % tag.lower(), re.I) - self.cdata_tag = tag.lower() + class HTMLParser(_html_parser.HTMLParser): + """ + Patched version of stdlib's HTMLParser with patch from: + http://bugs.python.org/issue670664 + """ + def __init__(self): + _html_parser.HTMLParser.__init__(self) + self.cdata_tag = None - def clear_cdata_mode(self): - self.interesting = _html_parser.interesting_normal - self.cdata_tag = None + def set_cdata_mode(self, tag): + try: + self.interesting = _html_parser.interesting_cdata + except AttributeError: + self.interesting = re.compile(r'' % tag.lower(), re.I) + self.cdata_tag = tag.lower() - # Internal -- handle starttag, return end or -1 if not terminated - def parse_starttag(self, i): - self.__starttag_text = None - endpos = self.check_for_whole_start_tag(i) - if endpos < 0: - return endpos - rawdata = self.rawdata - self.__starttag_text = rawdata[i:endpos] + def clear_cdata_mode(self): + self.interesting = _html_parser.interesting_normal + self.cdata_tag = None - # Now parse the data between i+1 and j into a tag and attrs - attrs = [] - match = tagfind.match(rawdata, i + 1) - assert match, 'unexpected call to parse_starttag()' - k = match.end() - self.lasttag = tag = match.group(1).lower() + # Internal -- handle starttag, return end or -1 if not terminated + def parse_starttag(self, i): + self.__starttag_text = None + endpos = self.check_for_whole_start_tag(i) + if endpos < 0: + return endpos + rawdata = self.rawdata + self.__starttag_text = rawdata[i:endpos] - while k < endpos: - m = _html_parser.attrfind.match(rawdata, k) - if not m: - break - attrname, rest, attrvalue = m.group(1, 2, 3) - if not rest: - attrvalue = None - elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ - attrvalue[:1] == '"' == attrvalue[-1:]: - attrvalue = attrvalue[1:-1] - if attrvalue: - attrvalue = self.unescape(attrvalue) - attrs.append((attrname.lower(), attrvalue)) - k = m.end() + # Now parse the data between i+1 and j into a tag and attrs + attrs = [] + match = tagfind.match(rawdata, i + 1) + assert match, 'unexpected call to parse_starttag()' + k = match.end() + self.lasttag = tag = match.group(1).lower() - end = rawdata[k:endpos].strip() - if end not in (">", "/>"): - lineno, offset = self.getpos() - if "\n" in self.__starttag_text: - lineno = lineno + self.__starttag_text.count("\n") - offset = len(self.__starttag_text) \ - - self.__starttag_text.rfind("\n") + while k < endpos: + m = _html_parser.attrfind.match(rawdata, k) + if not m: + break + attrname, rest, attrvalue = m.group(1, 2, 3) + if not rest: + attrvalue = None + elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ + attrvalue[:1] == '"' == attrvalue[-1:]: + attrvalue = attrvalue[1:-1] + if attrvalue: + attrvalue = self.unescape(attrvalue) + attrs.append((attrname.lower(), attrvalue)) + k = m.end() + + end = rawdata[k:endpos].strip() + if end not in (">", "/>"): + lineno, offset = self.getpos() + if "\n" in self.__starttag_text: + lineno = lineno + self.__starttag_text.count("\n") + offset = len(self.__starttag_text) \ + - self.__starttag_text.rfind("\n") + else: + offset = offset + len(self.__starttag_text) + self.error("junk characters in start tag: %r" + % (rawdata[k:endpos][:20],)) + if end.endswith('/>'): + # XHTML-style empty tag: + self.handle_startendtag(tag, attrs) else: - offset = offset + len(self.__starttag_text) - self.error("junk characters in start tag: %r" - % (rawdata[k:endpos][:20],)) - if end.endswith('/>'): - # XHTML-style empty tag: - self.handle_startendtag(tag, attrs) - else: - self.handle_starttag(tag, attrs) - if tag in self.CDATA_CONTENT_ELEMENTS: - self.set_cdata_mode(tag) # <--------------------------- Changed - return endpos + self.handle_starttag(tag, attrs) + if tag in self.CDATA_CONTENT_ELEMENTS: + self.set_cdata_mode(tag) # <--------------------------- Changed + return endpos - # Internal -- parse endtag, return end or -1 if incomplete - def parse_endtag(self, i): - rawdata = self.rawdata - assert rawdata[i:i + 2] == " - if not match: - return -1 - j = match.end() - match = _html_parser.endtagfind.match(rawdata, i) # - if not match: - if self.cdata_tag is not None: # *** add *** - self.handle_data(rawdata[i:j]) # *** add *** - return j # *** add *** - self.error("bad end tag: %r" % (rawdata[i:j],)) - # --- changed start --------------------------------------------------- - tag = match.group(1).strip() - if self.cdata_tag is not None: - if tag.lower() != self.cdata_tag: - self.handle_data(rawdata[i:j]) - return j - # --- changed end ----------------------------------------------------- - self.handle_endtag(tag.lower()) - self.clear_cdata_mode() - return j + # Internal -- parse endtag, return end or -1 if incomplete + def parse_endtag(self, i): + rawdata = self.rawdata + assert rawdata[i:i + 2] == " + if not match: + return -1 + j = match.end() + match = _html_parser.endtagfind.match(rawdata, i) # + if not match: + if self.cdata_tag is not None: # *** add *** + self.handle_data(rawdata[i:j]) # *** add *** + return j # *** add *** + self.error("bad end tag: %r" % (rawdata[i:j],)) + # --- changed start --------------------------------------------------- + tag = match.group(1).strip() + if self.cdata_tag is not None: + if tag.lower() != self.cdata_tag: + self.handle_data(rawdata[i:j]) + return j + # --- changed end ----------------------------------------------------- + self.handle_endtag(tag.lower()) + self.clear_cdata_mode() + return j diff --git a/django/utils/http.py b/django/utils/http.py index 22e81a33d7..d3c70f1209 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import calendar import datetime import re @@ -13,7 +15,7 @@ except ImportError: # Python 2 from email.utils import formatdate from django.utils.datastructures import MultiValueDict -from django.utils.encoding import force_text, smart_str +from django.utils.encoding import force_str, force_text from django.utils.functional import allow_lazy from django.utils import six @@ -37,7 +39,7 @@ def urlquote(url, safe='/'): can safely be used as part of an argument to a subsequent iri_to_uri() call without double-quoting occurring. """ - return force_text(urllib_parse.quote(smart_str(url), smart_str(safe))) + return force_text(urllib_parse.quote(force_str(url), force_str(safe))) urlquote = allow_lazy(urlquote, six.text_type) def urlquote_plus(url, safe=''): @@ -47,7 +49,7 @@ def urlquote_plus(url, safe=''): returned string can safely be used as part of an argument to a subsequent iri_to_uri() call without double-quoting occurring. """ - return force_text(urllib_parse.quote_plus(smart_str(url), smart_str(safe))) + return force_text(urllib_parse.quote_plus(force_str(url), force_str(safe))) urlquote_plus = allow_lazy(urlquote_plus, six.text_type) def urlunquote(quoted_url): @@ -55,7 +57,7 @@ def urlunquote(quoted_url): A wrapper for Python's urllib.unquote() function that can operate on the result of django.utils.http.urlquote(). """ - return force_text(urllib_parse.unquote(smart_str(quoted_url))) + return force_text(urllib_parse.unquote(force_str(quoted_url))) urlunquote = allow_lazy(urlunquote, six.text_type) def urlunquote_plus(quoted_url): @@ -63,7 +65,7 @@ def urlunquote_plus(quoted_url): A wrapper for Python's urllib.unquote_plus() function that can operate on the result of django.utils.http.urlquote_plus(). """ - return force_text(urllib_parse.unquote_plus(smart_str(quoted_url))) + return force_text(urllib_parse.unquote_plus(force_str(quoted_url))) urlunquote_plus = allow_lazy(urlunquote_plus, six.text_type) def urlencode(query, doseq=0): @@ -77,8 +79,8 @@ def urlencode(query, doseq=0): elif hasattr(query, 'items'): query = query.items() return urllib_parse.urlencode( - [(smart_str(k), - [smart_str(i) for i in v] if isinstance(v, (list,tuple)) else smart_str(v)) + [(force_str(k), + [force_str(i) for i in v] if isinstance(v, (list,tuple)) else force_str(v)) for k, v in query], doseq) @@ -211,7 +213,7 @@ def parse_etags(etag_str): if not etags: # etag_str has wrong format, treat it as an opaque string then return [etag_str] - etags = [e.decode('string_escape') for e in etags] + etags = [e.encode('ascii').decode('unicode_escape') for e in etags] return etags def quote_etag(etag): diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py index 2ccae8cdd0..7624bb9c76 100644 --- a/django/utils/ipv6.py +++ b/django/utils/ipv6.py @@ -263,6 +263,6 @@ def _is_shorthand_ip(ip_str): """ if ip_str.count('::') == 1: return True - if filter(lambda x: len(x) < 4, ip_str.split(':')): + if any(len(x) < 4 for x in ip_str.split(':')): return True return False diff --git a/django/utils/safestring.py b/django/utils/safestring.py index bfaefd07ee..07e0bf4cea 100644 --- a/django/utils/safestring.py +++ b/django/utils/safestring.py @@ -10,36 +10,43 @@ from django.utils import six class EscapeData(object): pass -class EscapeString(bytes, EscapeData): +class EscapeBytes(bytes, EscapeData): """ - A string that should be HTML-escaped when output. + A byte string that should be HTML-escaped when output. """ pass -class EscapeUnicode(six.text_type, EscapeData): +class EscapeText(six.text_type, EscapeData): """ - A unicode object that should be HTML-escaped when output. + A unicode string object that should be HTML-escaped when output. """ pass +if six.PY3: + EscapeString = EscapeText +else: + EscapeString = EscapeBytes + # backwards compatibility for Python 2 + EscapeUnicode = EscapeText + class SafeData(object): pass -class SafeString(bytes, SafeData): +class SafeBytes(bytes, SafeData): """ - A string subclass that has been specifically marked as "safe" (requires no + A bytes subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. """ def __add__(self, rhs): """ - Concatenating a safe string with another safe string or safe unicode - object is safe. Otherwise, the result is no longer safe. + Concatenating a safe byte string with another safe byte string or safe + unicode string is safe. Otherwise, the result is no longer safe. """ - t = super(SafeString, self).__add__(rhs) - if isinstance(rhs, SafeUnicode): - return SafeUnicode(t) - elif isinstance(rhs, SafeString): - return SafeString(t) + t = super(SafeBytes, self).__add__(rhs) + if isinstance(rhs, SafeText): + return SafeText(t) + elif isinstance(rhs, SafeBytes): + return SafeBytes(t) return t def _proxy_method(self, *args, **kwargs): @@ -51,25 +58,25 @@ class SafeString(bytes, SafeData): method = kwargs.pop('method') data = method(self, *args, **kwargs) if isinstance(data, bytes): - return SafeString(data) + return SafeBytes(data) else: - return SafeUnicode(data) + return SafeText(data) decode = curry(_proxy_method, method=bytes.decode) -class SafeUnicode(six.text_type, SafeData): +class SafeText(six.text_type, SafeData): """ - A unicode subclass that has been specifically marked as "safe" for HTML - output purposes. + A unicode (Python 2) / str (Python 3) subclass that has been specifically + marked as "safe" for HTML output purposes. """ def __add__(self, rhs): """ - Concatenating a safe unicode object with another safe string or safe - unicode object is safe. Otherwise, the result is no longer safe. + Concatenating a safe unicode string with another safe byte string or + safe unicode string is safe. Otherwise, the result is no longer safe. """ - t = super(SafeUnicode, self).__add__(rhs) + t = super(SafeText, self).__add__(rhs) if isinstance(rhs, SafeData): - return SafeUnicode(t) + return SafeText(t) return t def _proxy_method(self, *args, **kwargs): @@ -81,12 +88,19 @@ class SafeUnicode(six.text_type, SafeData): method = kwargs.pop('method') data = method(self, *args, **kwargs) if isinstance(data, bytes): - return SafeString(data) + return SafeBytes(data) else: - return SafeUnicode(data) + return SafeText(data) encode = curry(_proxy_method, method=six.text_type.encode) +if six.PY3: + SafeString = SafeText +else: + SafeString = SafeBytes + # backwards compatibility for Python 2 + SafeUnicode = SafeText + def mark_safe(s): """ Explicitly mark a string as safe for (HTML) output purposes. The returned @@ -97,10 +111,10 @@ def mark_safe(s): if isinstance(s, SafeData): return s if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes): - return SafeString(s) + return SafeBytes(s) if isinstance(s, (six.text_type, Promise)): - return SafeUnicode(s) - return SafeString(bytes(s)) + return SafeText(s) + return SafeString(str(s)) def mark_for_escaping(s): """ @@ -113,8 +127,8 @@ def mark_for_escaping(s): if isinstance(s, (SafeData, EscapeData)): return s if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes): - return EscapeString(s) + return EscapeBytes(s) if isinstance(s, (six.text_type, Promise)): - return EscapeUnicode(s) - return EscapeString(bytes(s)) + return EscapeText(s) + return EscapeBytes(bytes(s)) diff --git a/django/utils/six.py b/django/utils/six.py index ceb5d70e58..e4ce939844 100644 --- a/django/utils/six.py +++ b/django/utils/six.py @@ -5,7 +5,7 @@ import sys import types __author__ = "Benjamin Peterson " -__version__ = "1.1.0" +__version__ = "1.2.0" # True if we are running on Python 3. @@ -26,19 +26,23 @@ else: text_type = unicode binary_type = str - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit + if sys.platform == "java": + # Jython always uses 32 bits. MAXSIZE = int((1 << 31) - 1) else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X def _add_doc(func, doc): @@ -201,12 +205,19 @@ else: _iteritems = "iteritems" +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + if PY3: def get_unbound_function(unbound): return unbound - - advance_iterator = next + Iterator = object def callable(obj): return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) @@ -214,9 +225,10 @@ else: def get_unbound_function(unbound): return unbound.im_func + class Iterator(object): - def advance_iterator(it): - return it.next() + def next(self): + return type(self).__next__(self) callable = callable _add_doc(get_unbound_function, @@ -231,15 +243,15 @@ get_function_defaults = operator.attrgetter(_func_defaults) def iterkeys(d): """Return an iterator over the keys of a dictionary.""" - return getattr(d, _iterkeys)() + return iter(getattr(d, _iterkeys)()) def itervalues(d): """Return an iterator over the values of a dictionary.""" - return getattr(d, _itervalues)() + return iter(getattr(d, _itervalues)()) def iteritems(d): """Return an iterator over the (key, value) pairs of a dictionary.""" - return getattr(d, _iteritems)() + return iter(getattr(d, _iteritems)()) if PY3: @@ -365,4 +377,6 @@ def iterlists(d): """Return an iterator over the values of a MultiValueDict.""" return getattr(d, _iterlists)() + add_move(MovedModule("_dummy_thread", "dummy_thread")) +add_move(MovedModule("_thread", "thread")) diff --git a/django/utils/text.py b/django/utils/text.py index 0838d79c65..c19708458b 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -4,13 +4,19 @@ import re import unicodedata import warnings from gzip import GzipFile -from django.utils.six.moves import html_entities from io import BytesIO from django.utils.encoding import force_text from django.utils.functional import allow_lazy, SimpleLazyObject from django.utils import six +from django.utils.six.moves import html_entities from django.utils.translation import ugettext_lazy, ugettext as _, pgettext +from django.utils.safestring import mark_safe + +if not six.PY3: + # Import force_unicode even though this module doesn't use it, because some + # people rely on it being here. + from django.utils.encoding import force_unicode # Capitalizes the first letter of a string. capfirst = lambda x: x and force_text(x)[0].upper() + force_text(x)[1:] @@ -287,7 +293,7 @@ ustring_re = re.compile("([\u0080-\uffff])") def javascript_quote(s, quote_double_quotes=False): def fix(match): - return b"\u%04x" % ord(match.group(1)) + return "\\u%04x" % ord(match.group(1)) if type(s) == bytes: s = s.decode('utf-8') @@ -378,3 +384,14 @@ def unescape_string_literal(s): quote = s[0] return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\') unescape_string_literal = allow_lazy(unescape_string_literal) + +def slugify(value): + """ + Converts to lowercase, removes non-word characters (alphanumerics and + underscores) and converts spaces to hyphens. Also strips leading and + trailing whitespace. + """ + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii') + value = re.sub('[^\w\s-]', '', value).strip().lower() + return mark_safe(re.sub('[-\s]+', '-', value)) +slugify = allow_lazy(slugify, six.text_type) diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index febde404a5..f3cc6348f6 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -79,10 +79,10 @@ def pgettext(context, message): def npgettext(context, singular, plural, number): return _trans.npgettext(context, singular, plural, number) -ngettext_lazy = lazy(ngettext, bytes) -gettext_lazy = lazy(gettext, bytes) -ungettext_lazy = lazy(ungettext, six.text_type) +gettext_lazy = lazy(gettext, str) +ngettext_lazy = lazy(ngettext, str) ugettext_lazy = lazy(ugettext, six.text_type) +ungettext_lazy = lazy(ungettext, six.text_type) pgettext_lazy = lazy(pgettext, six.text_type) npgettext_lazy = lazy(npgettext, six.text_type) diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 9e6eadcd48..9fd33a7ea8 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -9,7 +9,7 @@ import gettext as gettext_module from threading import local from django.utils.importlib import import_module -from django.utils.encoding import smart_str, smart_text +from django.utils.encoding import force_str, force_text from django.utils.safestring import mark_safe, SafeData from django.utils import six from django.utils.six import StringIO @@ -259,6 +259,11 @@ def do_translate(message, translation_function): return result def gettext(message): + """ + Returns a string of the translation of the message. + + Returns a string on Python 3 and an UTF-8-encoded bytestring on Python 2. + """ return do_translate(message, 'gettext') if six.PY3: @@ -296,8 +301,10 @@ def do_ntranslate(singular, plural, number, translation_function): def ngettext(singular, plural, number): """ - Returns a UTF-8 bytestring of the translation of either the singular or - plural, based on the number. + Returns a string of the translation of either the singular or plural, + based on the number. + + Returns a string on Python 3 and an UTF-8-encoded bytestring on Python 2. """ return do_ntranslate(singular, plural, number, 'ngettext') @@ -447,7 +454,7 @@ def templatize(src, origin=None): from django.conf import settings from django.template import (Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK, TOKEN_COMMENT, TRANSLATOR_COMMENT_MARK) - src = smart_text(src, settings.FILE_CHARSET) + src = force_text(src, settings.FILE_CHARSET) out = StringIO() message_context = None intrans = False @@ -462,7 +469,7 @@ def templatize(src, origin=None): content = ''.join(comment) translators_comment_start = None for lineno, line in enumerate(content.splitlines(True)): - if line.lstrip().startswith(smart_text(TRANSLATOR_COMMENT_MARK)): + if line.lstrip().startswith(TRANSLATOR_COMMENT_MARK): translators_comment_start = lineno for lineno, line in enumerate(content.splitlines(True)): if translators_comment_start is not None and lineno >= translators_comment_start: @@ -577,7 +584,7 @@ def templatize(src, origin=None): out.write(' # %s' % t.contents) else: out.write(blankout(t.contents, 'X')) - return smart_str(out.getvalue()) + return force_str(out.getvalue()) def parse_accept_lang_header(lang_string): """ diff --git a/django/utils/tzinfo.py b/django/utils/tzinfo.py index 208b7e7191..654c99778e 100644 --- a/django/utils/tzinfo.py +++ b/django/utils/tzinfo.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import time from datetime import timedelta, tzinfo -from django.utils.encoding import smart_text, smart_str, DEFAULT_LOCALE_ENCODING +from django.utils.encoding import force_str, force_text, DEFAULT_LOCALE_ENCODING # Python's doc say: "A tzinfo subclass must have an __init__() method that can # be called with no arguments". FixedOffset and LocalTimezone don't honor this @@ -53,7 +53,7 @@ class LocalTimezone(tzinfo): self._tzname = self.tzname(dt) def __repr__(self): - return smart_str(self._tzname) + return force_str(self._tzname) def __getinitargs__(self): return self.__dt, @@ -72,7 +72,7 @@ class LocalTimezone(tzinfo): def tzname(self, dt): try: - return smart_text(time.tzname[self._isdst(dt)], + return force_text(time.tzname[self._isdst(dt)], DEFAULT_LOCALE_ENCODING) except UnicodeDecodeError: return None diff --git a/django/views/debug.py b/django/views/debug.py index b275ef9e73..ed99d8dfe6 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -14,7 +14,7 @@ from django.template import Template, Context, TemplateDoesNotExist from django.template.defaultfilters import force_escape, pprint from django.utils.html import escape from django.utils.importlib import import_module -from django.utils.encoding import smart_text, smart_bytes +from django.utils.encoding import force_bytes, smart_text from django.utils import six HIDDEN_SETTINGS = re.compile('API|TOKEN|KEY|SECRET|PASS|PROFANITIES_LIST|SIGNATURE') @@ -440,7 +440,7 @@ def technical_404_response(request, exception): 'root_urlconf': settings.ROOT_URLCONF, 'request_path': request.path_info[1:], # Trim leading slash 'urlpatterns': tried, - 'reason': smart_bytes(exception, errors='replace'), + 'reason': force_bytes(exception, errors='replace'), 'request': request, 'settings': get_safe_settings(), }) diff --git a/django/views/generic/base.py b/django/views/generic/base.py index 6554e898ca..e11412ba4d 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -18,6 +18,8 @@ class ContextMixin(object): """ def get_context_data(self, **kwargs): + if 'view' not in kwargs: + kwargs['view'] = self return kwargs @@ -140,11 +142,11 @@ class TemplateResponseMixin(object): class TemplateView(TemplateResponseMixin, ContextMixin, View): """ - A view that renders a template. This view is different from all the others - insofar as it also passes ``kwargs`` as ``params`` to the template context. + A view that renders a template. This view will also pass into the context + any keyword arguments passed by the url conf. """ def get(self, request, *args, **kwargs): - context = self.get_context_data(params=kwargs) + context = self.get_context_data(**kwargs) return self.render_to_response(context) diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index 08b7318738..d44246f0b7 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -327,6 +327,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): Abstract base class for date-based views displaying a list of objects. """ allow_empty = False + date_list_period = 'year' def get(self, request, *args, **kwargs): self.date_list, self.object_list, extra_context = self.get_dated_items() @@ -370,13 +371,18 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): return qs - def get_date_list(self, queryset, date_type): + def get_date_list_period(self): + return self.date_list_period + + def get_date_list(self, queryset, date_type=None): """ Get a date list by calling `queryset.dates()`, checking along the way for empty lists that aren't allowed. """ date_field = self.get_date_field() allow_empty = self.get_allow_empty() + if date_type is None: + date_type = self.get_date_list_period() date_list = queryset.dates(date_field, date_type)[::-1] if date_list is not None and not date_list and not allow_empty: @@ -400,7 +406,7 @@ class BaseArchiveIndexView(BaseDateListView): Return (date_list, items, extra_context) for this request. """ qs = self.get_dated_queryset(ordering='-%s' % self.get_date_field()) - date_list = self.get_date_list(qs, 'year') + date_list = self.get_date_list(qs) if not date_list: qs = qs.none() @@ -419,6 +425,7 @@ class BaseYearArchiveView(YearMixin, BaseDateListView): """ List of objects published in a given year. """ + date_list_period = 'month' make_object_list = False def get_dated_items(self): @@ -438,7 +445,7 @@ class BaseYearArchiveView(YearMixin, BaseDateListView): } qs = self.get_dated_queryset(ordering='-%s' % date_field, **lookup_kwargs) - date_list = self.get_date_list(qs, 'month') + date_list = self.get_date_list(qs) if not self.get_make_object_list(): # We need this to be a queryset since parent classes introspect it @@ -470,6 +477,8 @@ class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): """ List of objects published in a given year. """ + date_list_period = 'day' + def get_dated_items(self): """ Return (date_list, items, extra_context) for this request. @@ -489,7 +498,7 @@ class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): } qs = self.get_dated_queryset(**lookup_kwargs) - date_list = self.get_date_list(qs, 'day') + date_list = self.get_date_list(qs) return (date_list, qs, { 'month': date, diff --git a/django/views/static.py b/django/views/static.py index bcac9475e2..2ff22ce13f 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -61,7 +61,7 @@ def serve(request, path, document_root=None, show_indexes=False): mimetype = mimetype or 'application/octet-stream' if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj.st_mtime, statobj.st_size): - return HttpResponseNotModified(content_type=mimetype) + return HttpResponseNotModified() with open(fullpath, 'rb') as f: response = HttpResponse(f.read(), content_type=mimetype) response["Last-Modified"] = http_date(statobj.st_mtime) diff --git a/docs/conf.py b/docs/conf.py index 39a280e464..433fd679a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -190,11 +190,10 @@ modindex_common_prefix = ["django."] # -- Options for LaTeX output -------------------------------------------------- -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +latex_elements = { + 'preamble': ('\\DeclareUnicodeCharacter{2264}{\\ensuremath{\\le}}' + '\\DeclareUnicodeCharacter{2265}{\\ensuremath{\\ge}}') +} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). @@ -218,9 +217,6 @@ latex_documents = [ # If true, show URL addresses after external links. #latex_show_urls = False -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - # Documents to append as an appendix to all manuals. #latex_appendices = [] diff --git a/docs/faq/general.txt b/docs/faq/general.txt index 771af8a2cc..659a8c5ad9 100644 --- a/docs/faq/general.txt +++ b/docs/faq/general.txt @@ -185,6 +185,6 @@ would be happy to help you. You might also be interested in posting a job to http://djangogigs.com/ . If you want to find Django-capable people in your local area, try -http://djangopeople.net/ . +https://people.djangoproject.com/ . .. _developers for hire page: https://code.djangoproject.com/wiki/DevelopersForHire diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 054c831fad..5b27af82d6 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -189,7 +189,7 @@ passed around inside the template code: They're commonly used for output that contains raw HTML that is intended to be interpreted as-is on the client side. - Internally, these strings are of type ``SafeString`` or ``SafeUnicode``. + Internally, these strings are of type ``SafeBytes`` or ``SafeText``. They share a common base class of ``SafeData``, so you can test for them using code like: @@ -204,8 +204,8 @@ passed around inside the template code: not. These strings are only escaped once, however, even if auto-escaping applies. - Internally, these strings are of type ``EscapeString`` or - ``EscapeUnicode``. Generally you don't have to worry about these; they + Internally, these strings are of type ``EscapeBytes`` or + ``EscapeText``. Generally you don't have to worry about these; they exist for the implementation of the :tfilter:`escape` filter. Template filter code falls into one of two situations: diff --git a/docs/howto/deployment/wsgi/gunicorn.txt b/docs/howto/deployment/wsgi/gunicorn.txt index 13d4043e37..c4483291a3 100644 --- a/docs/howto/deployment/wsgi/gunicorn.txt +++ b/docs/howto/deployment/wsgi/gunicorn.txt @@ -13,7 +13,7 @@ There are two ways to use Gunicorn with Django. One is to have Gunicorn treat Django as just another WSGI application. The second is to use Gunicorn's special `integration with Django`_. -.. _integration with Django: http://gunicorn.org/run.html#django-manage-py_ +.. _integration with Django: http://gunicorn.org/run.html#django-manage-py Installing Gunicorn =================== diff --git a/docs/index.txt b/docs/index.txt index 011ecdb0bc..3f4e30385c 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -104,9 +104,12 @@ to know about views via the links below: :doc:`Custom storage ` * **Class-based views:** - :doc:`Overview` | - :doc:`Built-in class-based views` | - :doc:`Built-in view mixins` + :doc:`Overview ` | + :doc:`Built-in display views ` | + :doc:`Built-in editing views ` | + :doc:`Using mixins ` | + :doc:`API reference ` | + :doc:`Flattened index` * **Advanced:** :doc:`Generating CSV ` | diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 5567c26fa1..2faace99a5 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -77,7 +77,7 @@ record of being helpful on the mailing lists, and a proven desire to dedicate serious time to Django. In return, they've been granted the coveted commit bit, and have free rein to hack on all parts of Django. -`Malcolm Tredinnick`_ +Malcolm Tredinnick Malcolm originally wanted to be a mathematician, somehow ended up a software developer. He's contributed to many Open Source projects, has served on the board of the GNOME foundation, and will kick your ass at chess. @@ -85,8 +85,6 @@ and have free rein to hack on all parts of Django. When he's not busy being an International Man of Mystery, Malcolm lives in Sydney, Australia. -.. _malcolm tredinnick: http://www.pointy-stick.com/ - `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 diff --git a/docs/internals/contributing/bugs-and-features.txt b/docs/internals/contributing/bugs-and-features.txt index 91a078cbc0..53b0e1007d 100644 --- a/docs/internals/contributing/bugs-and-features.txt +++ b/docs/internals/contributing/bugs-and-features.txt @@ -62,8 +62,6 @@ To understand the lifecycle of your ticket once you have created it, refer to .. _django-updates: http://groups.google.com/group/django-updates -.. _reporting-security-issues: - Reporting user interface bugs and features ------------------------------------------ diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index d15b2f43ae..6d4fb7eef1 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -550,5 +550,16 @@ with the :ttag:`url` template tag:
  • {{ poll.question }}
  • +.. note:: + + If ``{% url 'polls.views.detail' poll.id %}`` (with quotes) doesn't work, + but ``{% url polls.views.detail poll.id %}`` (without quotes) does, that + means you're using a version of Django ≤ 1.4. In this case, add the + following declaration at the top of your template: + + .. code-block:: html+django + + {% load url from future %} + When you're comfortable with writing views, read :doc:`part 4 of this tutorial ` to learn about simple form processing and generic views. diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 5e0360c88f..3f82b44f46 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -8,6 +8,9 @@ themselves or inherited from. They may not provide all the capabilities required for projects, in which case there are Mixins and Generic class-based views. +View +---- + .. class:: django.views.generic.base.View The master class-based base view. All other class-based views inherit from @@ -31,13 +34,13 @@ views. **Example urls.py**:: from django.conf.urls import patterns, url - + from myapp.views import MyView - + urlpatterns = patterns('', url(r'^mine/$', MyView.as_view(), name='my-view'), ) - + **Methods** .. method:: dispatch(request, *args, **kwargs) @@ -61,13 +64,16 @@ views. The default implementation returns ``HttpResponseNotAllowed`` with list of allowed methods in plain text. - - .. note:: + + .. note:: Documentation on class-based views is a work in progress. As yet, only the methods defined directly on the class are documented here, not methods defined on superclasses. +TemplateView +------------ + .. class:: django.views.generic.base.TemplateView Renders a given template, passing it a ``{{ params }}`` template variable, @@ -84,22 +90,22 @@ views. 1. :meth:`dispatch()` 2. :meth:`http_method_not_allowed()` 3. :meth:`get_context_data()` - + **Example views.py**:: from django.views.generic.base import TemplateView - + from articles.models import Article class HomePageView(TemplateView): template_name = "home.html" - + def get_context_data(self, **kwargs): context = super(HomePageView, self).get_context_data(**kwargs) context['latest_articles'] = Article.objects.all()[:5] return context - + **Example urls.py**:: from django.conf.urls import patterns, url @@ -126,12 +132,15 @@ views. * ``params``: The dictionary of keyword arguments captured from the URL pattern that served the view. - .. note:: + .. note:: Documentation on class-based views is a work in progress. As yet, only the methods defined directly on the class are documented here, not methods defined on superclasses. +RedirectView +------------ + .. class:: django.views.generic.base.RedirectView Redirects to a given URL. @@ -159,7 +168,7 @@ views. from django.shortcuts import get_object_or_404 from django.views.generic.base import RedirectView - + from articles.models import Article class ArticleCounterRedirectView(RedirectView): @@ -176,7 +185,7 @@ views. from django.conf.urls import patterns, url from django.views.generic.base import RedirectView - + from article.views import ArticleCounterRedirectView urlpatterns = patterns('', @@ -217,7 +226,7 @@ views. behavior they wish, as long as the method returns a redirect-ready URL string. - .. note:: + .. note:: Documentation on class-based views is a work in progress. As yet, only the methods defined directly on the class are documented here, not methods diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt new file mode 100644 index 0000000000..cbce3690e2 --- /dev/null +++ b/docs/ref/class-based-views/flattened-index.txt @@ -0,0 +1,556 @@ +=========================================== +Class-based generic views - flattened index +=========================================== + +This index provides an alternate organization of the reference documentation +for class-based views. For each view, the effective attributes and methods from +the class tree are represented under that view. For the reference +documentation organized by the class which defines the behavior, see +:doc:`Class-based views
    ` + +Simple generic views +-------------------- + +View +~~~~ +.. class:: View() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.base.View.http_method_names` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` + +TemplateView +~~~~~~~~~~~~ +.. class:: TemplateView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.base.TemplateView.get` +* :meth:`~django.views.generic.base.TemplateView.get_context_data` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +RedirectView +~~~~~~~~~~~~ +.. class:: RedirectView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.base.RedirectView.permanent` +* :attr:`~django.views.generic.base.RedirectView.query_string` +* :attr:`~django.views.generic.base.RedirectView.url` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.RedirectView.delete` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.base.RedirectView.get` +* :meth:`~django.views.generic.base.RedirectView.get_redirect_url` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.base.RedirectView.options` +* :meth:`~django.views.generic.base.RedirectView.post` +* :meth:`~django.views.generic.base.RedirectView.put` + +Detail Views +------------ + +DetailView +~~~~~~~~~~ +.. class:: DetailView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.detail.SingleObjectMixin.model` +* :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_url_kwarg` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_field` +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.detail.BaseDetailView.get` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +List Views +---------- + +ListView +~~~~~~~~ +.. class:: ListView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.list.BaseListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +Editing views +------------- + +FormView +~~~~~~~~ +.. class:: FormView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.edit.FormMixin.form_class` [:meth:`~django.views.generic.edit.FormMixin.get_form_class`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.edit.FormMixin.initial` [:meth:`~django.views.generic.edit.FormMixin.get_initial`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.edit.FormMixin.success_url` [:meth:`~django.views.generic.edit.FormMixin.get_success_url`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.edit.FormMixin.form_invalid` +* :meth:`~django.views.generic.edit.FormMixin.form_valid` +* :meth:`~django.views.generic.edit.ProcessFormView.get` +* :meth:`~django.views.generic.edit.FormMixin.get_context_data` +* :meth:`~django.views.generic.edit.FormMixin.get_form` +* :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.edit.ProcessFormView.post` +* :meth:`~django.views.generic.edit.ProcessFormView.put` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +CreateView +~~~~~~~~~~ +.. class:: CreateView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.edit.FormMixin.form_class` [:meth:`~django.views.generic.edit.FormMixin.get_form_class`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.edit.FormMixin.initial` [:meth:`~django.views.generic.edit.FormMixin.get_initial`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.model` +* :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_url_kwarg` +* :attr:`~django.views.generic.edit.FormMixin.success_url` [:meth:`~django.views.generic.edit.FormMixin.get_success_url`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_field` +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.edit.FormMixin.form_invalid` +* :meth:`~django.views.generic.edit.FormMixin.form_valid` +* :meth:`~django.views.generic.edit.ProcessFormView.get` +* :meth:`~django.views.generic.edit.FormMixin.get_context_data` +* :meth:`~django.views.generic.edit.FormMixin.get_form` +* :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.edit.ProcessFormView.post` +* :meth:`~django.views.generic.edit.ProcessFormView.put` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +UpdateView +~~~~~~~~~~ +.. class:: UpdateView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.edit.FormMixin.form_class` [:meth:`~django.views.generic.edit.FormMixin.get_form_class`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.edit.FormMixin.initial` [:meth:`~django.views.generic.edit.FormMixin.get_initial`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.model` +* :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_url_kwarg` +* :attr:`~django.views.generic.edit.FormMixin.success_url` [:meth:`~django.views.generic.edit.FormMixin.get_success_url`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_field` +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.edit.FormMixin.form_invalid` +* :meth:`~django.views.generic.edit.FormMixin.form_valid` +* :meth:`~django.views.generic.edit.ProcessFormView.get` +* :meth:`~django.views.generic.edit.FormMixin.get_context_data` +* :meth:`~django.views.generic.edit.FormMixin.get_form` +* :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.edit.ProcessFormView.post` +* :meth:`~django.views.generic.edit.ProcessFormView.put` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +DeleteView +~~~~~~~~~~ +.. class:: DeleteView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.detail.SingleObjectMixin.model` +* :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_url_kwarg` +* :attr:`~django.views.generic.edit.DeletionMixin.success_url` [:meth:`~django.views.generic.edit.DeletionMixin.get_success_url`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_field` +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.edit.DeletionMixin.delete` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.detail.BaseDetailView.get` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.edit.DeletionMixin.post` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +Date-based views +---------------- + +ArchiveIndexView +~~~~~~~~~~~~~~~~ +.. class:: ArchiveIndexView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.dates.BaseDateListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +YearArchiveView +~~~~~~~~~~~~~~~ +.. class:: YearArchiveView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.dates.BaseYearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.BaseYearArchiveView.get_make_object_list`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` +* :attr:`~django.views.generic.dates.YearMixin.year` [:meth:`~django.views.generic.dates.YearMixin.get_year`] +* :attr:`~django.views.generic.dates.YearMixin.year_format` [:meth:`~django.views.generic.dates.YearMixin.get_year_format`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.dates.BaseDateListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +MonthArchiveView +~~~~~~~~~~~~~~~~ +.. class:: MonthArchiveView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] +* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` +* :attr:`~django.views.generic.dates.YearMixin.year` [:meth:`~django.views.generic.dates.YearMixin.get_year`] +* :attr:`~django.views.generic.dates.YearMixin.year_format` [:meth:`~django.views.generic.dates.YearMixin.get_year_format`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.dates.BaseDateListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` +* :meth:`~django.views.generic.dates.MonthMixin.get_next_month` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +WeekArchiveView +~~~~~~~~~~~~~~~ +.. class:: WeekArchiveView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` +* :attr:`~django.views.generic.dates.WeekMixin.week` [:meth:`~django.views.generic.dates.WeekMixin.get_week`] +* :attr:`~django.views.generic.dates.WeekMixin.week_format` [:meth:`~django.views.generic.dates.WeekMixin.get_week_format`] +* :attr:`~django.views.generic.dates.YearMixin.year` [:meth:`~django.views.generic.dates.YearMixin.get_year`] +* :attr:`~django.views.generic.dates.YearMixin.year_format` [:meth:`~django.views.generic.dates.YearMixin.get_year_format`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.dates.BaseDateListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +DayArchiveView +~~~~~~~~~~~~~~ +.. class:: DayArchiveView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.dates.DayMixin.day` [:meth:`~django.views.generic.dates.DayMixin.get_day`] +* :attr:`~django.views.generic.dates.DayMixin.day_format` [:meth:`~django.views.generic.dates.DayMixin.get_day_format`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] +* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` +* :attr:`~django.views.generic.dates.YearMixin.year` [:meth:`~django.views.generic.dates.YearMixin.get_year`] +* :attr:`~django.views.generic.dates.YearMixin.year_format` [:meth:`~django.views.generic.dates.YearMixin.get_year_format`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.dates.BaseDateListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` +* :meth:`~django.views.generic.dates.DayMixin.get_next_day` +* :meth:`~django.views.generic.dates.MonthMixin.get_next_month` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.dates.DayMixin.get_previous_day` +* :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +TodayArchiveView +~~~~~~~~~~~~~~~~ +.. class:: TodayArchiveView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_allow_empty`] +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.dates.DayMixin.day` [:meth:`~django.views.generic.dates.DayMixin.get_day`] +* :attr:`~django.views.generic.dates.DayMixin.day_format` [:meth:`~django.views.generic.dates.DayMixin.get_day_format`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.list.MultipleObjectMixin.model` +* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] +* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] +* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class` +* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` +* :attr:`~django.views.generic.dates.YearMixin.year` [:meth:`~django.views.generic.dates.YearMixin.get_year`] +* :attr:`~django.views.generic.dates.YearMixin.year_format` [:meth:`~django.views.generic.dates.YearMixin.get_year_format`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.dates.BaseDateListView.get` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` +* :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` +* :meth:`~django.views.generic.dates.DayMixin.get_next_day` +* :meth:`~django.views.generic.dates.MonthMixin.get_next_month` +* :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` +* :meth:`~django.views.generic.dates.DayMixin.get_previous_day` +* :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + +DateDetailView +~~~~~~~~~~~~~~ +.. class:: DateDetailView() + +**Attributes** (with optional accessor): + +* :attr:`~django.views.generic.dates.DateMixin.allow_future` [:meth:`~django.views.generic.dates.DateMixin.get_allow_future`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] +* :attr:`~django.views.generic.dates.DayMixin.day` [:meth:`~django.views.generic.dates.DayMixin.get_day`] +* :attr:`~django.views.generic.dates.DayMixin.day_format` [:meth:`~django.views.generic.dates.DayMixin.get_day_format`] +* :attr:`~django.views.generic.base.View.http_method_names` +* :attr:`~django.views.generic.detail.SingleObjectMixin.model` +* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`] +* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] +* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] +* :attr:`~django.views.generic.detail.SingleObjectMixin.slug_url_kwarg` +* :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_field` +* :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` +* :attr:`~django.views.generic.dates.YearMixin.year` [:meth:`~django.views.generic.dates.YearMixin.get_year`] +* :attr:`~django.views.generic.dates.YearMixin.year_format` [:meth:`~django.views.generic.dates.YearMixin.get_year_format`] + +**Methods** + +* :meth:`~django.views.generic.base.View.as_view` +* :meth:`~django.views.generic.base.View.dispatch` +* :meth:`~django.views.generic.detail.BaseDetailView.get` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` +* :meth:`~django.views.generic.dates.DayMixin.get_next_day` +* :meth:`~django.views.generic.dates.MonthMixin.get_next_month` +* :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` +* :meth:`~django.views.generic.dates.DayMixin.get_previous_day` +* :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` +* :meth:`~django.views.generic.base.View.head` +* :meth:`~django.views.generic.base.View.http_method_not_allowed` +* :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 69a98df77b..12776cbb94 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -5,6 +5,9 @@ Generic date views Date-based generic views (in the module :mod:`django.views.generic.dates`) are views for displaying drilldown pages for date-based data. +ArchiveIndexView +---------------- + .. class:: django.views.generic.dates.ArchiveIndexView A top-level index page showing the "latest" objects, by date. Objects with @@ -21,11 +24,17 @@ are views for displaying drilldown pages for date-based data. * :class:`django.views.generic.list.MultipleObjectMixin` * :class:`django.views.generic.dates.DateMixin` * :class:`django.views.generic.base.View` - + **Notes** * Uses a default ``context_object_name`` of ``latest``. * Uses a default ``template_name_suffix`` of ``_archive``. + * Defaults to providing ``date_list`` by year, but this can be altered to + month or day using the attribute ``date_list_period``. This also applies + to all subclass views. + +YearArchiveView +--------------- .. class:: django.views.generic.dates.YearArchiveView @@ -86,6 +95,9 @@ are views for displaying drilldown pages for date-based data. * Uses a default ``template_name_suffix`` of ``_archive_year``. +MonthArchiveView +---------------- + .. class:: django.views.generic.dates.MonthArchiveView A monthly archive page showing all objects in a given month. Objects with a @@ -134,6 +146,9 @@ are views for displaying drilldown pages for date-based data. * Uses a default ``template_name_suffix`` of ``_archive_month``. +WeekArchiveView +--------------- + .. class:: django.views.generic.dates.WeekArchiveView A weekly archive page showing all objects in a given week. Objects with a @@ -175,6 +190,9 @@ are views for displaying drilldown pages for date-based data. * Uses a default ``template_name_suffix`` of ``_archive_week``. +DayArchiveView +-------------- + .. class:: django.views.generic.dates.DayArchiveView A day archive page showing all objects in a given day. Days in the future @@ -225,6 +243,9 @@ are views for displaying drilldown pages for date-based data. * Uses a default ``template_name_suffix`` of ``_archive_day``. +TodayArchiveView +---------------- + .. class:: django.views.generic.dates.TodayArchiveView A day archive page showing all objects for *today*. This is exactly the @@ -246,6 +267,10 @@ are views for displaying drilldown pages for date-based data. * :class:`django.views.generic.dates.DateMixin` * :class:`django.views.generic.base.View` + +DateDetailView +-------------- + .. class:: django.views.generic.dates.DateDetailView A page representing an individual object. If the object has a date value in diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index bbf0d4f05a..ef3bc179ee 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -5,6 +5,9 @@ Generic display views The two following generic class-based views are designed to display data. On many projects they are typically the most commonly used views. +DetailView +---------- + .. class:: django.views.generic.detail.DetailView While this view is executing, ``self.object`` will contain the object that @@ -39,7 +42,7 @@ many projects they are typically the most commonly used views. from articles.models import Article class ArticleDetailView(DetailView): - + model = Article def get_context_data(self, **kwargs): @@ -55,7 +58,10 @@ many projects they are typically the most commonly used views. urlpatterns = patterns('', url(r'^(?P[-_\w]+)/$', ArticleDetailView.as_view(), name='article-detail'), - ) + ) + +ListView +-------- .. class:: django.views.generic.list.ListView diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index a65a59bc8b..2fac06ee02 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -2,7 +2,7 @@ Generic editing views ===================== -The following views are described on this page and provide a foundation for +The following views are described on this page and provide a foundation for editing content: * :class:`django.views.generic.edit.FormView` @@ -13,7 +13,7 @@ editing content: .. note:: Some of the examples on this page assume that a model titled 'Author' - has been defined. For these cases we assume the following has been defined + has been defined. For these cases we assume the following has been defined in `myapp/models.py`:: from django import models @@ -25,6 +25,9 @@ editing content: def get_absolute_url(self): return reverse('author-detail', kwargs={'pk': self.pk}) +FormView +-------- + .. class:: django.views.generic.edit.FormView A view that displays a form. On error, redisplays the form with validation @@ -69,6 +72,8 @@ editing content: form.send_email() return super(ContactView, self).form_valid(form) +CreateView +---------- .. class:: django.views.generic.edit.CreateView @@ -94,7 +99,7 @@ editing content: .. attribute:: template_name_suffix The CreateView page displayed to a GET request uses a - ``template_name_suffix`` of ``'_form.html'``. For + ``template_name_suffix`` of ``'_form.html'``. For example, changing this attribute to ``'_create_form.html'`` for a view creating objects for the the example `Author` model would cause the the default `template_name` to be ``'myapp/author_create_form.html'``. @@ -107,6 +112,9 @@ editing content: class AuthorCreate(CreateView): model = Author +UpdateView +---------- + .. class:: django.views.generic.edit.UpdateView A view that displays a form for editing an existing object, redisplaying @@ -133,10 +141,10 @@ editing content: .. attribute:: template_name_suffix The UpdateView page displayed to a GET request uses a - ``template_name_suffix`` of ``'_form.html'``. For + ``template_name_suffix`` of ``'_form.html'``. For example, changing this attribute to ``'_update_form.html'`` for a view updating objects for the the example `Author` model would cause the the - default `template_name` to be ``'myapp/author_update_form.html'``. + default `template_name` to be ``'myapp/author_update_form.html'``. **Example views.py**:: @@ -146,6 +154,9 @@ editing content: class AuthorUpdate(UpdateView): model = Author +DeleteView +---------- + .. class:: django.views.generic.edit.DeleteView A view that displays a confirmation page and deletes an existing object. @@ -171,10 +182,10 @@ editing content: .. attribute:: template_name_suffix The DeleteView page displayed to a GET request uses a - ``template_name_suffix`` of ``'_confirm_delete.html'``. For + ``template_name_suffix`` of ``'_confirm_delete.html'``. For example, changing this attribute to ``'_check_delete.html'`` for a view deleting objects for the the example `Author` model would cause the the - default `template_name` to be ``'myapp/author_check_delete.html'``. + default `template_name` to be ``'myapp/author_check_delete.html'``. **Example views.py**:: @@ -185,4 +196,4 @@ editing content: class AuthorDelete(DeleteView): model = Author - success_url = reverse_lazy('author-list') + success_url = reverse_lazy('author-list') diff --git a/docs/ref/class-based-views/index.txt b/docs/ref/class-based-views/index.txt index f2271d2506..f0e7bbc6c1 100644 --- a/docs/ref/class-based-views/index.txt +++ b/docs/ref/class-based-views/index.txt @@ -6,13 +6,14 @@ Class-based views API reference. For introductory material, see :doc:`/topics/class-based-views/index`. .. toctree:: - :maxdepth: 1 + :maxdepth: 3 base generic-display generic-editing generic-date-based mixins + flattened-index Specification ------------- diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index a65471e68d..6bf6f10b5d 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -3,6 +3,9 @@ Date-based mixins ================= +YearMixin +--------- + .. class:: django.views.generic.dates.YearMixin A mixin that can be used to retrieve and provide parsing information for a @@ -36,6 +39,9 @@ Date-based mixins Raises a 404 if no valid year specification can be found. +MonthMixin +---------- + .. class:: django.views.generic.dates.MonthMixin A mixin that can be used to retrieve and provide parsing information for a @@ -82,6 +88,9 @@ Date-based mixins date provided. If ``allow_empty = False``, returns the previous month that contained data. +DayMixin +-------- + .. class:: django.views.generic.dates.DayMixin A mixin that can be used to retrieve and provide parsing information for a @@ -127,6 +136,9 @@ Date-based mixins Returns a date object containing the previous day. If ``allow_empty = False``, returns the previous day that contained data. +WeekMixin +--------- + .. class:: django.views.generic.dates.WeekMixin A mixin that can be used to retrieve and provide parsing information for a @@ -161,6 +173,9 @@ Date-based mixins Raises a 404 if no valid week specification can be found. +DateMixin +--------- + .. class:: django.views.generic.dates.DateMixin A mixin class providing common behavior for all date-based views. @@ -204,6 +219,9 @@ Date-based mixins is greater than the current date/time. Returns :attr:`DateMixin.allow_future` by default. +BaseDateListView +---------------- + .. class:: django.views.generic.dates.BaseDateListView A base class that provides common behavior for all date-based views. There diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index 89610889db..95dd24f442 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -14,6 +14,9 @@ The following mixins are used to construct Django's editing views: Examples of how these are combined into editing views can be found at the documentation on ``Generic editing views``. +FormMixin +--------- + .. class:: django.views.generic.edit.FormMixin A mixin class that provides facilities for creating and displaying forms. @@ -90,6 +93,9 @@ The following mixins are used to construct Django's editing views: :meth:`~django.views.generic.FormMixin.form_invalid`. +ModelFormMixin +-------------- + .. class:: django.views.generic.edit.ModelFormMixin A form mixin that works on ModelForms, rather than a standalone form. @@ -147,12 +153,16 @@ The following mixins are used to construct Django's editing views: .. method:: form_invalid() Renders a response, providing the invalid form as context. - + + +ProcessFormView +--------------- + .. class:: django.views.generic.edit.ProcessFormView A mixin that provides basic HTTP GET and POST workflow. - .. note:: + .. note:: This is named 'ProcessFormView' and inherits directly from :class:`django.views.generic.base.View`, but breaks if used diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index b414355c6b..8bc613b887 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -2,6 +2,9 @@ Multiple object mixins ====================== +MultipleObjectMixin +------------------- + .. class:: django.views.generic.list.MultipleObjectMixin A mixin that can be used to display a list of objects. @@ -148,6 +151,9 @@ Multiple object mixins this context variable will be ``None``. +MultipleObjectTemplateResponseMixin +----------------------------------- + .. class:: django.views.generic.list.MultipleObjectTemplateResponseMixin A mixin class that performs template-based response rendering for views diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 33d0db3134..61fc945cd3 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -2,6 +2,9 @@ Simple mixins ============= +ContextMixin +------------ + .. class:: django.views.generic.base.ContextMixin .. versionadded:: 1.5 @@ -14,8 +17,23 @@ Simple mixins .. method:: get_context_data(**kwargs) - Returns a dictionary representing the template context. The - keyword arguments provided will make up the returned context. + Returns a dictionary representing the template context. The keyword + arguments provided will make up the returned context. + + The template context of all class-based generic views include a + ``view`` variable that points to the ``View`` instance. + + .. admonition:: Use ``alters_data`` where appropriate + + Note that having the view instance in the template context may + expose potentially hazardous methods to template authors. To + prevent methods like this from being called in the template, set + ``alters_data=True`` on those methods. For more information, read + the documentation on :ref:`rendering a template context + `. + +TemplateResponseMixin +--------------------- .. class:: django.views.generic.base.TemplateResponseMixin diff --git a/docs/ref/class-based-views/mixins-single-object.txt b/docs/ref/class-based-views/mixins-single-object.txt index a31608dbd4..77f52b96c6 100644 --- a/docs/ref/class-based-views/mixins-single-object.txt +++ b/docs/ref/class-based-views/mixins-single-object.txt @@ -2,6 +2,9 @@ Single object mixins ==================== +SingleObjectMixin +----------------- + .. class:: django.views.generic.detail.SingleObjectMixin Provides a mechanism for looking up an object associated with the @@ -86,6 +89,9 @@ Single object mixins ``context_object_name`` is specified, that variable will also be set in the context, with the same value as ``object``. +SingleObjectTemplateResponseMixin +--------------------------------- + .. class:: django.views.generic.detail.SingleObjectTemplateResponseMixin A mixin class that performs template-based response rendering for views diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 4d39981a4d..66a5a2cc4f 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -561,8 +561,6 @@ subclass:: .. attribute:: ModelAdmin.list_filter - .. versionchanged:: 1.4 - Set ``list_filter`` to activate filters in the right sidebar of the change list page of the admin, as illustrated in the following screenshot: @@ -586,6 +584,8 @@ subclass:: class PersonAdmin(UserAdmin): list_filter = ('company__name',) + .. versionadded:: 1.4 + * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`, which you need to provide the ``title`` and ``parameter_name`` attributes to and override the ``lookups`` and ``queryset`` methods, @@ -623,7 +623,7 @@ subclass:: provided in the query string and retrievable via `self.value()`. """ - # Compare the requested value (either '80s' or 'other') + # Compare the requested value (either '80s' or '90s') # to decide how to filter the queryset. if self.value() == '80s': return queryset.filter(birthday__gte=date(1980, 1, 1), @@ -671,6 +671,8 @@ subclass:: birthday__lte=date(1999, 12, 31)).exists(): yield ('90s', _('in the nineties')) + .. versionadded:: 1.4 + * a tuple, where the first element is a field name and the second element is a class inheriting from :mod:`django.contrib.admin.FieldListFilter`, for example:: diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index af937e036e..4b1dd96280 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -158,11 +158,13 @@ For example:: .. warning:: - There's a known bug in Safari/Webkit which causes the named anchor to be + There's a `known bug`_ in Safari/Webkit which causes the named anchor to be forgotten following a redirect. The practical impact for comments is that the Safari/webkit browsers will arrive at the correct page but will not scroll to the named anchor. +.. _`known bug`: https://bugs.webkit.org/show_bug.cgi?id=24175 + .. templatetag:: get_comment_count Counting comments diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index f25cb31e4b..8d352ff8b2 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -84,47 +84,94 @@ AJAX While the above method can be used for AJAX POST requests, it has some inconveniences: you have to remember to pass the CSRF token in as POST data with every POST request. For this reason, there is an alternative method: on each -XMLHttpRequest, set a custom `X-CSRFToken` header to the value of the CSRF +XMLHttpRequest, set a custom ``X-CSRFToken`` header to the value of the CSRF token. This is often easier, because many javascript frameworks provide hooks -that allow headers to be set on every request. In jQuery, you can use the -``ajaxSend`` event as follows: +that allow headers to be set on every request. + +As a first step, you must get the CSRF token itself. The recommended source for +the token is the ``csrftoken`` cookie, which will be set if you've enabled CSRF +protection for your views as outlined above. + +.. note:: + + The CSRF token cookie is named ``csrftoken`` by default, but you can control + the cookie name via the :setting:`CSRF_COOKIE_NAME` setting. + +Acquiring the token is straightforward: .. code-block:: javascript - jQuery(document).ajaxSend(function(event, xhr, settings) { - function getCookie(name) { - var cookieValue = null; - if (document.cookie && document.cookie != '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) == (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } + // using jQuery + function getCookie(name) { + var cookieValue = null; + if (document.cookie && document.cookie != '') { + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) == (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; } } - return cookieValue; - } - function sameOrigin(url) { - // url could be relative or scheme relative or absolute - var host = document.location.host; // host + port - var protocol = document.location.protocol; - var sr_origin = '//' + host; - var origin = protocol + sr_origin; - // Allow absolute or scheme relative URLs to same origin - return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || - (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || - // or any other URL that isn't scheme relative or absolute i.e relative. - !(/^(\/\/|http:|https:).*/.test(url)); - } - function safeMethod(method) { - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } + return cookieValue; + } + var csrftoken = getCookie('csrftoken'); - if (!safeMethod(settings.type) && sameOrigin(settings.url)) { - xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); +The above code could be simplified by using the `jQuery cookie plugin +`_ to replace ``getCookie``: + +.. code-block:: javascript + + var csrftoken = $.cookie('csrftoken'); + +.. note:: + + The CSRF token is also present in the DOM, but only if explicitly included + using :ttag:`csrf_token` in a template. The cookie contains the canonical + token; the ``CsrfViewMiddleware`` will prefer the cookie to the token in + the DOM. Regardless, you're guaranteed to have the cookie if the token is + present in the DOM, so you should use the cookie! + +.. warning:: + + If your view is not rendering a template containing the :ttag:`csrf_token` + template tag, Django might not set the CSRF token cookie. This is common in + cases where forms are dynamically added to the page. To address this case, + Django provides a view decorator which forces setting of the cookie: + :func:`~django.views.decorators.csrf.ensure_csrf_cookie`. + +Finally, you'll have to actually set the header on your AJAX request, while +protecting the CSRF token from being sent to other domains. + +.. code-block:: javascript + + function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + function sameOrigin(url) { + // test that a given url is a same-origin URL + // url could be relative or scheme relative or absolute + var host = document.location.host; // host + port + var protocol = document.location.protocol; + var sr_origin = '//' + host; + var origin = protocol + sr_origin; + // Allow absolute or scheme relative URLs to same origin + return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || + (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || + // or any other URL that isn't scheme relative or absolute i.e relative. + !(/^(\/\/|http:|https:).*/.test(url)); + } + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) { + // Send the token to same-origin, relative URLs only. + // Send the token only if the method warrants CSRF protection + // Using the CSRFToken value acquired earlier + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } } }); @@ -133,18 +180,32 @@ that allow headers to be set on every request. In jQuery, you can use the Due to a bug introduced in jQuery 1.5, the example above will not work correctly on that version. Make sure you are running at least jQuery 1.5.1. -Adding this to a javascript file that is included on your site will ensure that -AJAX POST requests that are made via jQuery will not be caught by the CSRF -protection. +You can use `settings.crossDomain `_ in +jQuery 1.5 and newer in order to replace the `sameOrigin` logic above: -The above code could be simplified by using the `jQuery cookie plugin -`_ to replace ``getCookie``, and -`settings.crossDomain `_ in jQuery 1.5 and -later to replace ``sameOrigin``. +.. code-block:: javascript -In addition, if the CSRF cookie has not been sent to the client by use of -:ttag:`csrf_token`, you may need to ensure the client receives the cookie by -using :func:`~django.views.decorators.csrf.ensure_csrf_cookie`. + function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + $.ajaxSetup({ + crossDomain: false, // obviates need for sameOrigin test + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type)) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } + } + }); + +.. note:: + + In a `security release blogpost`_, a simpler "same origin test" example + was provided which only checked for a relative URL. The ``sameOrigin`` + test above supersedes that example—it works for edge cases like + scheme-relative or absolute URLs for the same domain. + +.. _security release blogpost: https://www.djangoproject.com/weblog/2011/feb/08/security/ Other template engines ---------------------- diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index 6b5773b5a4..3de449708f 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -42,6 +42,16 @@ To install the flatpages app, follow these steps: 2. Add ``'django.contrib.flatpages'`` to your :setting:`INSTALLED_APPS` setting. +Then either: + +3. Add an entry in your URLconf. For example:: + + urlpatterns = patterns('', + ('^pages/', include('django.contrib.flatpages.urls')), + ) + +or: + 3. Add ``'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware'`` to your :setting:`MIDDLEWARE_CLASSES` setting. @@ -57,8 +67,38 @@ and ``django_flatpage_sites``. ``django_flatpage`` is a simple lookup table that simply maps a URL to a title and bunch of text content. ``django_flatpage_sites`` associates a flatpage with a site. +Using the URLconf +----------------- + +There are several ways to include the flat pages in your URLconf. You can +dedicate a particular path to flat pages:: + + urlpatterns = patterns('', + ('^pages/', include('django.contrib.flatpages.urls')), + ) + +You can also set it up as a "catchall" pattern. In this case, it is important +to place the pattern at the end of the other urlpatterns:: + + # Your other patterns here + urlpatterns += patterns('django.contrib.flatpages.views', + (r'^(?P.*)$', 'flatpage'), + ) + +Another common setup is to use flat pages for a limited set of known pages and +to hard code the urls, so you can reference them with the :ttag:`url` template +tag:: + + urlpatterns += patterns('django.contrib.flatpages.views', + url(r'^about-us/$', 'flatpage', {'url': '/about-us/'}, name='about'), + url(r'^license/$', 'flatpage', {'url': '/license/'}, name='license'), + ) + +Using the middleware +-------------------- + The :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` -does all of the work. +can do all of the work. .. class:: FlatpageFallbackMiddleware @@ -255,4 +295,3 @@ For example: {% get_flatpages '/about/' as about_pages %} {% get_flatpages about_prefix as about_pages %} {% get_flatpages '/about/' for someuser as about_pages %} - diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index 7d229a5d66..b8e585a4d2 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -86,8 +86,8 @@ the message itself. Here's what the :file:`forms.py` might look like:: section :ref:`Handling files ` below to learn more about what to do. -Creating a ``WizardView`` class -------------------------------- +Creating a ``WizardView`` subclass +---------------------------------- The next step is to create a :class:`django.contrib.formtools.wizard.views.WizardView` subclass. You can @@ -528,7 +528,7 @@ We define our wizard in a ``views.py``:: We need to add the ``ContactWizard`` to our ``urls.py`` file:: - from django.conf.urls import pattern + from django.conf.urls import patterns from myapp.forms import ContactForm1, ContactForm2 from myapp.views import ContactWizard, show_message_form_condition diff --git a/docs/ref/contrib/gis/install.txt b/docs/ref/contrib/gis/install.txt index 72bd72a6f8..3e952c173b 100644 --- a/docs/ref/contrib/gis/install.txt +++ b/docs/ref/contrib/gis/install.txt @@ -60,14 +60,14 @@ The geospatial libraries required for a GeoDjango installation depends on the spatial database used. The following lists the library requirements, supported versions, and any notes for each of the supported database backends: -================== ============================== ================== ========================================================== +================== ============================== ================== ========================================= Database Library Requirements Supported Versions Notes -================== ============================== ================== ========================================================== +================== ============================== ================== ========================================= PostgreSQL GEOS, PROJ.4, PostGIS 8.1+ Requires PostGIS. MySQL GEOS 5.x Not OGC-compliant; limited functionality. Oracle GEOS 10.2, 11 XE not supported; not tested with 9. -SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 2.3+, pysqlite2 2.5+, and Django 1.1. -================== ============================== ================== ========================================================== +SQLite GEOS, GDAL, PROJ.4, SpatiaLite 3.6.+ Requires SpatiaLite 2.3+, pysqlite2 2.5+ +================== ============================== ================== ========================================= .. _geospatial_libs: @@ -140,7 +140,7 @@ internal geometry representation used by GeoDjango (it's behind the "lazy" geometries). Specifically, the C API library is called (e.g., ``libgeos_c.so``) directly from Python using ctypes. -First, download GEOS 3.2 from the refractions Web site and untar the source +First, download GEOS 3.3.0 from the refractions Web site and untar the source archive:: $ wget http://download.osgeo.org/geos/geos-3.3.0.tar.bz2 @@ -421,7 +421,7 @@ After SQLite has been built with the R*Tree module enabled, get the latest SpatiaLite library source and tools bundle from the `download page`__:: $ wget http://www.gaia-gis.it/gaia-sins/libspatialite-sources/libspatialite-amalgamation-2.3.1.tar.gz - $ wget http://www.gaia-gis.it/gaia-sins/libspatialite-sources/spatialite-tools-2.3.1.tar.gz + $ wget http://www.gaia-gis.it/gaia-sins/spatialite-tools-sources/spatialite-tools-2.3.1.tar.gz $ tar xzf libspatialite-amalgamation-2.3.1.tar.gz $ tar xzf spatialite-tools-2.3.1.tar.gz @@ -467,8 +467,8 @@ pysqlite2 ^^^^^^^^^ Because SpatiaLite must be loaded as an external extension, it requires the -``enable_load_extension`` method, which is only available in versions 2.5+. -Thus, download pysqlite2 2.6, and untar:: +``enable_load_extension`` method, which is only available in versions 2.5+ of +pysqlite2. Thus, download pysqlite2 2.6, and untar:: $ wget http://pysqlite.googlecode.com/files/pysqlite-2.6.0.tar.gz $ tar xzf pysqlite-2.6.0.tar.gz @@ -582,8 +582,8 @@ Creating a spatial database for SpatiaLite After you've installed SpatiaLite, you'll need to create a number of spatial metadata tables in your database in order to perform spatial queries. -If you're using SpatiaLite 3.0 or newer, use the ``spatialite`` utility to -call the ``InitSpatiaMetaData()`` function, like this:: +If you're using SpatiaLite 2.4 or newer, use the ``spatialite`` utility to +call the ``InitSpatialMetaData()`` function, like this:: $ spatialite geodjango.db "SELECT InitSpatialMetaData();" the SPATIAL_REF_SYS table already contains some row(s) @@ -593,19 +593,17 @@ call the ``InitSpatiaMetaData()`` function, like this:: You can safely ignore the error messages shown. When you've done this, you can skip the rest of this section. -If you're using a version of SpatiaLite older than 3.0, you'll need to download -a database-initialization file and execute its SQL queries in your database. +If you're using SpatiaLite 2.3, you'll need to download a +database-initialization file and execute its SQL queries in your database. -First, get it from the appropriate SpatiaLite Resources page ( -http://www.gaia-gis.it/spatialite-2.3.1/resources.html for 2.3 or -http://www.gaia-gis.it/spatialite-2.4.0/ for 2.4):: +First, get it from the `SpatiaLite Resources`__ page:: $ wget http://www.gaia-gis.it/spatialite-2.3.1/init_spatialite-2.3.sql.gz $ gunzip init_spatialite-2.3.sql.gz Then, use the ``spatialite`` command to initialize a spatial database:: - $ spatialite geodjango.db < init_spatialite-2.X.sql + $ spatialite geodjango.db < init_spatialite-2.3.sql .. note:: @@ -613,6 +611,8 @@ Then, use the ``spatialite`` command to initialize a spatial database:: you want to use. Use the same in the :setting:`DATABASES` ``"name"`` key inside your ``settings.py``. +__ http://www.gaia-gis.it/spatialite-2.3.1/resources.html + Add ``django.contrib.gis`` to :setting:`INSTALLED_APPS` ------------------------------------------------------- @@ -643,10 +643,6 @@ Invoke the Django shell from your project and execute the >>> from django.contrib.gis.utils import add_srs_entry >>> add_srs_entry(900913) -.. note:: - - In Django 1.1 the name of this function is ``add_postgis_srs``. - This adds an entry for the 900913 SRID to the ``spatial_ref_sys`` (or equivalent) table, making it possible for the spatial database to transform coordinates in this projection. You only need to execute this command *once* per spatial database. @@ -824,8 +820,10 @@ Download the framework packages for: * GDAL Install the packages in the order they are listed above, as the GDAL and SQLite -packages require the packages listed before them. Afterwards, you can also -install the KyngChaos binary packages for `PostgreSQL and PostGIS`__. +packages require the packages listed before them. + +Afterwards, you can also install the KyngChaos binary packages for `PostgreSQL +and PostGIS`__. After installing the binary packages, you'll want to add the following to your ``.profile`` to be able to run the package programs from the command-line:: diff --git a/docs/ref/contrib/gis/testing.txt b/docs/ref/contrib/gis/testing.txt index 78663b967c..d12c884a1b 100644 --- a/docs/ref/contrib/gis/testing.txt +++ b/docs/ref/contrib/gis/testing.txt @@ -119,9 +119,10 @@ Settings ``SPATIALITE_SQL`` ^^^^^^^^^^^^^^^^^^ -Only relevant when using a SpatiaLite version older than 3.0. +Only relevant when using a SpatiaLite version 2.3. -By default, the GeoDjango test runner looks for the SpatiaLite SQL in the +By default, the GeoDjango test runner looks for the :ref:`file containing the +SpatiaLite dababase-initialization SQL code ` in the same directory where it was invoked (by default the same directory where ``manage.py`` is located). To use a different location, add the following to your settings:: diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index 3a63493137..15863aee7b 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -671,6 +671,17 @@ of abstraction:: __ http://spatialreference.org/ref/epsg/32140/ +.. admonition:: Raw queries + + When using :doc:`raw queries
    `, you should generally wrap + your geometry fields with the ``asText()`` SQL function so as the field + value will be recognized by GEOS:: + + City.objects.raw('SELECT id, name, asText(point) from myapp_city') + + This is not absolutely required by PostGIS, but generally you should only + use raw queries when you know exactly what you are doing. + Lazy Geometries --------------- Geometries come to GeoDjango in a standardized textual representation. Upon diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 92b5665bea..3e256e9d9e 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -56,10 +56,10 @@ will do some additional queries to set these parameters. Transaction handling --------------------- -:doc:`By default
    `, Django starts a transaction when a -database connection is first used and commits the result at the end of the -request/response handling. The PostgreSQL backends normally operate the same -as any other Django backend in this respect. +:doc:`By default
    `, Django runs with an open +transaction which it commits automatically when any built-in, data-altering +model function is called. The PostgreSQL backends normally operate the same as +any other Django backend in this respect. .. _postgresql-autocommit-mode: diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index 99547f1c9e..ada614df45 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -91,14 +91,18 @@ The ``ContentFile`` Class .. class:: ContentFile(File) The ``ContentFile`` class inherits from :class:`~django.core.files.File`, - but unlike :class:`~django.core.files.File` it operates on string content, - rather than an actual file. For example:: + but unlike :class:`~django.core.files.File` it operates on string content + (bytes also supported), rather than an actual file. For example:: from __future__ import unicode_literals from django.core.files.base import ContentFile - f1 = ContentFile(b"my string content") - f2 = ContentFile("my unicode content encoded as UTF-8".encode('UTF-8')) + f1 = ContentFile("esta sentencia está en español") + f2 = ContentFile(b"these are bytes") + + .. versionchanged:: 1.5 + + ContentFile also accepts Unicode strings. .. currentmodule:: django.core.files.images diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index a43163c5e9..275c696230 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -70,8 +70,8 @@ If ``True``, the field is allowed to be blank. Default is ``False``. Note that this is different than :attr:`~Field.null`. :attr:`~Field.null` is purely database-related, whereas :attr:`~Field.blank` is validation-related. If -a field has ``blank=True``, validation on Django's admin site will allow entry -of an empty value. If a field has ``blank=False``, the field will be required. +a field has ``blank=True``, form validation will allow entry of an empty value. +If a field has ``blank=False``, the field will be required. .. _field-choices: @@ -81,14 +81,11 @@ of an empty value. 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. +field. If this is given, the default form widget will be a select box with +these choices instead of the standard text field. -If this is given, Django's admin will use a select box instead of the standard -text field and will limit choices to the choices given. - -A choices list is an iterable of 2-tuples; the first element in each -tuple is the actual value to be stored, and the second element is the -human-readable name. For example:: +The first element in each tuple is the actual value to be stored, and the +second element is the human-readable name. For example:: YEAR_IN_SCHOOL_CHOICES = ( ('FR', 'Freshman'), @@ -176,7 +173,7 @@ scenes. .. attribute:: Field.db_index -If ``True``, djadmin:`django-admin.py sqlindexes ` will output a +If ``True``, :djadmin:`django-admin.py sqlindexes ` will output a ``CREATE INDEX`` statement for this field. ``db_tablespace`` @@ -203,8 +200,8 @@ callable it will be called every time a new object is created. .. attribute:: Field.editable -If ``False``, the field will not be editable in the admin or via forms -automatically generated from the model class. Default is ``True``. +If ``False``, the field will not be displayed in the admin or any other +:class:`~django.forms.ModelForm`. Default is ``True``. ``error_messages`` ------------------ @@ -224,11 +221,11 @@ the `Field types`_ section below. .. attribute:: Field.help_text -Extra "help" text to be displayed under the field on the object's admin form. -It's useful for documentation even if your object doesn't have an admin form. +Extra "help" text to be displayed with the form widget. It's useful for +documentation even if your field isn't used on a form. -Note that this value is *not* HTML-escaped when it's displayed in the admin -interface. This lets you include HTML in :attr:`~Field.help_text` if you so +Note that this value is *not* HTML-escaped in automatically-generated +forms. This lets you include HTML in :attr:`~Field.help_text` if you so desire. For example:: help_text="Please use the following format: YYYY-MM-DD." @@ -259,7 +256,7 @@ Only one primary key is allowed on an object. If ``True``, this field must be unique throughout the table. -This is enforced at the database level and at the Django admin-form level. If +This is enforced at the database level and by model validation. If you try to save a model with a duplicate value in a :attr:`~Field.unique` field, a :exc:`django.db.IntegrityError` will be raised by the model's :meth:`~django.db.models.Model.save` method. @@ -279,7 +276,7 @@ For example, if you have a field ``title`` that has ``unique_for_date="pub_date"``, then Django wouldn't allow the entry of two records with the same ``title`` and ``pub_date``. -This is enforced at the Django admin-form level but not at the database level. +This is enforced by model validation but not at the database level. ``unique_for_month`` -------------------- @@ -337,7 +334,7 @@ otherwise. See :ref:`automatic-primary-key-fields`. A 64 bit integer, much like an :class:`IntegerField` except that it is guaranteed to fit numbers from -9223372036854775808 to 9223372036854775807. The -admin represents this as an ```` (a single-line input). +default form widget for this field is a :class:`~django.forms.TextInput`. ``BooleanField`` @@ -347,7 +344,8 @@ admin represents this as an ```` (a single-line input). A true/false field. -The admin represents this as a checkbox. +The default form widget for this field is a +:class:`~django.forms.CheckboxInput`. If you need to accept :attr:`~Field.null` values then use :class:`NullBooleanField` instead. @@ -361,7 +359,7 @@ A string field, for small- to large-sized strings. For large amounts of text, use :class:`~django.db.models.TextField`. -The admin represents this as an ```` (a single-line input). +The default form widget for this field is a :class:`~django.forms.TextInput`. :class:`CharField` has one extra required argument: @@ -414,9 +412,10 @@ optional arguments: for creation of timestamps. Note that the current date is *always* used; it's not just a default value that you can override. -The admin represents this as an ```` with a JavaScript -calendar, and a shortcut for "Today". Includes an additional ``invalid_date`` -error message key. +The default form widget for this field is a +:class:`~django.forms.TextInput`. The admin adds a JavaScript calendar, +and a shortcut for "Today". Includes an additional ``invalid_date`` error +message key. .. note:: As currently implemented, setting ``auto_now`` or ``auto_now_add`` to @@ -431,8 +430,9 @@ error message key. A date and time, represented in Python by a ``datetime.datetime`` instance. Takes the same extra arguments as :class:`DateField`. -The admin represents this as two ```` fields, with -JavaScript shortcuts. +The default form widget for this field is a single +:class:`~django.forms.TextInput`. The admin uses two separate +:class:`~django.forms.TextInput` widgets with JavaScript shortcuts. ``DecimalField`` ---------------- @@ -461,7 +461,7 @@ decimal places:: models.DecimalField(..., max_digits=19, decimal_places=10) -The admin represents this as an ```` (a single-line input). +The default form widget for this field is a :class:`~django.forms.TextInput`. .. note:: @@ -539,8 +539,8 @@ Also has one optional argument: Optional. A storage object, which handles the storage and retrieval of your files. See :doc:`/topics/files` for details on how to provide this object. -The admin represents this field as an ```` (a file-upload -widget). +The default form widget for this field is a +:class:`~django.forms.widgets.FileInput`. Using a :class:`FileField` or an :class:`ImageField` (see below) in a model takes a few steps: @@ -725,7 +725,7 @@ can change the maximum length using the :attr:`~CharField.max_length` argument. A floating-point number represented in Python by a ``float`` instance. -The admin represents this as an ```` (a single-line input). +The default form widget for this field is a :class:`~django.forms.TextInput`. .. _floatfield_vs_decimalfield: @@ -776,16 +776,16 @@ length using the :attr:`~CharField.max_length` argument. .. class:: IntegerField([**options]) -An integer. The admin represents this as an ```` (a -single-line input). +An integer. The default form widget for this field is a +:class:`~django.forms.TextInput`. ``IPAddressField`` ------------------ .. class:: IPAddressField([**options]) -An IP address, in string format (e.g. "192.0.2.30"). The admin represents this -as an ```` (a single-line input). +An IP address, in string format (e.g. "192.0.2.30"). The default form widget +for this field is a :class:`~django.forms.TextInput`. ``GenericIPAddressField`` ------------------------- @@ -795,8 +795,8 @@ as an ```` (a single-line input). .. versionadded:: 1.4 An IPv4 or IPv6 address, in string format (e.g. ``192.0.2.30`` or -``2a02:42fe::4``). The admin represents this as an ```` -(a single-line input). +``2a02:42fe::4``). The default form widget for this field is a +:class:`~django.forms.TextInput`. The IPv6 address normalization follows :rfc:`4291#section-2.2` section 2.2, including using the IPv4 format suggested in paragraph 3 of that section, like @@ -823,8 +823,8 @@ are converted to lowercase. .. class:: NullBooleanField([**options]) Like a :class:`BooleanField`, but allows ``NULL`` as one of the options. Use -this instead of a :class:`BooleanField` with ``null=True``. The admin represents -this as a ``