diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2012-09-05 09:39:03 -0400 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2012-09-05 09:39:03 -0400 |
| commit | b546e7eb633022ee1962570387f22fb2bcea46ed (patch) | |
| tree | f87f4a2d68fb66afae39148fa35489930710b623 | |
| parent | cd583d6dbd222ae61331a6965b0e1fc86c974c50 (diff) | |
| parent | cff911f4ba3b3e6393c58da5131ce8b188a68f0c (diff) | |
Merge branch 'master' into schema-alteration
410 files changed, 5815 insertions, 2567 deletions
@@ -373,6 +373,7 @@ answer newbie questions, and generally made Django that much better: michael.mcewan@gmail.com Paul McLanahan <paul@mclanahan.net> Tobias McNulty <http://www.caktusgroup.com/blog> + Andrews Medina <andrewsmedina@gmail.com> Zain Memon Christian Metts michal@plovarna.cz @@ -467,6 +468,7 @@ answer newbie questions, and generally made Django that much better: Vinay Sajip <vinay_sajip@yahoo.co.uk> Bartolome Sanchez Salado <i42sasab@uco.es> Kadesarin Sanjek + Tim Saylor <tim.saylor@gmail.com> Massimo Scamarcia <massimo.scamarcia@gmail.com> Paulo Scardine <paulo@scardine.com.br> 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 %} <ul class="object-tools"> {% block object-tools-items %} - <li><a href="history/" class="historylink">{% trans "History" %}</a></li> + <li><a href="{% url opts|admin_urlname:'history' original.pk %}" class="historylink">{% trans "History" %}</a></li> {% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%} {% endblock %} </ul> 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 '<EasyModel for %s>' % smart_bytes(self.model._meta.object_name) + return force_str('<EasyModel for %s>' % 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('<EasyField for %s.%s>' % (self.model.model._meta.object_name, self.field.name)) + return force_str('<EasyField for %s.%s>' % (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('<EasyChoice for %s.%s>' % (self.model.model._meta.object_name, self.field.name)) + return force_str('<EasyChoice for %s.%s>' % (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('<EasyInstance for %s (%s)>' % (self.model.model._meta.object_name, self.instance._get_pk_val())) + return force_str('<EasyInstance for %s (%s)>' % (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('<EasyInstanceField for %s.%s>' % (self.model.model._meta.object_name, self.field.name)) + return force_str('<EasyInstanceField for %s.%s>' % (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 += "<url><loc>%s/users/%s/</loc></url>" % (self.base_url, username) - self.assertEqual(response.content, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> %s </urlset> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap> </sitemapindex> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <!-- This is a customised template --> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap> </sitemapindex> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>%s/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> </urlset> -""" % (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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>%s/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> </urlset> -""" % (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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <!-- This is a customised template --> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>%s/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> </urlset> -""" % (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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>http://testserver/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> </urlset> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap><loc>%s/cached/sitemap-simple.xml</loc></sitemap> </sitemapindex> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap><loc>%s/secure/sitemap-simple.xml</loc></sitemap> </sitemapindex> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>%s/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> </urlset> -""" % (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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap><loc>%s/simple/sitemap-simple.xml</loc></sitemap> </sitemapindex> -""" % 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, """<?xml version="1.0" encoding="UTF-8"?> + expected_content = """<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url><loc>%s/location/</loc><lastmod>%s</lastmod><changefreq>never</changefreq><priority>0.5</priority></url> </urlset> -""" % (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 + + att_name, params, notes = self.normalize_col_name( + column_name, used_column_names, is_relation) + extra_params.update(params) + comment_notes.extend(notes) - # 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 + 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 '<Options for %s>' % 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('<input{0} />', 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 = '<could not parse>' if request._post_parse_error: post = '<could not parse>' @@ -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 = '<could not parse>' try: cookies = (pformat(COOKIES_override) if COOKIES_override is not None else pformat(request.COOKIES)) - except: + except Exception: cookies = '<could not parse>' try: meta = (pformat(META_override) if META_override is not None else pformat(request.META)) - except: + except Exception: meta = '<could not parse>' 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='<Unknown Template>'): 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 "<Text Node: '%s'>" % smart_bytes(self.s[:25], 'ascii', + return force_str("<Text Node: '%s'>" % 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('</%s>' % 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,10 +47,18 @@ 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'): from django.utils.translation import trans_real 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 --- /dev/null +++ b/django/utils/2to3_fixes/__init__.py 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<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' ) -datetime_re = re.compile( - r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})' - r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})' +time_re = re.compile( + r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' - r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$' ) -time_re = re.compile( - r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' +datetime_re = re.compile( + r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})' + r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})' r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' + r'(?P<tzinfo>Z|[+-]\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): @@ -121,6 +141,19 @@ def smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): 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): if encoding == 'utf-8': return s @@ -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('</%s>' % 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'</\s*%s\s*>' % 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'</\s*%s\s*>' % 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: <span attr="value" /> + 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: <span attr="value" /> - 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] == "</", "unexpected call to parse_endtag" - match = _html_parser.endendtag.search(rawdata, i + 1) # > - if not match: - return -1 - j = match.end() - match = _html_parser.endtagfind.match(rawdata, i) # </ + tag + > - 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] == "</", "unexpected call to parse_endtag" + match = _html_parser.endendtag.search(rawdata, i + 1) # > + if not match: + return -1 + j = match.end() + match = _html_parser.endtagfind.match(rawdata, i) # </ + tag + > + 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 <benjamin@python.org>" -__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 <howto/custom-file-storage>` * **Class-based views:** - :doc:`Overview<topics/class-based-views/index>` | - :doc:`Built-in class-based views<ref/class-based-views/index>` | - :doc:`Built-in view mixins<ref/class-based-views/mixins>` + :doc:`Overview <topics/class-based-views/index>` | + :doc:`Built-in display views <topics/class-based-views/generic-display>` | + :doc:`Built-in editing views <topics/class-based-views/generic-editing>` | + :doc:`Using mixins <topics/class-based-views/mixins>` | + :doc:`API reference <ref/class-based-views/index>` | + :doc:`Flattened index<ref/class-based-views/flattened-index>` * **Advanced:** :doc:`Generating CSV <howto/outputting-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: <li><a href="{% url 'polls.views.detail' poll.id %}">{{ poll.question }}</a></li> +.. 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 </intro/tutorial04>` 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</ref/class-based-views/index>` + +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<slug>[-_\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 + <alters-data-description>`. + +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'); + +The above code could be simplified by using the `jQuery cookie plugin +<http://plugins.jquery.com/project/Cookie>`_ 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! - if (!safeMethod(settings.type) && sameOrigin(settings.url)) { - xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); +.. 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 <http://api.jquery.com/jQuery.ajax>`_ 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 -<http://plugins.jquery.com/project/Cookie>`_ to replace ``getCookie``, and -`settings.crossDomain <http://api.jquery.com/jQuery.ajax>`_ in jQuery 1.5 and -later to replace ``sameOrigin``. +.. code-block:: javascript + + 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. -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`. +.. _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<url>.*)$', '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 <wizard-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 <create_spatialite_db>` 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 </topics/db/sql>`, 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 </topics/db/transactions>`, 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 </topics/db/transactions>`, 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 <sqlindexes>` will output a +If ``True``, :djadmin:`django-admin.py sqlindexes <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: <em>YYYY-MM-DD</em>." @@ -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 ``<input type="text">`` (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 ``<input type="text">`` (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 ``<input type="text">`` (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 ``<input type="text">`` 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 ``<input type="text">`` 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 ``<input type="text">`` (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 ``<input type="file">`` (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 ``<input type="text">`` (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 ``<input type="text">`` (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 ``<input type="text">`` (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 ``<input type="text">`` (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 ``<input type="text">`` -(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 ``<select>`` box with "Unknown", "Yes" and "No" choices. +this instead of a :class:`BooleanField` with ``null=True``. The default form +widget for this field is a :class:`~django.forms.NullBooleanSelect`. ``PositiveIntegerField`` ------------------------ @@ -875,8 +875,8 @@ Like an :class:`IntegerField`, but only allows values under a certain .. class:: TextField([**options]) -A large text field. The admin represents this as a ``<textarea>`` (a multi-line -input). +A large text field. The default form widget for this field is a +:class:`~django.forms.Textarea`. .. admonition:: MySQL users @@ -893,8 +893,8 @@ input). A time, represented in Python by a ``datetime.time`` instance. Accepts the same auto-population options as :class:`DateField`. -The admin represents this as an ``<input type="text">`` with some JavaScript -shortcuts. +The default form widget for this field is a :class:`~django.forms.TextInput`. +The admin adds some JavaScript shortcuts. ``URLField`` ------------ @@ -903,7 +903,7 @@ shortcuts. A :class:`CharField` for a URL. -The admin represents this as an ``<input type="text">`` (a single-line input). +The default form widget for this field is a :class:`~django.forms.TextInput`. Like all :class:`CharField` subclasses, :class:`URLField` takes the optional :attr:`~CharField.max_length`argument. If you don't specify @@ -979,9 +979,9 @@ define the details of how the relation works. .. attribute:: ForeignKey.limit_choices_to A dictionary of lookup arguments and values (see :doc:`/topics/db/queries`) - that limit the available admin choices for this object. Use this with - functions from the Python ``datetime`` module to limit choices of objects by - date. For example:: + that limit the available admin or ModelForm choices for this object. Use + this with functions from the Python ``datetime`` module to limit choices of + objects by date. For example:: limit_choices_to = {'pub_date__lte': datetime.now} diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 14541ad0d1..472ac96457 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -48,7 +48,7 @@ that, you need to :meth:`~Model.save()`. 2. Add a method on a custom manager (usually preferred):: class BookManager(models.Manager): - def create_book(title): + def create_book(self, title): book = self.create(title=title) # do something with the book return book @@ -386,6 +386,12 @@ perform an update on all fields. Specifying ``update_fields`` will force an update. +When saving a model fetched through deferred model loading +(:meth:`~Model.only()` or :meth:`~Model.defer()`) only the fields loaded from +the DB will get updated. In effect there is an automatic ``update_fields`` in +this case. If you assign or change any deferred field value, these fields will +be added to the updated fields. + Deleting objects ================ @@ -453,9 +459,9 @@ using ``__str__()`` like this:: last_name = models.CharField(max_length=50) def __str__(self): - # Note use of django.utils.encoding.smart_bytes() here because + # Note use of django.utils.encoding.force_bytes() here because # first_name and last_name will be unicode strings. - return smart_bytes('%s %s' % (self.first_name, self.last_name)) + return force_bytes('%s %s' % (self.first_name, self.last_name)) ``get_absolute_url`` -------------------- diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 8c188c67c3..b59b2ece82 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1110,6 +1110,14 @@ one, doing so will result in an error. reader, is slightly faster and consumes a little less memory in the Python process. +.. versionchanged:: 1.5 + +.. note:: + + When calling :meth:`~Model.save()` for instances with deferred fields, + only the loaded fields will be saved. See :meth:`~Model.save()` for more + details. + only ~~~~ @@ -1154,6 +1162,14 @@ All of the cautions in the note for the :meth:`defer` documentation apply to options. Also note that using :meth:`only` and omitting a field requested using :meth:`select_related` is an error as well. +.. versionchanged:: 1.5 + +.. note:: + + When calling :meth:`~Model.save()` for instances with deferred fields, + only the loaded fields will be saved. See :meth:`~Model.save()` for more + details. + using ~~~~~ diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 551ee00c6b..21e99de10d 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -731,10 +731,11 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in .. class:: HttpResponseRedirect - The constructor takes a single argument -- the path to redirect to. This - can be a fully qualified URL (e.g. ``'http://www.yahoo.com/search/'``) or - an absolute path with no domain (e.g. ``'/search/'``). Note that this - returns an HTTP status code 302. + The first argument to the constructor is required -- the path to redirect + to. This can be a fully qualified URL + (e.g. ``'http://www.yahoo.com/search/'``) or an absolute path with no + domain (e.g. ``'/search/'``). See :class:`HttpResponse` for other optional + constructor arguments. Note that this returns an HTTP status code 302. .. class:: HttpResponsePermanentRedirect @@ -743,8 +744,9 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in .. class:: HttpResponseNotModified - The constructor doesn't take any arguments. Use this to designate that a - page hasn't been modified since the user's last request (status code 304). + The constructor doesn't take any arguments and no content should be added + to this response. Use this to designate that a page hasn't been modified + since the user's last request (status code 304). .. class:: HttpResponseBadRequest @@ -760,8 +762,9 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in .. class:: HttpResponseNotAllowed - Like :class:`HttpResponse`, but uses a 405 status code. Takes a single, - required argument: a list of permitted methods (e.g. ``['GET', 'POST']``). + Like :class:`HttpResponse`, but uses a 405 status code. The first argument + to the constructor is required: a list of permitted methods (e.g. + ``['GET', 'POST']``). .. class:: HttpResponseGone diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 531ff33da2..4729a2b6f1 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -183,7 +183,7 @@ compose a prefix, version and key into a final cache key. The default implementation is equivalent to the function:: def make_key(key, key_prefix, version): - return ':'.join([key_prefix, str(version), smart_bytes(key)]) + return ':'.join([key_prefix, str(version), key]) You may use any key function you want, as long as it has the same argument signature. @@ -747,14 +747,15 @@ Did you catch that? NEVER deploy a site into production with :setting:`DEBUG` turned on. One of the main features of debug mode is the display of detailed error pages. -If your app raises an exception when ``DEBUG`` is ``True``, Django will display -a detailed traceback, including a lot of metadata about your environment, such -as all the currently defined Django settings (from ``settings.py``). +If your app raises an exception when :setting:`DEBUG` is ``True``, Django will +display a detailed traceback, including a lot of metadata about your +environment, such as all the currently defined Django settings (from +``settings.py``). As a security measure, Django will *not* include settings that might be -sensitive (or offensive), such as ``SECRET_KEY`` or ``PROFANITIES_LIST``. -Specifically, it will exclude any setting whose name includes any of the -following: +sensitive (or offensive), such as :setting:`SECRET_KEY` or +:setting:`PROFANITIES_LIST`. Specifically, it will exclude any setting whose +name includes any of the following: * API * KEY @@ -1356,7 +1357,7 @@ MANAGERS Default: ``()`` (Empty tuple) A tuple in the same format as :setting:`ADMINS` that specifies who should get -broken-link notifications when ``SEND_BROKEN_LINK_EMAILS=True``. +broken-link notifications when :setting:`SEND_BROKEN_LINK_EMAILS` is ``True``. .. setting:: MEDIA_ROOT @@ -1365,8 +1366,8 @@ MEDIA_ROOT Default: ``''`` (Empty string) -Absolute path to the directory that holds media for this installation, used -for :doc:`managing stored files </topics/files>`. +Absolute filesystem path to the directory that will hold :doc:`user-uploaded +files </topics/files>`. Example: ``"/var/www/example.com/media/"`` @@ -1537,9 +1538,23 @@ SECRET_KEY Default: ``''`` (Empty string) -A secret key for this particular Django installation. Used to provide a seed in -secret-key hashing algorithms. Set this to a random string -- the longer, the -better. ``django-admin.py startproject`` creates one automatically. +A secret key for a particular Django installation. This is used to provide +:doc:`cryptographic signing </topics/signing>`, and should be set to a unique, +unpredictable value. + +:djadmin:`django-admin.py startproject <startproject>` automatically adds a +randomly-generated ``SECRET_KEY`` to each new project. + +.. warning:: + + **Keep this value secret.** + + Running Django with a known :setting:`SECRET_KEY` defeats many of Django's + security protections, and can lead to privilege escalation and remote code + execution vulnerabilities. + +.. versionchanged:: 1.5 + Django will now refuse to start if :setting:`SECRET_KEY` is not set. .. setting:: SECURE_PROXY_SSL_HEADER diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index bd2b4c6e9d..48bd346788 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -125,6 +125,10 @@ dot in a variable name, it tries the following lookups, in this order: * Attribute lookup. Example: ``foo.bar`` * List-index lookup. Example: ``foo[bar]`` +Note that "bar" in a template expression like ``{{ foo.bar }}`` will be +interpreted as a literal string and not using the value of the variable "bar", +if one exists in the template context. + The template system uses the first lookup type that works. It's short-circuit logic. Here are a few examples:: @@ -198,6 +202,8 @@ straight lookups. Here are some things to keep in mind: * A variable can only be called if it has no required arguments. Otherwise, the system will return an empty string. +.. _alters-data-description: + * Obviously, there can be side effects when calling some variables, and it'd be either foolish or a security hole to allow the template system to access them. @@ -422,6 +428,10 @@ optional, third positional argument, ``processors``. In this example, the my_data_dictionary, context_instance=RequestContext(request)) + Alternatively, use the :meth:`~django.shortcuts.render()` shortcut which is + the same as a call to :func:`~django.shortcuts.render_to_response()` with a + context_instance argument that forces the use of a ``RequestContext``. + Here's what each of the default processors does: django.contrib.auth.context_processors.auth diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index b6cb1035d3..de19578cac 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -187,6 +187,14 @@ The functions defined in this module share the following properties: 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__``. +.. function:: python_2_unicode_compatible + + 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. + .. function:: smart_text(s, encoding='utf-8', strings_only=False, errors='strict') .. versionadded:: 1.5 @@ -232,14 +240,29 @@ The functions defined in this module share the following properties: If ``strings_only`` is ``True``, don't convert (some) non-string-like objects. +.. function:: force_bytes(s, encoding='utf-8', strings_only=False, errors='strict') + + .. versionadded:: 1.5 + + Similar to ``smart_bytes``, except that lazy instances are resolved to + bytestrings, rather than kept as lazy objects. + + If ``strings_only`` is ``True``, don't convert (some) non-string-like + objects. + .. function:: smart_str(s, encoding='utf-8', strings_only=False, errors='strict') Alias of :func:`smart_bytes` on Python 2 and :func:`smart_text` on Python - 3. This function always returns a :class:`str`. + 3. This function returns a :class:`str` or a lazy string. For instance, this is suitable for writing to :attr:`sys.stdout` on Python 2 and 3. +.. function:: force_str(s, encoding='utf-8', strings_only=False, errors='strict') + + Alias of :func:`force_bytes` on Python 2 and :func:`force_text` on Python + 3. This function always returns a :class:`str`. + .. function:: iri_to_uri(iri) Convert an Internationalized Resource Identifier (IRI) portion to a URI @@ -478,6 +501,33 @@ escaping HTML. through :func:`conditional_escape` which (ultimately) calls :func:`~django.utils.encoding.force_text` on the values. +.. function:: strip_tags(value) + + Removes anything that looks like an html tag from the string, that is + anything contained within ``<>``. + + For example:: + + strip_tags(value) + + If ``value`` is ``"<b>Joel</b> <button>is</button> a <span>slug</span>"`` the + return value will be ``"Joel is a slug"``. + +.. function:: remove_tags(value, tags) + + Removes a list of [X]HTML tag names from the output. + + For example:: + + remove_tags(value, ["b", "span"]) + + If ``value`` is ``"<b>Joel</b> <button>is</button> a <span>slug</span>"`` the + return value will be ``"Joel <button>is</button> a slug"``. + + Note that this filter is case-sensitive. + + If ``value`` is ``"<B>Joel</B> <button>is</button> a <span>slug</span>"`` the + return value will be ``"<B>Joel</B> <button>is</button> a slug"``. .. _str.format: http://docs.python.org/library/stdtypes.html#str.format @@ -552,15 +602,29 @@ string" means that the producer of the string has already turned characters that should not be interpreted by the HTML engine (e.g. '<') into the appropriate entities. +.. class:: SafeBytes + + .. versionadded:: 1.5 + + A :class:`bytes` subclass that has been specifically marked as "safe" + (requires no further escaping) for HTML output purposes. + .. class:: SafeString - A string subclass that has been specifically marked as "safe" (requires no - further escaping) for HTML output purposes. + A :class:`str` subclass that has been specifically marked as "safe" + (requires no further escaping) for HTML output purposes. This is + :class:`SafeBytes` on Python 2 and :class:`SafeText` on Python 3. + +.. class:: SafeText + + .. versionadded:: 1.5 + + A :class:`str` (in Python 3) or :class:`unicode` (in Python 2) subclass + that has been specifically marked as "safe" for HTML output purposes. .. class:: SafeUnicode - A unicode subclass that has been specifically marked as "safe" for HTML - output purposes. + Historical name of :class:`SafeText`. Only available under Python 2. .. function:: mark_safe(s) @@ -577,6 +641,24 @@ appropriate entities. Can be called multiple times on a single string (the resulting escaping is only applied once). +``django.utils.text`` +===================== + +.. module:: django.utils.text + :synopsis: Text manipulation. + +.. function:: slugify + + Converts to lowercase, removes non-word characters (alphanumerics and + underscores) and converts spaces to hyphens. Also strips leading and trailing + whitespace. + + For example:: + + slugify(value) + + If ``value`` is ``"Joel is a slug"``, the output will be ``"joel-is-a-slug"``. + ``django.utils.translation`` ============================ diff --git a/docs/releases/1.3.2.txt b/docs/releases/1.3.2.txt new file mode 100644 index 0000000000..16e00f9732 --- /dev/null +++ b/docs/releases/1.3.2.txt @@ -0,0 +1,14 @@ +========================== +Django 1.3.2 release notes +========================== + +*July 30, 2012* + +This is the second security release in the Django 1.3 series, fixing several +security issues in Django 1.3. Django 1.3.2 is a recommended upgrade for +all users of Django 1.3. + +For a full list of issues addressed in this release, see the `security +advisory`_. + +.. _security advisory: https://www.djangoproject.com/weblog/2012/jul/30/security-releases-issued/ diff --git a/docs/releases/1.4.1.txt b/docs/releases/1.4.1.txt new file mode 100644 index 0000000000..7c6568e5b5 --- /dev/null +++ b/docs/releases/1.4.1.txt @@ -0,0 +1,14 @@ +========================== +Django 1.4.1 release notes +========================== + +*July 30, 2012* + +This is the first security release in the Django 1.4 series, fixing several +security issues in Django 1.4. Django 1.4.1 is a recommended upgrade for +all users of Django 1.4. + +For a full list of issues addressed in this release, see the `security +advisory`_. + +.. _security advisory: https://www.djangoproject.com/weblog/2012/jul/30/security-releases-issued/ diff --git a/docs/releases/1.4.2.txt b/docs/releases/1.4.2.txt new file mode 100644 index 0000000000..6f2e9aca2e --- /dev/null +++ b/docs/releases/1.4.2.txt @@ -0,0 +1,14 @@ +========================== +Django 1.4.2 release notes +========================== + +*TO BE RELEASED* + +This is the second security release in the Django 1.4 series. + +Backwards incompatible changes +============================== + +* The newly introduced :class:`~django.db.models.GenericIPAddressField` + constructor arguments have been adapted to match those of all other model + fields. The first two keyword arguments are now verbose_name and name. diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index f789ebde40..5728d8559a 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -42,6 +42,10 @@ keyword argument ``update_fields``. By using this argument it is possible to save only a select list of model's fields. This can be useful for performance reasons or when trying to avoid overwriting concurrent changes. +Deferred instances (those loaded by .only() or .defer()) will automatically +save just the loaded fields. If any field is set manually after load, that +field will also get updated on save. + See the :meth:`Model.save() <django.db.models.Model.save()>` documentation for more details. @@ -80,6 +84,12 @@ By passing ``False`` using this argument it is now possible to retreive the :class:`ContentType <django.contrib.contenttypes.models.ContentType>` associated with proxy models. +New ``view`` variable in class-based views context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In all :doc:`generic class-based views </topics/class-based-views/index>` +(or any class-based view inheriting from ``ContextMixin``), the context dictionary +contains a ``view`` variable that points to the ``View`` instance. + Minor features ~~~~~~~~~~~~~~ @@ -134,6 +144,14 @@ year|date:"Y" }}``. ``next_year`` and ``previous_year`` were also added in the context. They are calculated according to ``allow_empty`` and ``allow_future``. +Context in TemplateView +~~~~~~~~~~~~~~~~~~~~~~~ + +For consistency with the design of the other generic views, +:class:`~django.views.generic.base.TemplateView` no longer passes a ``params`` +dictionary into the context, instead passing the variables from the URLconf +directly into the context. + OPTIONS, PUT and DELETE requests in the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -154,6 +172,54 @@ If you were using the ``data`` parameter in a PUT request without a ``content_type``, you must encode your data before passing it to the test client and set the ``content_type`` argument. +.. _simplejson-incompatibilities: + +System version of :mod:`simplejson` no longer used +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:ref:`As explained below <simplejson-deprecation>`, Django 1.5 deprecates +:mod:`django.utils.simplejson` in favor of Python 2.6's built-in :mod:`json` +module. In theory, this change is harmless. Unfortunately, because of +incompatibilities between versions of :mod:`simplejson`, it may trigger errors +in some circumstances. + +JSON-related features in Django 1.4 always used :mod:`django.utils.simplejson`. +This module was actually: + +- A system version of :mod:`simplejson`, if one was available (ie. ``import + simplejson`` works), if it was more recent than Django's built-in copy or it + had the C speedups, or +- The :mod:`json` module from the standard library, if it was available (ie. + Python 2.6 or greater), or +- A built-in copy of version 2.0.7 of :mod:`simplejson`. + +In Django 1.5, those features use Python's :mod:`json` module, which is based +on version 2.0.9 of :mod:`simplejson`. + +There are no known incompatibilities between Django's copy of version 2.0.7 and +Python's copy of version 2.0.9. However, there are some incompatibilities +between other versions of :mod:`simplejson`: + +- While the :mod:`simplejson` API is documented as always returning unicode + strings, the optional C implementation can return a byte string. This was + fixed in Python 2.7. +- :class:`simplejson.JSONEncoder` gained a ``namedtuple_as_object`` keyword + argument in version 2.2. + +More information on these incompatibilities is available in `ticket #18023`_. + +The net result is that, if you have installed :mod:`simplejson` and your code +uses Django's serialization internals directly -- for instance +:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from +:mod:`simplejson` to :mod:`json` could break your code. (In general, changes to +internals aren't documented; we're making an exception here.) + +At this point, the maintainers of Django believe that using :mod:`json` from +the standard library offers the strongest guarantee of backwards-compatibility. +They recommend to use it from now on. + +.. _ticket #18023: https://code.djangoproject.com/ticket/18023#comment:10 + String types of hasher method parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -161,7 +227,7 @@ If you have written a :ref:`custom password hasher <auth_password_storage>`, your ``encode()``, ``verify()`` or ``safe_summary()`` methods should accept Unicode parameters (``password``, ``salt`` or ``encoded``). If any of the hashing methods need byte strings, you can use the -:func:`~django.utils.encoding.smart_bytes` utility to encode the strings. +:func:`~django.utils.encoding.force_bytes` utility to encode the strings. Validation of previous_page_number and next_page_number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -257,19 +323,38 @@ Miscellaneous * :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError` instead of :exc:`ValueError` for non-integer inputs. +* The ``slugify`` template filter is now available as a standard python + function at :func:`django.utils.text.slugify`. Similarly, ``remove_tags`` is + available at :func:`django.utils.html.remove_tags`. + Features deprecated in 1.5 ========================== +.. _simplejson-deprecation: + ``django.utils.simplejson`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Since Django 1.5 drops support for Python 2.5, we can now rely on the -:mod:`json` module being in Python's standard library -- so we've removed -our own copy of ``simplejson``. You can safely change any use of -:mod:`django.utils.simplejson` to :mod:`json`. +:mod:`json` module being available in Python's standard library, so we've +removed our own copy of :mod:`simplejson`. You should now import :mod:`json` +instead :mod:`django.utils.simplejson`. + +Unfortunately, this change might have unwanted side-effects, because of +incompatibilities between versions of :mod:`simplejson` -- see the +:ref:`backwards-incompatible changes <simplejson-incompatibilities>` section. +If you rely on features added to :mod:`simplejson` after it became Python's +:mod:`json`, you should import :mod:`simplejson` explicitly. ``itercompat.product`` ~~~~~~~~~~~~~~~~~~~~~~ The :func:`~django.utils.itercompat.product` function has been deprecated. Use the built-in :func:`itertools.product` instead. + +``django.utils.encoding.StrAndUnicode`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. +Define a ``__str__`` method and apply the +:func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index e38ab646d6..fa55a4d206 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -27,6 +27,8 @@ Final releases .. toctree:: :maxdepth: 1 + .. 1.4.2 (uncomment on release) + 1.4.1 1.4 1.3 release @@ -34,6 +36,7 @@ Final releases .. toctree:: :maxdepth: 1 + 1.3.2 1.3.1 1.3 diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 307691bd4a..f8dbbb65a6 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -460,7 +460,7 @@ algorithm. Increasing the work factor ~~~~~~~~~~~~~~~~~~~~~~~~~~ -The PDKDF2 and bcrypt algorithms use a number of iterations or rounds of +The PBKDF2 and bcrypt algorithms use a number of iterations or rounds of hashing. This deliberately slows down attackers, making attacks against hashed passwords harder. However, as computing power increases, the number of iterations needs to be increased. We've chosen a reasonable default (and will @@ -468,7 +468,7 @@ increase it with each release of Django), but you may wish to tune it up or down, depending on your security needs and available processing power. To do so, you'll subclass the appropriate algorithm and override the ``iterations`` parameters. For example, to increase the number of iterations used by the -default PDKDF2 algorithm: +default PBKDF2 algorithm: 1. Create a subclass of ``django.contrib.auth.hashers.PBKDF2PasswordHasher``:: @@ -1170,24 +1170,25 @@ includes a few other useful built-in views located in :file:`registration/password_reset_form.html` if not supplied. * ``email_template_name``: The full name of a template to use for - generating the email with the new password. Defaults to + generating the email with the reset password link. Defaults to :file:`registration/password_reset_email.html` if not supplied. * ``subject_template_name``: The full name of a template to use for - the subject of the email with the new password. Defaults + the subject of the email with the reset password link. Defaults to :file:`registration/password_reset_subject.txt` if not supplied. .. versionadded:: 1.4 - * ``password_reset_form``: Form that will be used to set the password. - Defaults to :class:`~django.contrib.auth.forms.PasswordResetForm`. + * ``password_reset_form``: Form that will be used to get the email of + the user to reset the password for. Defaults to + :class:`~django.contrib.auth.forms.PasswordResetForm`. - * ``token_generator``: Instance of the class to check the password. This - will default to ``default_token_generator``, it's an instance of + * ``token_generator``: Instance of the class to check the one time link. + This will default to ``default_token_generator``, it's an instance of ``django.contrib.auth.tokens.PasswordResetTokenGenerator``. * ``post_reset_redirect``: The URL to redirect to after a successful - password change. + password reset request. * ``from_email``: A valid email address. By default Django uses the :setting:`DEFAULT_FROM_EMAIL`. @@ -1218,7 +1219,7 @@ includes a few other useful built-in views located in * ``uid``: The user's id encoded in base 36. - * ``token``: Token to check that the password is valid. + * ``token``: Token to check that the reset link is valid. Sample ``registration/password_reset_email.html`` (email body template): diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 219b6c7795..f13238e342 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -864,7 +864,7 @@ key version to provide a final cache key. By default, the three parts are joined using colons to produce a final string:: def make_key(key, key_prefix, version): - return ':'.join([key_prefix, str(version), smart_bytes(key)]) + return ':'.join([key_prefix, str(version), key]) If you want to combine the parts in different ways, or apply other processing to the final key (e.g., taking a hash digest of the key diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 1ac70e6938..6637bd5fcb 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -11,8 +11,7 @@ to structure your views and reuse code by harnessing inheritance and mixins. There are also some generic views for simple tasks which we'll get to later, but you may want to design your own structure of reusable views which suits your use case. For full details, see the -:doc:`class-based views reference -documentation</ref/class-based-views/index>`. +:doc:`class-based views reference documentation</ref/class-based-views/index>`. .. toctree:: :maxdepth: 1 @@ -32,41 +31,12 @@ redirect, and :class:`~django.views.generic.base.TemplateView` extends the base to make it also render a template. -Simple usage -============ - -Class-based generic views (and any class-based views that inherit from -the base classes Django provides) can be configured in two -ways: subclassing, or passing in arguments directly in the URLconf. - -When you subclass a class-based view, you can override attributes -(such as the ``template_name``) or methods (such as ``get_context_data``) -in your subclass to provide new values or methods. Consider, for example, -a view that just displays one template, ``about.html``. Django has a -generic view to do this - :class:`~django.views.generic.base.TemplateView` - -so we can just subclass it, and override the template name:: - - # some_app/views.py - from django.views.generic import TemplateView - - class AboutView(TemplateView): - template_name = "about.html" - -Then, we just need to add this new view into our URLconf. As the class-based -views themselves are classes, we point the URL to the ``as_view`` class method -instead, which is the entry point for class-based views:: - - # urls.py - from django.conf.urls import patterns, url, include - from some_app.views import AboutView - - urlpatterns = patterns('', - (r'^about/', AboutView.as_view()), - ) +Simple usage in your URLconf +============================ -Alternatively, if you're only changing a few simple attributes on a -class-based view, you can simply pass the new attributes into the ``as_view`` -method call itself:: +The simplest way to use generic views is to create them directly in your +URLconf. If you're only changing a few simple attributes on a class-based view, +you can simply pass them into the ``as_view`` method call itself:: from django.conf.urls import patterns, url, include from django.views.generic import TemplateView @@ -75,93 +45,41 @@ method call itself:: (r'^about/', TemplateView.as_view(template_name="about.html")), ) +Any arguments given will override the ``template_name`` on the A similar overriding pattern can be used for the ``url`` attribute on :class:`~django.views.generic.base.RedirectView`. -.. _jsonresponsemixin-example: - -More than just HTML -------------------- -Where class based views shine is when you want to do the same thing many times. -Suppose you're writing an API, and every view should return JSON instead of -rendered HTML. +Subclassing generic views +========================= -We can create a mixin class to use in all of our views, handling the -conversion to JSON once. +The second, more powerful way to use generic views is to inherit from an +existing view and override attributes (such as the ``template_name``) or +methods (such as ``get_context_data``) in your subclass to provide new values +or methods. Consider, for example, a view that just displays one template, +``about.html``. Django has a generic view to do this - +:class:`~django.views.generic.base.TemplateView` - so we can just subclass it, +and override the template name:: -For example, a simple JSON mixin might look something like this:: - - import json - from django.http import HttpResponse - - class JSONResponseMixin(object): - """ - A mixin that can be used to render a JSON response. - """ - response_class = HttpResponse - - def render_to_response(self, context, **response_kwargs): - """ - Returns a JSON response, transforming 'context' to make the payload. - """ - response_kwargs['content_type'] = 'application/json' - return self.response_class( - self.convert_context_to_json(context), - **response_kwargs - ) - - def convert_context_to_json(self, context): - "Convert the context dictionary into a JSON object" - # Note: This is *EXTREMELY* naive; in reality, you'll need - # to do much more complex handling to ensure that arbitrary - # objects -- such as Django model instances or querysets - # -- can be serialized as JSON. - return json.dumps(context) - -Now we mix this into the base view:: - - from django.views.generic import View - - class JSONView(JSONResponseMixin, View): - pass - -Equally we could use our mixin with one of the generic views. We can make our -own version of :class:`~django.views.generic.detail.DetailView` by mixing -:class:`JSONResponseMixin` with the -:class:`~django.views.generic.detail.BaseDetailView` -- (the -:class:`~django.views.generic.detail.DetailView` before template -rendering behavior has been mixed in):: + # some_app/views.py + from django.views.generic import TemplateView - class JSONDetailView(JSONResponseMixin, BaseDetailView): - pass + class AboutView(TemplateView): + template_name = "about.html" -This view can then be deployed in the same way as any other -:class:`~django.views.generic.detail.DetailView`, with exactly the -same behavior -- except for the format of the response. +Then we just need to add this new view into our URLconf. +`~django.views.generic.base.TemplateView` is a class, not a function, so we +point the URL to the ``as_view`` class method instead, which provides a +function-like entry to class-based views:: -If you want to be really adventurous, you could even mix a -:class:`~django.views.generic.detail.DetailView` subclass that is able -to return *both* HTML and JSON content, depending on some property of -the HTTP request, such as a query argument or a HTTP header. Just mix -in both the :class:`JSONResponseMixin` and a -:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, -and override the implementation of :func:`render_to_response()` to defer -to the appropriate subclass depending on the type of response that the user -requested:: + # urls.py + from django.conf.urls import patterns, url, include + from some_app.views import AboutView - class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): - def render_to_response(self, context): - # Look for a 'format=json' GET argument - if self.request.GET.get('format','html') == 'json': - return JSONResponseMixin.render_to_response(self, context) - else: - return SingleObjectTemplateResponseMixin.render_to_response(self, context) + urlpatterns = patterns('', + (r'^about/', AboutView.as_view()), + ) -Because of the way that Python resolves method overloading, the local -``render_to_response()`` implementation will override the versions provided by -:class:`JSONResponseMixin` and -:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. For more information on how to use the built in generic views, consult the next topic on :doc:`generic class based views</topics/class-based-views/generic-display>`. @@ -171,16 +89,15 @@ Decorating class-based views .. highlightlang:: python -The extension of class-based views isn't limited to using mixins. You -can use also use decorators. +Since class-based views aren't functions, decorating them works differently +depending on if you're using ``as_view`` or creating a subclass. Decorating in URLconf --------------------- The simplest way of decorating class-based views is to decorate the result of the :meth:`~django.views.generic.base.View.as_view` method. -The easiest place to do this is in the URLconf where you deploy your -view:: +The easiest place to do this is in the URLconf where you deploy your view:: from django.contrib.auth.decorators import login_required, permission_required from django.views.generic import TemplateView diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 0c19d60e35..f07769fb8a 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -69,7 +69,7 @@ interface to working with templates in class-based views. add more members to the dictionary. Building up Django's generic class-based views -=============================================== +============================================== Let's look at how two of Django's generic class-based views are built out of mixins providing discrete functionality. We'll consider @@ -222,8 +222,7 @@ we'll want the functionality provided by :class:`~django.views.generic.detail.SingleObjectMixin`. We'll demonstrate this with the publisher modelling we used in the -:doc:`generic class-based views -introduction<generic-display>`. +:doc:`generic class-based views introduction<generic-display>`. .. code-block:: python @@ -233,11 +232,11 @@ introduction<generic-display>`. from django.views.generic import View from django.views.generic.detail import SingleObjectMixin from books.models import Author - + class RecordInterest(View, SingleObjectMixin): """Records the current user's interest in an author.""" model = Author - + def post(self, request, *args, **kwargs): if not request.user.is_authenticated(): return HttpResponseForbidden() @@ -256,9 +255,7 @@ we're interested in, which it just does with a simple call to ``self.get_object()``. Everything else is taken care of for us by the mixin. -We can hook this into our URLs easily enough: - -.. code-block:: python +We can hook this into our URLs easily enough:: # urls.py from books.views import RecordInterest @@ -294,8 +291,6 @@ object. In order to do this, we need to have two different querysets: We'll figure that out ourselves in :meth:`get_queryset()` so we can take into account the Publisher we're looking at. -.. highlightlang:: python - .. note:: We have to think carefully about :meth:`get_context_data()`. @@ -311,15 +306,15 @@ Now we can write a new :class:`PublisherDetail`:: from django.views.generic import ListView from django.views.generic.detail import SingleObjectMixin from books.models import Publisher - + class PublisherDetail(SingleObjectMixin, ListView): paginate_by = 2 template_name = "books/publisher_detail.html" - + def get_context_data(self, **kwargs): kwargs['publisher'] = self.object return super(PublisherDetail, self).get_context_data(**kwargs) - + def get_queryset(self): self.object = self.get_object(Publisher.objects.all()) return self.object.book_set.all() @@ -339,26 +334,26 @@ have to create lots of books to see the pagination working! Here's the template you'd want to use:: {% extends "base.html" %} - + {% block content %} <h2>Publisher {{ publisher.name }}</h2> - + <ol> {% for book in page_obj %} <li>{{ book.title }}</li> {% endfor %} </ol> - + <div class="pagination"> <span class="step-links"> {% if page_obj.has_previous %} <a href="?page={{ page_obj.previous_page_number }}">previous</a> {% endif %} - + <span class="current"> Page {{ page_obj.number }} of {{ paginator.num_pages }}. </span> - + {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number }}">next</a> {% endif %} @@ -428,9 +423,9 @@ code so that on ``POST`` the form gets called appropriately. the views implement :meth:`get()`, and things would get much more confusing. -Our new :class:`AuthorDetail` looks like this: +.. highlightlang:: python -.. code-block:: python +Our new :class:`AuthorDetail` looks like this:: # CAUTION: you almost certainly do not want to do this. # It is provided as part of a discussion of problems you can @@ -455,7 +450,7 @@ Our new :class:`AuthorDetail` looks like this: 'author-detail', kwargs = {'pk': self.object.pk}, ) - + def get_context_data(self, **kwargs): form_class = self.get_form_class() form = self.get_form(form_class) @@ -464,7 +459,7 @@ Our new :class:`AuthorDetail` looks like this: } context.update(kwargs) return super(AuthorDetail, self).get_context_data(**context) - + def post(self, request, *args, **kwargs): form_class = self.get_form_class() form = self.get_form(form_class) @@ -472,14 +467,14 @@ Our new :class:`AuthorDetail` looks like this: return self.form_valid(form) else: return self.form_invalid(form) - + def form_valid(self, form): if not self.request.user.is_authenticated(): return HttpResponseForbidden() self.object = self.get_object() # record the interest using the message in form.cleaned_data return super(AuthorDetail, self).form_valid(form) - + :meth:`get_success_url()` is just providing somewhere to redirect to, which gets used in the default implementation of :meth:`form_valid()`. We have to provide our own :meth:`post()` as @@ -525,21 +520,21 @@ write our own :meth:`get_context_data()` to make the from django.views.generic import DetailView from django import forms from books.models import Author - + class AuthorInterestForm(forms.Form): message = forms.CharField() - + class AuthorDisplay(DetailView): - + queryset = Author.objects.all() - + def get_context_data(self, **kwargs): context = { 'form': AuthorInterestForm(), } context.update(kwargs) return super(AuthorDisplay, self).get_context_data(**context) - + Then the :class:`AuthorInterest` is a simple :class:`FormView`, but we have to bring in :class:`SingleObjectMixin` so we can find the author we're talking about, and we have to remember to set @@ -550,7 +545,7 @@ template as :class:`AuthorDisplay` is using on ``GET``. from django.views.generic import FormView from django.views.generic.detail import SingleObjectMixin - + class AuthorInterest(FormView, SingleObjectMixin): template_name = 'books/author_detail.html' form_class = AuthorInterestForm @@ -561,13 +556,13 @@ template as :class:`AuthorDisplay` is using on ``GET``. 'object': self.get_object(), } return super(AuthorInterest, self).get_context_data(**context) - + def get_success_url(self): return reverse( 'author-detail', kwargs = {'pk': self.object.pk}, ) - + def form_valid(self, form): if not self.request.user.is_authenticated(): return HttpResponseForbidden() @@ -588,13 +583,13 @@ using a different template. .. code-block:: python from django.views.generic import View - + class AuthorDetail(View): - + def get(self, request, *args, **kwargs): view = AuthorDisplay.as_view() return view(request, *args, **kwargs) - + def post(self, request, *args, **kwargs): view = AuthorInterest.as_view() return view(request, *args, **kwargs) @@ -603,3 +598,88 @@ This approach can also be used with any other generic class-based views or your own class-based views inheriting directly from :class:`View` or :class:`TemplateView`, as it keeps the different views as separate as possible. + +.. _jsonresponsemixin-example: + +More than just HTML +=================== + +Where class based views shine is when you want to do the same thing many times. +Suppose you're writing an API, and every view should return JSON instead of +rendered HTML. + +We can create a mixin class to use in all of our views, handling the +conversion to JSON once. + +For example, a simple JSON mixin might look something like this:: + + import json + from django.http import HttpResponse + + class JSONResponseMixin(object): + """ + A mixin that can be used to render a JSON response. + """ + response_class = HttpResponse + + def render_to_response(self, context, **response_kwargs): + """ + Returns a JSON response, transforming 'context' to make the payload. + """ + response_kwargs['content_type'] = 'application/json' + return self.response_class( + self.convert_context_to_json(context), + **response_kwargs + ) + + def convert_context_to_json(self, context): + "Convert the context dictionary into a JSON object" + # Note: This is *EXTREMELY* naive; in reality, you'll need + # to do much more complex handling to ensure that arbitrary + # objects -- such as Django model instances or querysets + # -- can be serialized as JSON. + return json.dumps(context) + +Now we mix this into the base TemplateView:: + + from django.views.generic import TemplateView + + class JSONView(JSONResponseMixin, TemplateView): + pass + +Equally we could use our mixin with one of the generic views. We can make our +own version of :class:`~django.views.generic.detail.DetailView` by mixing +:class:`JSONResponseMixin` with the +:class:`~django.views.generic.detail.BaseDetailView` -- (the +:class:`~django.views.generic.detail.DetailView` before template +rendering behavior has been mixed in):: + + class JSONDetailView(JSONResponseMixin, BaseDetailView): + pass + +This view can then be deployed in the same way as any other +:class:`~django.views.generic.detail.DetailView`, with exactly the +same behavior -- except for the format of the response. + +If you want to be really adventurous, you could even mix a +:class:`~django.views.generic.detail.DetailView` subclass that is able +to return *both* HTML and JSON content, depending on some property of +the HTTP request, such as a query argument or a HTTP header. Just mix +in both the :class:`JSONResponseMixin` and a +:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, +and override the implementation of :func:`render_to_response()` to defer +to the appropriate subclass depending on the type of response that the user +requested:: + + class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): + def render_to_response(self, context): + # Look for a 'format=json' GET argument + if self.request.GET.get('format','html') == 'json': + return JSONResponseMixin.render_to_response(self, context) + else: + return SingleObjectTemplateResponseMixin.render_to_response(self, context) + +Because of the way that Python resolves method overloading, the local +``render_to_response()`` implementation will override the versions provided by +:class:`JSONResponseMixin` and +:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index ce15dc9535..f29cc28332 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -108,8 +108,8 @@ determine a few things: * The database column type (e.g. ``INTEGER``, ``VARCHAR``). -* The :doc:`widget </ref/forms/widgets>` to use in Django's admin interface, - if you care to use it (e.g. ``<input type="text">``, ``<select>``). +* The default :doc:`widget </ref/forms/widgets>` to use when rendering a form + field (e.g. ``<input type="text">``, ``<select>``). * The minimal validation requirements, used in Django's admin and in automatically-generated forms. @@ -143,13 +143,13 @@ ones: 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 - :attr:`blank=True <Field.blank>`, validation on Django's admin site will + :attr:`blank=True <Field.blank>`, form validation will allow entry of an empty value. If a field has :attr:`blank=False <Field.blank>`, the field will be required. :attr:`~Field.choices` An iterable (e.g., a list or tuple) of 2-tuples to use as choices for - this field. If this is given, Django's admin will use a select box + this field. If this is given, the default form widget will be a select box instead of the standard text field and will limit choices to the choices given. @@ -164,7 +164,7 @@ ones: ) The first element in each tuple is the value that will be stored in the - database, the second element will be displayed by the admin interface, + database, the second element will be displayed by the default form widget or in a ModelChoiceField. Given an instance of a model object, the display value for a choices field can be accessed using the ``get_FOO_display`` method. For example:: @@ -195,9 +195,8 @@ ones: created. :attr:`~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. :attr:`~Field.primary_key` If ``True``, this field is the primary key for the model. @@ -360,13 +359,12 @@ It doesn't matter which model has the :class:`~django.db.models.ManyToManyField`, but you should only put it in one of the models -- not both. -Generally, :class:`~django.db.models.ManyToManyField` instances should go in the -object that's going to be edited in the admin interface, if you're using -Django's admin. In the above example, ``toppings`` is in ``Pizza`` (rather than -``Topping`` having a ``pizzas`` :class:`~django.db.models.ManyToManyField` ) -because it's more natural to think about a pizza having toppings than a -topping being on multiple pizzas. The way it's set up above, the ``Pizza`` admin -form would let users select the toppings. +Generally, :class:`~django.db.models.ManyToManyField` instances should go in +the object that's going to be edited on a form. In the above example, +``toppings`` is in ``Pizza`` (rather than ``Topping`` having a ``pizzas`` +:class:`~django.db.models.ManyToManyField` ) because it's more natural to think +about a pizza having toppings than a topping being on multiple pizzas. The way +it's set up above, the ``Pizza`` form would let users select the toppings. .. seealso:: diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 86e785a19d..03a7d3b7cd 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -525,7 +525,7 @@ registered with any ``Admin`` instance:: admin.site.register(Author, MultiDBModelAdmin) admin.site.register(Publisher, PublisherAdmin) - othersite = admin.Site('othersite') + othersite = admin.AdminSite('othersite') othersite.register(Publisher, MultiDBModelAdmin) This example sets up two admin sites. On the first site, the diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index b4d4eb1062..60437c1120 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -900,10 +900,10 @@ possible to easily create new instance with all fields' values copied. In the simplest case, you can just set ``pk`` to ``None``. Using our blog example:: blog = Blog(name='My blog', tagline='Blogging is easy') - blog.save() # post.pk == 1 + blog.save() # blog.pk == 1 blog.pk = None - blog.save() # post.pk == 2 + blog.save() # blog.pk == 2 Things get more complicated if you use inheritance. Consider a subclass of ``Blog``:: diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt index 4399e7572c..615dd7193c 100644 --- a/docs/topics/forms/media.txt +++ b/docs/topics/forms/media.txt @@ -65,9 +65,9 @@ through this property:: >>> w = CalendarWidget() >>> print(w.media) - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/animations.js"></script> - <script type="text/javascript" src="http://media.example.com/actions.js"></script> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/animations.js"></script> + <script type="text/javascript" src="http://static.example.com/actions.js"></script> Here's a list of all possible ``Media`` options. There are no required options. @@ -110,9 +110,9 @@ requirements:: If this last CSS definition were to be rendered, it would become the following HTML:: - <link href="http://media.example.com/pretty.css" type="text/css" media="screen" rel="stylesheet" /> - <link href="http://media.example.com/lo_res.css" type="text/css" media="tv,projector" rel="stylesheet" /> - <link href="http://media.example.com/newspaper.css" type="text/css" media="print" rel="stylesheet" /> + <link href="http://static.example.com/pretty.css" type="text/css" media="screen" rel="stylesheet" /> + <link href="http://static.example.com/lo_res.css" type="text/css" media="tv,projector" rel="stylesheet" /> + <link href="http://static.example.com/newspaper.css" type="text/css" media="print" rel="stylesheet" /> ``js`` ~~~~~~ @@ -140,11 +140,11 @@ basic Calendar widget from the example above:: >>> w = FancyCalendarWidget() >>> print(w.media) - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> - <link href="http://media.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/animations.js"></script> - <script type="text/javascript" src="http://media.example.com/actions.js"></script> - <script type="text/javascript" src="http://media.example.com/whizbang.js"></script> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <link href="http://static.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/animations.js"></script> + <script type="text/javascript" src="http://static.example.com/actions.js"></script> + <script type="text/javascript" src="http://static.example.com/whizbang.js"></script> The FancyCalendar widget inherits all the media from it's parent widget. If you don't want media to be inherited in this way, add an ``extend=False`` @@ -160,8 +160,8 @@ declaration to the media declaration:: >>> w = FancyCalendarWidget() >>> print(w.media) - <link href="http://media.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/whizbang.js"></script> + <link href="http://static.example.com/fancy.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/whizbang.js"></script> If you require even more control over media inheritance, define your media using a `dynamic property`_. Dynamic properties give you complete control over @@ -253,12 +253,12 @@ to filter out a medium of interest. For example:: >>> w = CalendarWidget() >>> print(w.media) - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/animations.js"></script> - <script type="text/javascript" src="http://media.example.com/actions.js"></script> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/animations.js"></script> + <script type="text/javascript" src="http://static.example.com/actions.js"></script> >>> print(w.media)['css'] - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> When you use the subscript operator, the value that is returned is a new Media object -- but one that only contains the media of interest. @@ -283,10 +283,10 @@ the resulting Media object contains the union of the media from both files:: >>> w1 = CalendarWidget() >>> w2 = OtherWidget() >>> print(w1.media + w2.media) - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/animations.js"></script> - <script type="text/javascript" src="http://media.example.com/actions.js"></script> - <script type="text/javascript" src="http://media.example.com/whizbang.js"></script> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/animations.js"></script> + <script type="text/javascript" src="http://static.example.com/actions.js"></script> + <script type="text/javascript" src="http://static.example.com/whizbang.js"></script> Media on Forms -------------- @@ -306,10 +306,10 @@ of adding the media definitions for all widgets that are part of the form:: >>> f = ContactForm() >>> f.media - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/animations.js"></script> - <script type="text/javascript" src="http://media.example.com/actions.js"></script> - <script type="text/javascript" src="http://media.example.com/whizbang.js"></script> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/animations.js"></script> + <script type="text/javascript" src="http://static.example.com/actions.js"></script> + <script type="text/javascript" src="http://static.example.com/whizbang.js"></script> If you want to associate additional media with a form -- for example, CSS for form layout -- simply add a media declaration to the form:: @@ -325,8 +325,8 @@ layout -- simply add a media declaration to the form:: >>> f = ContactForm() >>> f.media - <link href="http://media.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> - <link href="http://media.example.com/layout.css" type="text/css" media="all" rel="stylesheet" /> - <script type="text/javascript" src="http://media.example.com/animations.js"></script> - <script type="text/javascript" src="http://media.example.com/actions.js"></script> - <script type="text/javascript" src="http://media.example.com/whizbang.js"></script> + <link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet" /> + <link href="http://static.example.com/layout.css" type="text/css" media="all" rel="stylesheet" /> + <script type="text/javascript" src="http://static.example.com/animations.js"></script> + <script type="text/javascript" src="http://static.example.com/actions.js"></script> + <script type="text/javascript" src="http://static.example.com/whizbang.js"></script> diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 0ca37745c7..8159c8850c 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -200,7 +200,9 @@ The first time you call ``is_valid()`` or access the ``errors`` attribute of a well as :ref:`model validation <validating-objects>`. This has the side-effect of cleaning the model you pass to the ``ModelForm`` constructor. For instance, calling ``is_valid()`` on your form will convert any date fields on your model -to actual date objects. +to actual date objects. If form validation fails, only some of the updates +may be applied. For this reason, you'll probably want to avoid reusing the +model instance. The ``save()`` method diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 877d3b4100..b3a830c25e 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -178,6 +178,52 @@ Three settings control Django's file upload behavior: Which means "try to upload to memory first, then fall back to temporary files." +Handling uploaded files with a model +------------------------------------ + +If you're saving a file on a :class:`~django.db.models.Model` with a +:class:`~django.db.models.FileField`, using a :class:`~django.forms.ModelForm` +makes this process much easier. The file object will be saved to the location +specified by the :attr:`~django.db.models.FileField.upload_to` argument of the +corresponding :class:`~django.db.models.FileField` when calling +``form.save()``:: + + from django.http import HttpResponseRedirect + from django.shortcuts import render + from .forms import ModelFormWithFileField + + def upload_file(request): + if request.method == 'POST': + form = ModelFormWithFileField(request.POST, request.FILES) + if form.is_valid(): + # file is saved + form.save() + return HttpResponseRedirect('/success/url/') + else: + form = ModelFormWithFileField() + return render('upload.html', {'form': form}) + +If you are constructing an object manually, you can simply assign the file +object from :attr:`request.FILES <django.http.HttpRequest.FILES>` to the file +field in the model:: + + from django.http import HttpResponseRedirect + from django.shortcuts import render + from .forms import UploadFileForm + from .models import ModelWithFileField + + def upload_file(request): + if request.method == 'POST': + form = UploadFileForm(request.POST, request.FILES) + if form.is_valid(): + instance = ModelWithFileField(file_field=request.FILES['file']) + instance.save() + return HttpResponseRedirect('/success/url/') + else: + form = UploadFileForm() + return render('upload.html', {'form': form}) + + ``UploadedFile`` objects ======================== diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 4e75dfe55f..7297184ed3 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -314,6 +314,9 @@ that should be called if none of the URL patterns match. By default, this is ``'django.views.defaults.page_not_found'``. That default value should suffice. +See the documentation about :ref:`the 404 (HTTP Not Found) view +<http_not_found_view>` for more information. + handler500 ---------- @@ -326,6 +329,9 @@ have runtime errors in view code. By default, this is ``'django.views.defaults.server_error'``. That default value should suffice. +See the documentation about :ref:`the 500 (HTTP Internal Server Error) view +<http_internal_server_error_view>` for more information. + Notes on capturing text in URLs =============================== @@ -568,10 +574,8 @@ For example:: (r'^blog/(?P<year>\d{4})/$', 'year_archive', {'foo': 'bar'}), ) -In this example, for a request to ``/blog/2005/``, Django will call the -``blog.views.year_archive()`` view, passing it these keyword arguments:: - - year='2005', foo='bar' +In this example, for a request to ``/blog/2005/``, Django will call +``blog.views.year_archive(year='2005', foo='bar')``. This technique is used in the :doc:`syndication framework </ref/contrib/syndication>` to pass metadata and diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index a8c85ddf6e..c4bd15e72e 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -127,6 +127,8 @@ called ``404.html`` and located in the top level of your template tree. Customizing error views ======================= +.. _http_not_found_view: + The 404 (page not found) view ----------------------------- @@ -167,6 +169,8 @@ Four things to note about 404 views: your 404 view will never be used, and your URLconf will be displayed instead, with some debug information. +.. _http_internal_server_error_view: + The 500 (server error) view ---------------------------- diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index bdbb04823d..9bd53da2b9 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -37,6 +37,13 @@ from your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting. controls if Django should implement format localization. See :doc:`/topics/i18n/formatting` for more details. +.. note:: + + Make sure you've activated translation for your project (the fastest way is + to check if :setting:`MIDDLEWARE_CLASSES` includes + :mod:`django.middleware.locale.LocaleMiddleware`). If you haven't yet, + see :ref:`how-django-discovers-language-preference`. + Internationalization: in Python code ==================================== @@ -317,10 +324,10 @@ Model fields and relationships ``verbose_name`` and ``help_text`` option values For example, to translate the help text of the *name* field in the following model, do the following:: - from django.utils.translation import ugettext_lazy + from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): - name = models.CharField(help_text=ugettext_lazy('This is the help text')) + name = models.CharField(help_text=_('This is the help text')) You can mark names of ``ForeignKey``, ``ManyTomanyField`` or ``OneToOneField`` relationship as translatable by using their ``verbose_name`` options:: @@ -344,14 +351,14 @@ It is recommended to always provide explicit relying on the fallback English-centric and somewhat naïve determination of verbose names Django performs bu looking at the model's class name:: - from django.utils.translation import ugettext_lazy + from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): - name = models.CharField(_('name'), help_text=ugettext_lazy('This is the help text')) + name = models.CharField(_('name'), help_text=_('This is the help text')) class Meta: - verbose_name = ugettext_lazy('my thing') - verbose_name_plural = ugettext_lazy('my things') + verbose_name = _('my thing') + verbose_name_plural = _('my things') Model methods ``short_description`` attribute values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -456,6 +463,9 @@ Internationalization: in template code Translations in :doc:`Django templates </topics/templates>` uses two template tags and a slightly different syntax than in Python code. To give your template access to these tags, put ``{% load i18n %}`` toward the top of your template. +As with all template tags, this tag needs to be loaded in all templates which +use translations, even those templates that extend from other templates which +have already loaded the ``i18n`` tag. .. templatetag:: trans diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index a878d42266..b54a9475ae 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -218,10 +218,11 @@ handlers, filters and formatters that you want in your logging setup, and the log levels and other properties that you want those components to have. -Logging is configured immediately after settings have been loaded. -Since the loading of settings is one of the first things that Django -does, you can be certain that loggers are always ready for use in your -project code. +Logging is configured as soon as settings have been loaded +(either manually using :func:`~django.conf.settings.configure` or when at least +one setting is accessed). Since the loading of settings is one of the first +things that Django does, you can be certain that loggers are always ready for +use in your project code. .. _dictConfig format: http://docs.python.org/library/logging.config.html#configuration-dictionary-schema @@ -515,6 +516,35 @@ logging module. through the filter. Handling of that record will not proceed if the callback returns False. + For instance, to filter out :class:`~django.http.UnreadablePostError` + (raised when a user cancels an upload) from the admin emails, you would + create a filter function:: + + from django.http import UnreadablePostError + + def skip_unreadable_post(record): + if record.exc_info: + exc_type, exc_value = record.exc_info[:2] + if isinstance(exc_value, UnreadablePostError): + return False + return True + + and then add it to your logging config:: + + 'filters': { + 'skip_unreadable_posts': { + '()': 'django.utils.log.CallbackFilter', + 'callback': skip_unreadable_post, + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['skip_unreadable_posts'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + .. class:: RequireDebugFalse() .. versionadded:: 1.4 diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index b09c1d2347..457486caa4 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -1,25 +1,213 @@ -====================== -Python 3 compatibility -====================== +=================== +Porting to Python 3 +=================== Django 1.5 is the first version of Django to support Python 3. The same code runs both on Python 2 (≥ 2.6.5) and Python 3 (≥ 3.2), thanks to the six_ -compatibility layer and ``unicode_literals``. +compatibility layer. .. _six: http://packages.python.org/six/ -This document is not meant as a Python 2 to Python 3 migration guide. There -are many existing resources, including `Python's official porting guide`_. -Rather, it describes guidelines that apply to Django's code and are -recommended for pluggable apps that run with both Python 2 and 3. +This document is primarily targeted at authors of pluggable application +who want to support both Python 2 and 3. It also describes guidelines that +apply to Django's code. + +Philosophy +========== + +This document assumes that you are familiar with the changes between Python 2 +and Python 3. If you aren't, read `Python's official porting guide`_ first. +Refreshing your knowledge of unicode handling on Python 2 and 3 will help; the +`Pragmatic Unicode`_ presentation is a good resource. + +Django uses the *Python 2/3 Compatible Source* strategy. Of course, you're +free to chose another strategy for your own code, especially if you don't need +to stay compatible with Python 2. But authors of pluggable applications are +encouraged to use the same porting strategy as Django itself. + +Writing compatible code is much easier if you target Python ≥ 2.6. You will +most likely take advantage of the compatibility functions introduced in Django +1.5, like :mod:`django.utils.six`, so your application will also require +Django ≥ 1.5. + +Obviously, writing compatible source code adds some overhead, and that can +cause frustration. Django's developers have found that attempting to write +Python 3 code that's compatible with Python 2 is much more rewarding than the +opposite. Not only does that make your code more future-proof, but Python 3's +advantages (like the saner string handling) start shining quickly. Dealing +with Python 2 becomes a backwards compatibility requirement, and we as +developers are used to dealing with such constraints. + +Porting tools provided by Django are inspired by this philosophy, and it's +reflected throughout this guide. .. _Python's official porting guide: http://docs.python.org/py3k/howto/pyporting.html +.. _Pragmatic Unicode: http://nedbatchelder.com/text/unipain.html + +Porting tips +============ + +Unicode literals +---------------- + +This step consists in: + +- Adding ``from __future__ import unicode_literals`` at the top of your Python + modules -- it's best to put it in each and every module, otherwise you'll + keep checking the top of your files to see which mode is in effect; +- Removing the ``u`` prefix before unicode strings; +- Adding a ``b`` prefix before bytestrings. + +Performing these changes systematically guarantees backwards compatibility. + +However, Django applications generally don't need bytestrings, since Django +only exposes unicode interfaces to the programmer. Python 3 discourages using +bytestrings, except for binary data or byte-oriented interfaces. Python 2 +makes bytestrings and unicode strings effectively interchangeable, as long as +they only contain ASCII data. Take advantage of this to use unicode strings +wherever possible and avoid the ``b`` prefixes. + +.. note:: + + Python 2's ``u`` prefix is a syntax error in Python 3.2 but it will be + allowed again in Python 3.3 thanks to :pep:`414`. Thus, this + transformation is optional if you target Python ≥ 3.3. It's still + recommended, per the "write Python 3 code" philosophy. + +String handling +--------------- + +Python 2's :class:`unicode` type was renamed :class:`str` in Python 3, +:class:`str` was renamed :class:`bytes`, and :class:`basestring` disappeared. +six_ provides :ref:`tools <string-handling-with-six>` to deal with these +changes. + +Django also contains several string related classes and functions in the +:mod:`django.utils.encoding` and :mod:`django.utils.safestring` modules. Their +names used the words ``str``, which doesn't mean the same thing in Python 2 +and Python 3, and ``unicode``, which doesn't exist in Python 3. In order to +avoid ambiguity and confusion these concepts were renamed ``bytes`` and +``text``. + +Here are the name changes in :mod:`django.utils.encoding`: + +================== ================== +Old name New name +================== ================== +``smart_str`` ``smart_bytes`` +``smart_unicode`` ``smart_text`` +``force_unicode`` ``force_text`` +================== ================== + +For backwards compatibility, the old names still work on Python 2. Under +Python 3, ``smart_str`` is an alias for ``smart_text``. + +.. note:: + + :mod:`django.utils.encoding` was deeply refactored in Django 1.5 to + provide a more consistent API. Check its documentation for more + information. + +:mod:`django.utils.safestring` is mostly used via the +:func:`~django.utils.safestring.mark_safe` and +:func:`~django.utils.safestring.mark_for_escaping` functions, which didn't +change. In case you're using the internals, here are the name changes: + +================== ================== +Old name New name +================== ================== +``EscapeString`` ``EscapeBytes`` +``EscapeUnicode`` ``EscapeText`` +``SafeString`` ``SafeBytes`` +``SafeUnicode`` ``SafeText`` +================== ================== + +For backwards compatibility, the old names still work on Python 2. Under +Python 3, ``EscapeString`` and ``SafeString`` are aliases for ``EscapeText`` +and ``SafeText`` respectively. + +:meth:`__str__` and :meth:`__unicode__` methods +----------------------------------------------- + +In Python 2, the object model specifies :meth:`__str__` and +:meth:`__unicode__` methods. If these methods exist, they must return +:class:`str` (bytes) and :class:`unicode` (text) respectively. + +The ``print`` statement and the :func:`str` built-in call :meth:`__str__` to +determine the human-readable representation of an object. The :func:`unicode` +built-in calls :meth:`__unicode__` if it exists, and otherwise falls back to +:meth:`__str__` and decodes the result with the system encoding. Conversely, +the :class:`~django.db.models.Model` base class automatically derives +:meth:`__str__` from :meth:`__unicode__` by encoding to UTF-8. + +In Python 3, there's simply :meth:`__str__`, which must return :class:`str` +(text). + +(It is also possible to define :meth:`__bytes__`, but Django application have +little use for that method, because they hardly ever deal with +:class:`bytes`.) + +Django provides a simple way to define :meth:`__str__` and :meth:`__unicode__` +methods that work on Python 2 and 3: you must define a :meth:`__str__` method +returning text and to apply the +:func:`~django.utils.encoding.python_2_unicode_compatible` decorator. + +On Python 3, the decorator is a no-op. On Python 2, it defines appropriate +:meth:`__unicode__` and :meth:`__str__` methods (replacing the original +:meth:`__str__` method in the process). Here's an example:: + + from __future__ import unicode_literals + from django.utils.encoding import python_2_unicode_compatible + + @python_2_unicode_compatible + class MyClass(object): + def __str__(self): + return "Instance of my class" + +This technique is the best match for Django's porting philosophy. + +Finally, note that :meth:`__repr__` must return a :class:`str` on all versions +of Python. + +:class:`dict` and :class:`dict`-like classes +-------------------------------------------- + +:meth:`dict.keys`, :meth:`dict.items` and :meth:`dict.values` return lists in +Python 2 and iterators in Python 3. :class:`~django.http.QueryDict` and the +:class:`dict`-like classes defined in :mod:`django.utils.datastructures` +behave likewise in Python 3. + +six_ provides compatibility functions to work around this change: +:func:`~six.iterkeys`, :func:`~six.iteritems`, and :func:`~six.itervalues`. +Django's bundled version adds :func:`~django.utils.six.iterlists` for +:class:`~django.utils.datastructures.MultiValueDict` and its subclasses. + +:class:`~django.http.HttpRequest` and :class:`~django.http.HttpResponse` objects +-------------------------------------------------------------------------------- + +According to :pep:`3333`: + +- headers are always :class:`str` objects, +- input and output streams are always :class:`bytes` objects. + +Specifically, :attr:`HttpResponse.content <django.http.HttpResponse.content>` +contains :class:`bytes`, which may become an issue if you compare it with a +:class:`str` in your tests. The preferred solution is to rely on +:meth:`~django.test.TestCase.assertContains` and +:meth:`~django.test.TestCase.assertNotContains`. These methods accept a +response and a unicode string as arguments. + +Coding guidelines +================= + +The following guidelines are enforced in Django's source code. They're also +recommended for third-party application who follow the same porting strategy. Syntax requirements -=================== +------------------- Unicode -------- +~~~~~~~ In Python 3, all strings are considered Unicode by default. The ``unicode`` type from Python 2 is called ``str`` in Python 3, and ``str`` becomes @@ -36,17 +224,25 @@ In order to enable the same behavior in Python 2, every module must import my_string = "This is an unicode literal" my_bytestring = b"This is a bytestring" -If you need a byte string under Python 2 and a unicode string under Python 3, -use the :func:`str` builtin:: +If you need a byte string literal under Python 2 and a unicode string literal +under Python 3, use the :func:`str` builtin:: str('my string') -Be cautious if you have to `slice bytestrings`_. +In Python 3, there aren't any automatic conversions between :class:`str` and +:class:`bytes`, and the :mod:`codecs` module became more strict. +:meth:`str.decode` always returns :class:`bytes`, and :meth:`bytes.decode` +always returns :class:`str`. As a consequence, the following pattern is +sometimes necessary:: + + value = value.encode('ascii', 'ignore').decode('ascii') + +Be cautious if you have to `index bytestrings`_. -.. _slice bytestrings: http://docs.python.org/py3k/howto/pyporting.html#bytes-literals +.. _index bytestrings: http://docs.python.org/py3k/howto/pyporting.html#bytes-literals Exceptions ----------- +~~~~~~~~~~ When you capture exceptions, use the ``as`` keyword:: @@ -59,17 +255,64 @@ This older syntax was removed in Python 3:: try: ... - except MyException, exc: + except MyException, exc: # Don't do that! ... The syntax to reraise an exception with a different traceback also changed. Use :func:`six.reraise`. +Magic methods +------------- + +Use the patterns below to handle magic methods renamed in Python 3. + +Iterators +~~~~~~~~~ + +:: + + class MyIterator(object): + def __iter__(self): + return self # implement some logic here + + def __next__(self): + raise StopIteration # implement some logic here + + next = __next__ # Python 2 compatibility + +Boolean evaluation +~~~~~~~~~~~~~~~~~~ + +:: + + class MyBoolean(object): + + def __bool__(self): + return True # implement some logic here + + __nonzero__ = __bool__ # Python 2 compatibility + +Division +~~~~~~~~ + +:: + + class MyDivisible(object): + + def __truediv__(self, other): + return self / other # implement some logic here + + __div__ = __truediv__ # Python 2 compatibility + + def __itruediv__(self, other): + return self // other # implement some logic here + + __idiv__ = __itruediv__ # Python 2 compatibility .. module: django.utils.six Writing compatible code with six -================================ +-------------------------------- six_ is the canonical compatibility library for supporting Python 2 and 3 in a single codebase. Read its documentation! @@ -78,8 +321,10 @@ a single codebase. Read its documentation! Here are the most common changes required to write compatible code. -String types ------------- +.. _string-handling-with-six: + +String handling +~~~~~~~~~~~~~~~ The ``basestring`` and ``unicode`` types were removed in Python 3, and the meaning of ``str`` changed. To test these types, use the following idioms:: @@ -92,7 +337,7 @@ Python ≥ 2.6 provides ``bytes`` as an alias for ``str``, so you don't need :attr:`six.binary_type`. ``long`` --------- +~~~~~~~~ The ``long`` type no longer exists in Python 3. ``1L`` is a syntax error. Use :data:`six.integer_types` check if a value is an integer or a long:: @@ -100,21 +345,27 @@ The ``long`` type no longer exists in Python 3. ``1L`` is a syntax error. Use isinstance(myvalue, six.integer_types) # replacement for (int, long) ``xrange`` ----------- +~~~~~~~~~~ Import :func:`six.moves.xrange` wherever you use ``xrange``. Moved modules -------------- +~~~~~~~~~~~~~ Some modules were renamed in Python 3. The :mod:`django.utils.six.moves <six.moves>` module provides a compatible location to import them. -In addition to six' defaults, Django's version provides ``dummy_thread`` as -``_dummy_thread``. +The ``urllib``, ``urllib2`` and ``urlparse`` modules were reworked in depth +and :mod:`django.utils.six.moves <six.moves>` doesn't handle them. Django +explicitly tries both locations, as follows:: + + try: + from urllib.parse import urlparse, urlunparse + except ImportError: # Python 2 + from urlparse import urlparse, urlunparse PY3 ---- +~~~ If you need different code in Python 2 and Python 3, check :data:`six.PY3`:: @@ -129,9 +380,9 @@ function. .. module:: django.utils.six Customizations of six -===================== +--------------------- -The version of six bundled with Django includes a few additional tools: +The version of six bundled with Django includes one extra function: .. function:: iterlists(MultiValueDict) @@ -140,3 +391,6 @@ The version of six bundled with Django includes a few additional tools: :meth:`~django.utils.datastructures.MultiValueDict.iterlists()` on Python 2 and :meth:`~django.utils.datastructures.MultiValueDict.lists()` on Python 3. + +In addition to six' defaults moves, Django's version provides ``thread`` as +``_thread`` and ``dummy_thread`` as ``_dummy_thread``. diff --git a/docs/topics/templates.txt b/docs/topics/templates.txt index 2fa6643975..af45a8d95b 100644 --- a/docs/topics/templates.txt +++ b/docs/topics/templates.txt @@ -102,7 +102,7 @@ Use a dot (``.``) to access attributes of a variable. attempts to loop over a ``collections.defaultdict``:: {% for k, v in defaultdict.iteritems %} - Do something with k and v here... + Do something with k and v here... {% endfor %} Because dictionary lookup happens first, that behavior kicks in and provides @@ -116,6 +116,10 @@ If you use a variable that doesn't exist, the template system will insert the value of the :setting:`TEMPLATE_STRING_IF_INVALID` setting, which is set to ``''`` (the empty string) by default. +Note that "bar" in a template expression like ``{{ foo.bar }}`` will be +interpreted as a literal string and not using the value of the variable "bar", +if one exists in the template context. + Filters ======= diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 1f4c970d3e..c4c73733f5 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -2039,7 +2039,7 @@ out the `full reference`_ for more details. self.selenium.find_element_by_xpath('//input[@value="Log in"]').click() # Wait until the response is received WebDriverWait(self.selenium, timeout).until( - lambda driver: driver.find_element_by_tag_name('body'), timeout=10) + lambda driver: driver.find_element_by_tag_name('body')) The tricky thing here is that there's really no such thing as a "page load," especially in modern Web apps that generate HTML dynamically after the @@ -1,4 +1,4 @@ [bdist_rpm] -doc_files = docs extras AUTHORS INSTALL LICENSE README +doc_files = docs extras AUTHORS INSTALL LICENSE README.rst install-script = scripts/rpm-install.sh diff --git a/tests/modeltests/aggregation/models.py b/tests/modeltests/aggregation/models.py index ccc12898b7..b4f797ee03 100644 --- a/tests/modeltests/aggregation/models.py +++ b/tests/modeltests/aggregation/models.py @@ -1,22 +1,26 @@ # coding: utf-8 from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=100) age = models.IntegerField() friends = models.ManyToManyField('self', blank=True) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Publisher(models.Model): name = models.CharField(max_length=255) num_awards = models.IntegerField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Book(models.Model): isbn = models.CharField(max_length=9) name = models.CharField(max_length=255) @@ -28,15 +32,16 @@ class Book(models.Model): publisher = models.ForeignKey(Publisher) pubdate = models.DateField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Store(models.Model): name = models.CharField(max_length=255) books = models.ManyToManyField(Book) original_opening = models.DateTimeField() friday_night_closing = models.TimeField() - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py index 06aa9cf3c3..660beddf49 100644 --- a/tests/modeltests/basic/models.py +++ b/tests/modeltests/basic/models.py @@ -5,8 +5,10 @@ This is a basic model with only two non-primary-key fields. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() @@ -14,5 +16,5 @@ class Article(models.Model): class Meta: ordering = ('pub_date','headline') - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/choices/models.py b/tests/modeltests/choices/models.py index ee01911573..2fa33a9680 100644 --- a/tests/modeltests/choices/models.py +++ b/tests/modeltests/choices/models.py @@ -10,6 +10,7 @@ field. This method returns the "human-readable" value of the field. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible GENDER_CHOICES = ( @@ -17,9 +18,10 @@ GENDER_CHOICES = ( ('F', 'Female'), ) +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=20) gender = models.CharField(max_length=1, choices=GENDER_CHOICES) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/custom_columns/models.py b/tests/modeltests/custom_columns/models.py index 39f1274a8f..16f0563d6b 100644 --- a/tests/modeltests/custom_columns/models.py +++ b/tests/modeltests/custom_columns/models.py @@ -18,24 +18,27 @@ from the default generated name, use the ``db_table`` parameter on the from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): first_name = models.CharField(max_length=30, db_column='firstname') last_name = models.CharField(max_length=30, db_column='last') - def __unicode__(self): + def __str__(self): return '%s %s' % (self.first_name, self.last_name) class Meta: db_table = 'my_author_table' ordering = ('last_name','first_name') +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) authors = models.ManyToManyField(Author, db_table='my_m2m_table') - def __unicode__(self): + def __str__(self): return self.headline class Meta: diff --git a/tests/modeltests/custom_managers/models.py b/tests/modeltests/custom_managers/models.py index a9845ad414..de7c1772ed 100644 --- a/tests/modeltests/custom_managers/models.py +++ b/tests/modeltests/custom_managers/models.py @@ -12,6 +12,7 @@ returns. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible # An example of a custom manager called "objects". @@ -19,13 +20,14 @@ class PersonManager(models.Manager): def get_fun_people(self): return self.filter(fun=True) +@python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) fun = models.BooleanField() objects = PersonManager() - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) # An example of a custom manager that sets get_query_set(). @@ -34,6 +36,7 @@ class PublishedBookManager(models.Manager): def get_query_set(self): return super(PublishedBookManager, self).get_query_set().filter(is_published=True) +@python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) author = models.CharField(max_length=30) @@ -41,7 +44,7 @@ class Book(models.Model): published_objects = PublishedBookManager() authors = models.ManyToManyField(Person, related_name='books') - def __unicode__(self): + def __str__(self): return self.title # An example of providing multiple custom managers. @@ -50,6 +53,7 @@ class FastCarManager(models.Manager): def get_query_set(self): return super(FastCarManager, self).get_query_set().filter(top_speed__gt=150) +@python_2_unicode_compatible class Car(models.Model): name = models.CharField(max_length=10) mileage = models.IntegerField() @@ -57,5 +61,5 @@ class Car(models.Model): cars = models.Manager() fast_cars = FastCarManager() - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/custom_methods/models.py b/tests/modeltests/custom_methods/models.py index 4e3da58851..cef3fd722b 100644 --- a/tests/modeltests/custom_methods/models.py +++ b/tests/modeltests/custom_methods/models.py @@ -7,13 +7,15 @@ Any method you add to a model will be available to instances. import datetime from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() - def __unicode__(self): + def __str__(self): return self.headline def was_published_today(self): diff --git a/tests/modeltests/custom_pk/fields.py b/tests/modeltests/custom_pk/fields.py index 68fb9dcd16..92096a0ff7 100644 --- a/tests/modeltests/custom_pk/fields.py +++ b/tests/modeltests/custom_pk/fields.py @@ -3,8 +3,10 @@ import string from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class MyWrapper(object): def __init__(self, value): self.value = value @@ -12,7 +14,7 @@ class MyWrapper(object): def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.value) - def __unicode__(self): + def __str__(self): return self.value def __eq__(self, other): @@ -20,8 +22,7 @@ class MyWrapper(object): return self.value == other.value return self.value == other -class MyAutoField(models.CharField): - __metaclass__ = models.SubfieldBase +class MyAutoField(six.with_metaclass(models.SubfieldBase, models.CharField)): def __init__(self, *args, **kwargs): kwargs['max_length'] = 10 @@ -30,7 +31,7 @@ class MyAutoField(models.CharField): def pre_save(self, instance, add): value = getattr(instance, self.attname, None) if not value: - value = MyWrapper(''.join(random.sample(string.lowercase, 10))) + value = MyWrapper(''.join(random.sample(string.ascii_lowercase, 10))) setattr(instance, self.attname, value) return value diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py index 8199b05a1a..5ef9b69f0c 100644 --- a/tests/modeltests/custom_pk/models.py +++ b/tests/modeltests/custom_pk/models.py @@ -11,8 +11,10 @@ from __future__ import absolute_import, unicode_literals from django.db import models from .fields import MyAutoField +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Employee(models.Model): employee_code = models.IntegerField(primary_key=True, db_column = 'code') first_name = models.CharField(max_length=20) @@ -20,22 +22,24 @@ class Employee(models.Model): class Meta: ordering = ('last_name', 'first_name') - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) +@python_2_unicode_compatible class Business(models.Model): name = models.CharField(max_length=20, primary_key=True) employees = models.ManyToManyField(Employee) class Meta: verbose_name_plural = 'businesses' - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Bar(models.Model): id = MyAutoField(primary_key=True, db_index=True) - def __unicode__(self): + def __str__(self): return repr(self.pk) diff --git a/tests/modeltests/defer/models.py b/tests/modeltests/defer/models.py index c64becf972..0688cbc984 100644 --- a/tests/modeltests/defer/models.py +++ b/tests/modeltests/defer/models.py @@ -3,18 +3,20 @@ Tests for defer() and only(). """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Secondary(models.Model): first = models.CharField(max_length=50) second = models.CharField(max_length=50) +@python_2_unicode_compatible class Primary(models.Model): name = models.CharField(max_length=50) value = models.CharField(max_length=50) related = models.ForeignKey(Secondary) - def __unicode__(self): + def __str__(self): return self.name class Child(Primary): diff --git a/tests/modeltests/delete/models.py b/tests/modeltests/delete/models.py index f8b78eb7b7..e0cec426ea 100644 --- a/tests/modeltests/delete/models.py +++ b/tests/modeltests/delete/models.py @@ -1,6 +1,10 @@ +from __future__ import unicode_literals + from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class R(models.Model): is_default = models.BooleanField(default=False) diff --git a/tests/modeltests/distinct_on_fields/models.py b/tests/modeltests/distinct_on_fields/models.py index 33665e9624..7982f435d0 100644 --- a/tests/modeltests/distinct_on_fields/models.py +++ b/tests/modeltests/distinct_on_fields/models.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Tag(models.Model): name = models.CharField(max_length=10) parent = models.ForeignKey('self', blank=True, null=True, @@ -10,19 +12,21 @@ class Tag(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Celebrity(models.Model): name = models.CharField("Name", max_length=20) greatest_fan = models.ForeignKey("Fan", null=True, unique=True) - def __unicode__(self): + def __str__(self): return self.name class Fan(models.Model): fan_of = models.ForeignKey(Celebrity) +@python_2_unicode_compatible class Staff(models.Model): id = models.IntegerField(primary_key=True) name = models.CharField(max_length=50) @@ -30,12 +34,13 @@ class Staff(models.Model): tags = models.ManyToManyField(Tag, through='StaffTag') coworkers = models.ManyToManyField('self') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class StaffTag(models.Model): staff = models.ForeignKey(Staff) tag = models.ForeignKey(Tag) - def __unicode__(self): + def __str__(self): return "%s -> %s" % (self.tag, self.staff) diff --git a/tests/modeltests/expressions/models.py b/tests/modeltests/expressions/models.py index 018a0cf795..f592a0eb13 100644 --- a/tests/modeltests/expressions/models.py +++ b/tests/modeltests/expressions/models.py @@ -4,15 +4,18 @@ Tests for F() query expression syntax. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Employee(models.Model): firstname = models.CharField(max_length=50) lastname = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return '%s %s' % (self.firstname, self.lastname) +@python_2_unicode_compatible class Company(models.Model): name = models.CharField(max_length=100) num_employees = models.PositiveIntegerField() @@ -25,5 +28,5 @@ class Company(models.Model): related_name='company_point_of_contact_set', null=True) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/field_defaults/models.py b/tests/modeltests/field_defaults/models.py index 18840c8b4b..c99d4871cd 100644 --- a/tests/modeltests/field_defaults/models.py +++ b/tests/modeltests/field_defaults/models.py @@ -13,11 +13,13 @@ field. from datetime import datetime from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField(default=datetime.now) - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/field_subclassing/fields.py b/tests/modeltests/field_subclassing/fields.py index 0d4ff98aa7..a3867e3671 100644 --- a/tests/modeltests/field_subclassing/fields.py +++ b/tests/modeltests/field_subclassing/fields.py @@ -5,8 +5,10 @@ import json from django.db import models from django.utils.encoding import force_text from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Small(object): """ A simple class to show that non-trivial Python objects can be used as @@ -15,19 +17,15 @@ class Small(object): def __init__(self, first, second): self.first, self.second = first, second - def __unicode__(self): - return '%s%s' % (force_text(self.first), force_text(self.second)) - def __str__(self): - return six.text_type(self).encode('utf-8') + return '%s%s' % (force_text(self.first), force_text(self.second)) -class SmallField(models.Field): +class SmallField(six.with_metaclass(models.SubfieldBase, models.Field)): """ Turns the "Small" class into a Django field. Because of the similarities with normal character fields and the fact that Small.__unicode__ does something sensible, we don't need to implement a lot here. """ - __metaclass__ = models.SubfieldBase def __init__(self, *args, **kwargs): kwargs['max_length'] = 2 @@ -57,8 +55,7 @@ class SmallerField(SmallField): pass -class JSONField(models.TextField): - __metaclass__ = models.SubfieldBase +class JSONField(six.with_metaclass(models.SubfieldBase, models.TextField)): description = ("JSONField automatically serializes and desializes values to " "and from JSON.") diff --git a/tests/modeltests/field_subclassing/models.py b/tests/modeltests/field_subclassing/models.py index 2df9664cdc..642573cc83 100644 --- a/tests/modeltests/field_subclassing/models.py +++ b/tests/modeltests/field_subclassing/models.py @@ -8,13 +8,15 @@ from django.db import models from django.utils.encoding import force_text from .fields import SmallField, SmallerField, JSONField +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class MyModel(models.Model): name = models.CharField(max_length=10) data = SmallField('small field') - def __unicode__(self): + def __str__(self): return force_text(self.name) class OtherModel(models.Model): diff --git a/tests/modeltests/files/tests.py b/tests/modeltests/files/tests.py index 565d4c8942..71ca103205 100644 --- a/tests/modeltests/files/tests.py +++ b/tests/modeltests/files/tests.py @@ -20,7 +20,7 @@ class FileStorageTests(TestCase): shutil.rmtree(temp_storage_location) def test_files(self): - temp_storage.save('tests/default.txt', ContentFile(b'default content')) + temp_storage.save('tests/default.txt', ContentFile('default content')) # Attempting to access a FileField from the class raises a descriptive # error self.assertRaises(AttributeError, lambda: Storage.normal) @@ -31,7 +31,7 @@ class FileStorageTests(TestCase): self.assertRaises(ValueError, lambda: obj1.normal.size) # Saving a file enables full functionality. - obj1.normal.save("django_test.txt", ContentFile(b"content")) + obj1.normal.save("django_test.txt", ContentFile("content")) self.assertEqual(obj1.normal.name, "tests/django_test.txt") self.assertEqual(obj1.normal.size, 7) self.assertEqual(obj1.normal.read(), b"content") @@ -59,7 +59,7 @@ class FileStorageTests(TestCase): # Save another file with the same name. obj2 = Storage() - obj2.normal.save("django_test.txt", ContentFile(b"more content")) + obj2.normal.save("django_test.txt", ContentFile("more content")) self.assertEqual(obj2.normal.name, "tests/django_test_1.txt") self.assertEqual(obj2.normal.size, 12) @@ -70,13 +70,13 @@ class FileStorageTests(TestCase): # Deleting an object does not delete the file it uses. obj2.delete() - obj2.normal.save("django_test.txt", ContentFile(b"more content")) + obj2.normal.save("django_test.txt", ContentFile("more content")) self.assertEqual(obj2.normal.name, "tests/django_test_2.txt") # Multiple files with the same name get _N appended to them. objs = [Storage() for i in range(3)] for o in objs: - o.normal.save("multiple_files.txt", ContentFile(b"Same Content")) + o.normal.save("multiple_files.txt", ContentFile("Same Content")) self.assertEqual( [o.normal.name for o in objs], ["tests/multiple_files.txt", "tests/multiple_files_1.txt", "tests/multiple_files_2.txt"] @@ -100,7 +100,7 @@ class FileStorageTests(TestCase): # Verify the fix for #5655, making sure the directory is only # determined once. obj4 = Storage() - obj4.random.save("random_file", ContentFile(b"random content")) + obj4.random.save("random_file", ContentFile("random content")) self.assertTrue(obj4.random.name.endswith("/random_file")) def test_max_length(self): @@ -135,6 +135,6 @@ class FileTests(unittest.TestCase): def test_file_mode(self): # Should not set mode to None if it is not present. # See #14681, stdlib gzip module crashes if mode is set to None - file = SimpleUploadedFile("mode_test.txt", "content") + file = SimpleUploadedFile("mode_test.txt", b"content") self.assertFalse(hasattr(file, 'mode')) g = gzip.GzipFile(fileobj=file) diff --git a/tests/modeltests/fixtures/models.py b/tests/modeltests/fixtures/models.py index 586f65e60f..8bd3501926 100644 --- a/tests/modeltests/fixtures/models.py +++ b/tests/modeltests/fixtures/models.py @@ -12,38 +12,43 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Category(models.Model): title = models.CharField(max_length=100) description = models.TextField() - def __unicode__(self): + def __str__(self): return self.title class Meta: ordering = ('title',) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() - def __unicode__(self): + def __str__(self): return self.headline class Meta: ordering = ('-pub_date', 'headline') +@python_2_unicode_compatible class Blog(models.Model): name = models.CharField(max_length=100) featured = models.ForeignKey(Article, related_name='fixtures_featured_set') articles = models.ManyToManyField(Article, blank=True, related_name='fixtures_articles_set') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Tag(models.Model): name = models.CharField(max_length=100) tagged_type = models.ForeignKey(ContentType, related_name="fixtures_tag_set") @@ -51,7 +56,7 @@ class Tag(models.Model): tagged = generic.GenericForeignKey(ct_field='tagged_type', fk_field='tagged_id') - def __unicode__(self): + def __str__(self): return '<%s: %s> tagged "%s"' % (self.tagged.__class__.__name__, self.tagged, self.name) @@ -59,10 +64,11 @@ class PersonManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) +@python_2_unicode_compatible class Person(models.Model): objects = PersonManager() name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name class Meta: @@ -79,19 +85,21 @@ class Spy(Person): objects = SpyManager() cover_blown = models.BooleanField(default=False) +@python_2_unicode_compatible class Visa(models.Model): person = models.ForeignKey(Person) permissions = models.ManyToManyField(Permission, blank=True) - def __unicode__(self): + def __str__(self): return '%s %s' % (self.person.name, ', '.join(p.name for p in self.permissions.all())) +@python_2_unicode_compatible class Book(models.Model): name = models.CharField(max_length=100) authors = models.ManyToManyField(Person) - def __unicode__(self): + def __str__(self): authors = ' and '.join(a.name for a in self.authors.all()) return '%s by %s' % (self.name, authors) if authors else self.name diff --git a/tests/modeltests/fixtures_model_package/models/__init__.py b/tests/modeltests/fixtures_model_package/models/__init__.py index d309165e9c..deeba48aa9 100644 --- a/tests/modeltests/fixtures_model_package/models/__init__.py +++ b/tests/modeltests/fixtures_model_package/models/__init__.py @@ -1,11 +1,13 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() - def __unicode__(self): + def __str__(self): return self.headline class Meta: diff --git a/tests/modeltests/generic_relations/models.py b/tests/modeltests/generic_relations/models.py index f2dcf7db24..2f025e660b 100644 --- a/tests/modeltests/generic_relations/models.py +++ b/tests/modeltests/generic_relations/models.py @@ -14,8 +14,10 @@ from __future__ import unicode_literals from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class TaggedItem(models.Model): """A tag on an item.""" tag = models.SlugField() @@ -27,12 +29,13 @@ class TaggedItem(models.Model): class Meta: ordering = ["tag", "content_type__name"] - def __unicode__(self): + def __str__(self): return self.tag class ValuableTaggedItem(TaggedItem): value = models.PositiveIntegerField() +@python_2_unicode_compatible class Comparison(models.Model): """ A model that tests having multiple GenericForeignKeys @@ -48,9 +51,10 @@ class Comparison(models.Model): first_obj = generic.GenericForeignKey(ct_field="content_type1", fk_field="object_id1") other_obj = generic.GenericForeignKey(ct_field="content_type2", fk_field="object_id2") - def __unicode__(self): + def __str__(self): return "%s is %s than %s" % (self.first_obj, self.comparative, self.other_obj) +@python_2_unicode_compatible class Animal(models.Model): common_name = models.CharField(max_length=150) latin_name = models.CharField(max_length=150) @@ -60,25 +64,27 @@ class Animal(models.Model): object_id_field="object_id1", content_type_field="content_type1") - def __unicode__(self): + def __str__(self): return self.common_name +@python_2_unicode_compatible class Vegetable(models.Model): name = models.CharField(max_length=150) is_yucky = models.BooleanField(default=True) tags = generic.GenericRelation(TaggedItem) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Mineral(models.Model): name = models.CharField(max_length=150) hardness = models.PositiveSmallIntegerField() # note the lack of an explicit GenericRelation here... - def __unicode__(self): + def __str__(self): return self.name class GeckoManager(models.Manager): diff --git a/tests/modeltests/get_latest/models.py b/tests/modeltests/get_latest/models.py index d8a690f48c..fe594dd802 100644 --- a/tests/modeltests/get_latest/models.py +++ b/tests/modeltests/get_latest/models.py @@ -9,8 +9,10 @@ farthest into the future." """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() @@ -18,14 +20,15 @@ class Article(models.Model): class Meta: get_latest_by = 'pub_date' - def __unicode__(self): + def __str__(self): return self.headline +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=30) birthday = models.DateField() # Note that this model doesn't have "get_latest_by" set. - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/get_object_or_404/models.py b/tests/modeltests/get_object_or_404/models.py index f0c73ed94a..bda060569e 100644 --- a/tests/modeltests/get_object_or_404/models.py +++ b/tests/modeltests/get_object_or_404/models.py @@ -11,23 +11,26 @@ performing a ``filter()`` lookup and raising a ``Http404`` exception if a """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name class ArticleManager(models.Manager): def get_query_set(self): return super(ArticleManager, self).get_query_set().filter(authors__name__icontains='sir') +@python_2_unicode_compatible class Article(models.Model): authors = models.ManyToManyField(Author) title = models.CharField(max_length=50) objects = models.Manager() by_a_sir = ArticleManager() - def __unicode__(self): + def __str__(self): return self.title diff --git a/tests/modeltests/get_or_create/models.py b/tests/modeltests/get_or_create/models.py index 78b92f09df..678f5a401c 100644 --- a/tests/modeltests/get_or_create/models.py +++ b/tests/modeltests/get_or_create/models.py @@ -9,14 +9,16 @@ parameters. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100) birthday = models.DateField() - def __unicode__(self): + def __str__(self): return '%s %s' % (self.first_name, self.last_name) class ManualPrimaryKeyTest(models.Model): diff --git a/tests/modeltests/invalid_models/tests.py b/tests/modeltests/invalid_models/tests.py index 9d7adb045c..e1fc68743e 100644 --- a/tests/modeltests/invalid_models/tests.py +++ b/tests/modeltests/invalid_models/tests.py @@ -1,11 +1,11 @@ import copy import sys -from io import BytesIO from django.core.management.validation import get_validation_errors from django.db.models.loading import cache, load_app from django.utils import unittest +from django.utils.six import StringIO class InvalidModelTestCase(unittest.TestCase): @@ -16,7 +16,7 @@ class InvalidModelTestCase(unittest.TestCase): # coloring attached (makes matching the results easier). We restore # sys.stderr afterwards. self.old_stdout = sys.stdout - self.stdout = BytesIO() + self.stdout = StringIO() sys.stdout = self.stdout # This test adds dummy applications to the app cache. These diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index b685750347..f388ddf403 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class Author(models.Model): @@ -15,6 +16,7 @@ class Author(models.Model): class Meta: ordering = ('name', ) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() @@ -22,7 +24,7 @@ class Article(models.Model): class Meta: ordering = ('-pub_date', 'headline') - def __unicode__(self): + def __str__(self): return self.headline class Tag(models.Model): @@ -31,24 +33,27 @@ class Tag(models.Model): class Meta: ordering = ('name', ) +@python_2_unicode_compatible class Season(models.Model): year = models.PositiveSmallIntegerField() gt = models.IntegerField(null=True, blank=True) - def __unicode__(self): + def __str__(self): return six.text_type(self.year) +@python_2_unicode_compatible class Game(models.Model): season = models.ForeignKey(Season, related_name='games') home = models.CharField(max_length=100) away = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return "%s at %s" % (self.away, self.home) +@python_2_unicode_compatible class Player(models.Model): name = models.CharField(max_length=100) games = models.ManyToManyField(Game, related_name='players') - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/m2m_and_m2o/models.py b/tests/modeltests/m2m_and_m2o/models.py index 92ed3fbcd9..99c7b01017 100644 --- a/tests/modeltests/m2m_and_m2o/models.py +++ b/tests/modeltests/m2m_and_m2o/models.py @@ -7,17 +7,19 @@ from __future__ import unicode_literals from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class User(models.Model): username = models.CharField(max_length=20) +@python_2_unicode_compatible class Issue(models.Model): num = models.IntegerField() cc = models.ManyToManyField(User, blank=True, related_name='test_issue_cc') client = models.ForeignKey(User, related_name='test_issue_client') - def __unicode__(self): + def __str__(self): return six.text_type(self.num) class Meta: diff --git a/tests/modeltests/m2m_intermediary/models.py b/tests/modeltests/m2m_intermediary/models.py index 85786e8458..e9ae422afb 100644 --- a/tests/modeltests/m2m_intermediary/models.py +++ b/tests/modeltests/m2m_intermediary/models.py @@ -12,27 +12,31 @@ field, which specifies the ``Reporter``'s position for the given article from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() - def __unicode__(self): + def __str__(self): return self.headline +@python_2_unicode_compatible class Writer(models.Model): reporter = models.ForeignKey(Reporter) article = models.ForeignKey(Article) position = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return '%s (%s)' % (self.reporter, self.position) diff --git a/tests/modeltests/m2m_multiple/models.py b/tests/modeltests/m2m_multiple/models.py index 3efe7108b9..c2e3b030a4 100644 --- a/tests/modeltests/m2m_multiple/models.py +++ b/tests/modeltests/m2m_multiple/models.py @@ -8,16 +8,19 @@ Set ``related_name`` to designate what the reverse relationship is called. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=20) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=50) pub_date = models.DateTimeField() @@ -26,6 +29,6 @@ class Article(models.Model): class Meta: ordering = ('pub_date',) - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/m2m_recursive/models.py b/tests/modeltests/m2m_recursive/models.py index 83c943ae60..b69930208c 100644 --- a/tests/modeltests/m2m_recursive/models.py +++ b/tests/modeltests/m2m_recursive/models.py @@ -17,12 +17,14 @@ appropriate. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=20) friends = models.ManyToManyField('self') idols = models.ManyToManyField('self', symmetrical=False, related_name='stalkers') - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/m2m_signals/models.py b/tests/modeltests/m2m_signals/models.py index 526c4a782e..e997d87f21 100644 --- a/tests/modeltests/m2m_signals/models.py +++ b/tests/modeltests/m2m_signals/models.py @@ -1,15 +1,18 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Part(models.Model): name = models.CharField(max_length=20) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Car(models.Model): name = models.CharField(max_length=20) default_parts = models.ManyToManyField(Part) @@ -18,12 +21,13 @@ class Car(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name class SportsCar(Car): price = models.IntegerField() +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=20) fans = models.ManyToManyField('self', related_name='idols', symmetrical=False) @@ -32,5 +36,5 @@ class Person(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/m2m_through/models.py b/tests/modeltests/m2m_through/models.py index aa71a049cb..a896f6d9ff 100644 --- a/tests/modeltests/m2m_through/models.py +++ b/tests/modeltests/m2m_through/models.py @@ -1,18 +1,21 @@ from datetime import datetime from django.db import models +from django.utils.encoding import python_2_unicode_compatible # M2M described on one of the models +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=128) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField(Person, through='Membership') @@ -22,9 +25,10 @@ class Group(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Membership(models.Model): person = models.ForeignKey(Person) group = models.ForeignKey(Group) @@ -34,16 +38,17 @@ class Membership(models.Model): class Meta: ordering = ('date_joined', 'invite_reason', 'group') - def __unicode__(self): + def __str__(self): return "%s is a member of %s" % (self.person.name, self.group.name) +@python_2_unicode_compatible class CustomMembership(models.Model): person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name") group = models.ForeignKey(Group) weird_fk = models.ForeignKey(Membership, null=True) date_joined = models.DateTimeField(default=datetime.now) - def __unicode__(self): + def __str__(self): return "%s is a member of %s" % (self.person.name, self.group.name) class Meta: @@ -54,11 +59,12 @@ class TestNoDefaultsOrNulls(models.Model): group = models.ForeignKey(Group) nodefaultnonull = models.CharField(max_length=5) +@python_2_unicode_compatible class PersonSelfRefM2M(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="Friendship", symmetrical=False) - def __unicode__(self): + def __str__(self): return self.name class Friendship(models.Model): diff --git a/tests/modeltests/m2o_recursive/models.py b/tests/modeltests/m2o_recursive/models.py index c0a4bdeec2..2775d713dd 100644 --- a/tests/modeltests/m2o_recursive/models.py +++ b/tests/modeltests/m2o_recursive/models.py @@ -11,19 +11,22 @@ Set ``related_name`` to designate what the reverse relationship is called. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=20) parent = models.ForeignKey('self', blank=True, null=True, related_name='child_set') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Person(models.Model): full_name = models.CharField(max_length=20) mother = models.ForeignKey('self', null=True, related_name='mothers_child_set') father = models.ForeignKey('self', null=True, related_name='fathers_child_set') - def __unicode__(self): + def __str__(self): return self.full_name diff --git a/tests/modeltests/many_to_many/models.py b/tests/modeltests/many_to_many/models.py index 5076e35653..a196c85092 100644 --- a/tests/modeltests/many_to_many/models.py +++ b/tests/modeltests/many_to_many/models.py @@ -8,22 +8,25 @@ objects, and a ``Publication`` has multiple ``Article`` objects. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Publication(models.Model): title = models.CharField(max_length=30) - def __unicode__(self): + def __str__(self): return self.title class Meta: ordering = ('title',) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) publications = models.ManyToManyField(Publication) - def __unicode__(self): + def __str__(self): return self.headline class Meta: diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py index 0d2688e8a4..4e2ed67eea 100644 --- a/tests/modeltests/many_to_one/models.py +++ b/tests/modeltests/many_to_one/models.py @@ -6,22 +6,25 @@ To define a many-to-one relationship, use ``ForeignKey()``. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) email = models.EmailField() - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() reporter = models.ForeignKey(Reporter) - def __unicode__(self): + def __str__(self): return self.headline class Meta: diff --git a/tests/modeltests/many_to_one_null/models.py b/tests/modeltests/many_to_one_null/models.py index be7e650c65..e00ca85928 100644 --- a/tests/modeltests/many_to_one_null/models.py +++ b/tests/modeltests/many_to_one_null/models.py @@ -6,14 +6,17 @@ To define a many-to-one relationship that can have a null foreign key, use """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Reporter(models.Model): name = models.CharField(max_length=30) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) reporter = models.ForeignKey(Reporter, null=True) @@ -21,5 +24,5 @@ class Article(models.Model): class Meta: ordering = ('headline',) - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 8942b21f73..90b019b611 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -14,6 +14,7 @@ import tempfile from django.core.files.storage import FileSystemStorage from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible temp_storage_dir = tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) @@ -31,23 +32,29 @@ ARTICLE_STATUS_CHAR = ( ('l', 'Live'), ) +@python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=20) slug = models.SlugField(max_length=20) url = models.CharField('The URL', max_length=40) - def __unicode__(self): + def __str__(self): return self.name + def __repr__(self): + return self.__str__() + +@python_2_unicode_compatible class Writer(models.Model): name = models.CharField(max_length=50, help_text='Use both first and last names.') class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=50) slug = models.SlugField() @@ -64,7 +71,7 @@ class Article(models.Model): self.created = datetime.date.today() return super(Article, self).save() - def __unicode__(self): + def __str__(self): return self.headline class ImprovedArticle(models.Model): @@ -76,26 +83,29 @@ class ImprovedArticleWithParentLink(models.Model): class BetterWriter(Writer): score = models.IntegerField() +@python_2_unicode_compatible class WriterProfile(models.Model): writer = models.OneToOneField(Writer, primary_key=True) age = models.PositiveIntegerField() - def __unicode__(self): + def __str__(self): return "%s is %s" % (self.writer, self.age) from django.contrib.localflavor.us.models import PhoneNumberField +@python_2_unicode_compatible class PhoneNumber(models.Model): phone = PhoneNumberField() description = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return self.phone +@python_2_unicode_compatible class TextFile(models.Model): description = models.CharField(max_length=20) file = models.FileField(storage=temp_storage, upload_to='tests', max_length=15) - def __unicode__(self): + def __str__(self): return self.description try: @@ -110,6 +120,7 @@ try: test_images = True + @python_2_unicode_compatible class ImageFile(models.Model): def custom_upload_path(self, filename): path = self.path or 'tests' @@ -125,9 +136,10 @@ try: width_field='width', height_field='height') path = models.CharField(max_length=16, blank=True, default='') - def __unicode__(self): + def __str__(self): return self.description + @python_2_unicode_compatible class OptionalImageFile(models.Model): def custom_upload_path(self, filename): path = self.path or 'tests' @@ -141,28 +153,31 @@ try: height = models.IntegerField(editable=False, null=True) path = models.CharField(max_length=16, blank=True, default='') - def __unicode__(self): + def __str__(self): return self.description except ImportError: test_images = False +@python_2_unicode_compatible class CommaSeparatedInteger(models.Model): field = models.CommaSeparatedIntegerField(max_length=20) - def __unicode__(self): + def __str__(self): return self.field +@python_2_unicode_compatible class Product(models.Model): slug = models.SlugField(unique=True) - def __unicode__(self): + def __str__(self): return self.slug +@python_2_unicode_compatible class Price(models.Model): price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField() - def __unicode__(self): + def __str__(self): return "%s for %s" % (self.quantity, self.price) class Meta: @@ -171,6 +186,7 @@ class Price(models.Model): class ArticleStatus(models.Model): status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) +@python_2_unicode_compatible class Inventory(models.Model): barcode = models.PositiveIntegerField(unique=True) parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) @@ -179,9 +195,12 @@ class Inventory(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name + def __repr__(self): + return self.__str__() + class Book(models.Model): title = models.CharField(max_length=40) author = models.ForeignKey(Writer, blank=True, null=True) @@ -202,31 +221,34 @@ class BookXtra(models.Model): class DerivedBook(Book, BookXtra): pass +@python_2_unicode_compatible class ExplicitPK(models.Model): key = models.CharField(max_length=20, primary_key=True) desc = models.CharField(max_length=20, blank=True, unique=True) class Meta: unique_together = ('key', 'desc') - def __unicode__(self): + def __str__(self): return self.key +@python_2_unicode_compatible class Post(models.Model): title = models.CharField(max_length=50, unique_for_date='posted', blank=True) slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) posted = models.DateField() - def __unicode__(self): + def __str__(self): return self.name class DerivedPost(Post): pass +@python_2_unicode_compatible class BigInt(models.Model): biggie = models.BigIntegerField() - def __unicode__(self): + def __str__(self): return six.text_type(self.biggie) class MarkupField(models.CharField): diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index 1da7f583be..038ce32287 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -179,14 +179,14 @@ class PriceFormWithoutQuantity(forms.ModelForm): class ModelFormBaseTest(TestCase): def test_base_form(self): - self.assertEqual(BaseCategoryForm.base_fields.keys(), + self.assertEqual(list(BaseCategoryForm.base_fields), ['name', 'slug', 'url']) def test_extra_fields(self): class ExtraFields(BaseCategoryForm): some_extra_field = forms.BooleanField() - self.assertEqual(ExtraFields.base_fields.keys(), + self.assertEqual(list(ExtraFields.base_fields), ['name', 'slug', 'url', 'some_extra_field']) def test_replace_field(self): @@ -215,7 +215,7 @@ class ModelFormBaseTest(TestCase): model = Category fields = ['url'] - self.assertEqual(LimitFields.base_fields.keys(), + self.assertEqual(list(LimitFields.base_fields), ['url']) def test_exclude_fields(self): @@ -224,7 +224,7 @@ class ModelFormBaseTest(TestCase): model = Category exclude = ['url'] - self.assertEqual(ExcludeFields.base_fields.keys(), + self.assertEqual(list(ExcludeFields.base_fields), ['name', 'slug']) def test_confused_form(self): @@ -237,7 +237,7 @@ class ModelFormBaseTest(TestCase): fields = ['name', 'url'] exclude = ['url'] - self.assertEqual(ConfusedForm.base_fields.keys(), + self.assertEqual(list(ConfusedForm.base_fields), ['name']) def test_mixmodel_form(self): @@ -254,13 +254,13 @@ class ModelFormBaseTest(TestCase): # overrides BaseCategoryForm.Meta. self.assertEqual( - MixModelForm.base_fields.keys(), + list(MixModelForm.base_fields), ['headline', 'slug', 'pub_date', 'writer', 'article', 'categories', 'status'] ) def test_article_form(self): self.assertEqual( - ArticleForm.base_fields.keys(), + list(ArticleForm.base_fields), ['headline', 'slug', 'pub_date', 'writer', 'article', 'categories', 'status'] ) @@ -270,7 +270,7 @@ class ModelFormBaseTest(TestCase): pass self.assertEqual( - BadForm.base_fields.keys(), + list(BadForm.base_fields), ['headline', 'slug', 'pub_date', 'writer', 'article', 'categories', 'status'] ) @@ -282,7 +282,7 @@ class ModelFormBaseTest(TestCase): """ pass - self.assertEqual(SubCategoryForm.base_fields.keys(), + self.assertEqual(list(SubCategoryForm.base_fields), ['name', 'slug', 'url']) def test_subclassmeta_form(self): @@ -312,7 +312,7 @@ class ModelFormBaseTest(TestCase): model = Category fields = ['url', 'name'] - self.assertEqual(OrderFields.base_fields.keys(), + self.assertEqual(list(OrderFields.base_fields), ['url', 'name']) self.assertHTMLEqual( str(OrderFields()), @@ -327,7 +327,7 @@ class ModelFormBaseTest(TestCase): fields = ['slug', 'url', 'name'] exclude = ['url'] - self.assertEqual(OrderFields2.base_fields.keys(), + self.assertEqual(list(OrderFields2.base_fields), ['slug', 'name']) @@ -751,9 +751,9 @@ class OldFormForXTests(TestCase): self.assertEqual(new_art.headline, 'New headline') # Add some categories and test the many-to-many form output. - self.assertEqual(map(lambda o: o.name, new_art.categories.all()), []) + self.assertQuerysetEqual(new_art.categories.all(), []) new_art.categories.add(Category.objects.get(name='Entertainment')) - self.assertEqual(map(lambda o: o.name, new_art.categories.all()), ["Entertainment"]) + self.assertQuerysetEqual(new_art.categories.all(), ["Entertainment"]) f = TestArticleForm(auto_id=False, instance=new_art) self.assertHTMLEqual(f.as_ul(), '''<li>Headline: <input type="text" name="headline" value="New headline" maxlength="50" /></li> <li>Slug: <input type="text" name="slug" value="new-headline" maxlength="50" /></li> @@ -815,7 +815,7 @@ class OldFormForXTests(TestCase): new_art = f.save() self.assertEqual(new_art.id == art_id_1, True) new_art = Article.objects.get(id=art_id_1) - self.assertEqual(map(lambda o: o.name, new_art.categories.order_by('name')), + self.assertQuerysetEqual(new_art.categories.order_by('name'), ["Entertainment", "It's a test"]) # Now, submit form data with no categories. This deletes the existing categories. @@ -824,7 +824,7 @@ class OldFormForXTests(TestCase): new_art = f.save() self.assertEqual(new_art.id == art_id_1, True) new_art = Article.objects.get(id=art_id_1) - self.assertEqual(map(lambda o: o.name, new_art.categories.all()), []) + self.assertQuerysetEqual(new_art.categories.all(), []) # Create a new article, with categories, via the form. f = ArticleForm({'headline': 'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': '1967-11-01', @@ -833,7 +833,7 @@ class OldFormForXTests(TestCase): art_id_2 = new_art.id self.assertEqual(art_id_2 not in (None, art_id_1), True) new_art = Article.objects.get(id=art_id_2) - self.assertEqual(map(lambda o: o.name, new_art.categories.order_by('name')), ["Entertainment", "It's a test"]) + self.assertQuerysetEqual(new_art.categories.order_by('name'), ["Entertainment", "It's a test"]) # Create a new article, with no categories, via the form. f = ArticleForm({'headline': 'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': '1967-11-01', @@ -842,7 +842,7 @@ class OldFormForXTests(TestCase): art_id_3 = new_art.id self.assertEqual(art_id_3 not in (None, art_id_1, art_id_2), True) new_art = Article.objects.get(id=art_id_3) - self.assertEqual(map(lambda o: o.name, new_art.categories.all()), []) + self.assertQuerysetEqual(new_art.categories.all(), []) # Create a new article, with categories, via the form, but use commit=False. # The m2m data won't be saved until save_m2m() is invoked on the form. @@ -857,11 +857,11 @@ class OldFormForXTests(TestCase): # The instance doesn't have m2m data yet new_art = Article.objects.get(id=art_id_4) - self.assertEqual(map(lambda o: o.name, new_art.categories.all()), []) + self.assertQuerysetEqual(new_art.categories.all(), []) # Save the m2m data on the form f.save_m2m() - self.assertEqual(map(lambda o: o.name, new_art.categories.order_by('name')), ["Entertainment", "It's a test"]) + self.assertQuerysetEqual(new_art.categories.order_by('name'), ["Entertainment", "It's a test"]) # Here, we define a custom ModelForm. Because it happens to have the same fields as # the Category model, we can just call the form's save() to apply its changes to an @@ -1007,12 +1007,12 @@ class OldFormForXTests(TestCase): f.clean(None) with self.assertRaises(ValidationError): f.clean([]) - self.assertEqual(map(lambda o: o.name, f.clean([c1.id])), ["Entertainment"]) - self.assertEqual(map(lambda o: o.name, f.clean([c2.id])), ["It's a test"]) - self.assertEqual(map(lambda o: o.name, f.clean([str(c1.id)])), ["Entertainment"]) - self.assertEqual(map(lambda o: o.name, f.clean([str(c1.id), str(c2.id)])), ["Entertainment", "It's a test"]) - self.assertEqual(map(lambda o: o.name, f.clean([c1.id, str(c2.id)])), ["Entertainment", "It's a test"]) - self.assertEqual(map(lambda o: o.name, f.clean((c1.id, str(c2.id)))), ["Entertainment", "It's a test"]) + self.assertQuerysetEqual(f.clean([c1.id]), ["Entertainment"]) + self.assertQuerysetEqual(f.clean([c2.id]), ["It's a test"]) + self.assertQuerysetEqual(f.clean([str(c1.id)]), ["Entertainment"]) + self.assertQuerysetEqual(f.clean([str(c1.id), str(c2.id)]), ["Entertainment", "It's a test"]) + self.assertQuerysetEqual(f.clean([c1.id, str(c2.id)]), ["Entertainment", "It's a test"]) + self.assertQuerysetEqual(f.clean((c1.id, str(c2.id))), ["Entertainment", "It's a test"]) with self.assertRaises(ValidationError): f.clean(['100']) with self.assertRaises(ValidationError): @@ -1025,7 +1025,7 @@ class OldFormForXTests(TestCase): # than caching it at time of instantiation. c6 = Category.objects.create(id=6, name='Sixth', url='6th') self.assertEqual(c6.name, 'Sixth') - self.assertEqual(map(lambda o: o.name, f.clean([c6.id])), ["Sixth"]) + self.assertQuerysetEqual(f.clean([c6.id]), ["Sixth"]) # Delete a Category object *after* the ModelMultipleChoiceField has already been # instantiated. This proves clean() checks the database during clean() rather @@ -1050,7 +1050,7 @@ class OldFormForXTests(TestCase): (c1.pk, 'Entertainment'), (c2.pk, "It's a test"), (c3.pk, 'Third')]) - self.assertEqual(map(lambda o: o.name, f.clean([c3.id])), ["Third"]) + self.assertQuerysetEqual(f.clean([c3.id]), ["Third"]) with self.assertRaises(ValidationError): f.clean([c4.id]) with self.assertRaises(ValidationError): @@ -1066,13 +1066,13 @@ class OldFormForXTests(TestCase): # OneToOneField ############################################################### - self.assertEqual(ImprovedArticleForm.base_fields.keys(), ['article']) + self.assertEqual(list(ImprovedArticleForm.base_fields), ['article']) - self.assertEqual(ImprovedArticleWithParentLinkForm.base_fields.keys(), []) + self.assertEqual(list(ImprovedArticleWithParentLinkForm.base_fields), []) bw = BetterWriter(name='Joe Better', score=10) bw.save() - self.assertEqual(sorted(model_to_dict(bw).keys()), + self.assertEqual(sorted(model_to_dict(bw)), ['id', 'name', 'score', 'writer_ptr']) form = BetterWriterForm({'name': 'Some Name', 'score': 12}) @@ -1463,7 +1463,7 @@ class OldFormForXTests(TestCase): model = Category fields = ['description', 'url'] - self.assertEqual(CategoryForm.base_fields.keys(), + self.assertEqual(list(CategoryForm.base_fields), ['description', 'url']) self.assertHTMLEqual(six.text_type(CategoryForm()), '''<tr><th><label for="id_description">Description:</label></th><td><input type="text" name="description" id="id_description" /></td></tr> @@ -1472,14 +1472,15 @@ class OldFormForXTests(TestCase): field = forms.ModelMultipleChoiceField(Inventory.objects.all(), to_field_name='barcode') self.assertEqual(tuple(field.choices), ((86, 'Apple'), (87, 'Core'), (22, 'Pear'))) - self.assertEqual(map(lambda o: o.name, field.clean([86])), ['Apple']) + self.assertQuerysetEqual(field.clean([86]), ['Apple']) form = SelectInventoryForm({'items': [87, 22]}) self.assertEqual(form.is_valid(), True) self.assertEqual(len(form.cleaned_data), 1) - self.assertEqual(map(lambda o: o.name, form.cleaned_data['items']), ['Core', 'Pear']) + self.assertQuerysetEqual(form.cleaned_data['items'], ['Core', 'Pear']) def test_model_field_that_returns_none_to_exclude_itself_with_explicit_fields(self): - self.assertEqual(CustomFieldForExclusionForm.base_fields.keys(), ['name']) + self.assertEqual(list(CustomFieldForExclusionForm.base_fields), + ['name']) self.assertHTMLEqual(six.text_type(CustomFieldForExclusionForm()), '''<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="10" /></td></tr>''') diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index a01a2da0fa..ae152448ab 100644 --- a/tests/modeltests/model_formsets/models.py +++ b/tests/modeltests/model_formsets/models.py @@ -4,20 +4,23 @@ import datetime from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=100) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name class BetterAuthor(Author): write_speed = models.IntegerField() +@python_2_unicode_compatible class Book(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100) @@ -28,20 +31,22 @@ class Book(models.Model): ) ordering = ['id'] - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class BookWithCustomPK(models.Model): my_pk = models.DecimalField(max_digits=5, decimal_places=0, primary_key=True) author = models.ForeignKey(Author) title = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return '%s: %s' % (self.my_pk, self.title) class Editor(models.Model): name = models.CharField(max_length=100) +@python_2_unicode_compatible class BookWithOptionalAltEditor(models.Model): author = models.ForeignKey(Author) # Optional secondary author @@ -53,21 +58,23 @@ class BookWithOptionalAltEditor(models.Model): ('author', 'title', 'alt_editor'), ) - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class AlternateBook(Book): notes = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return '%s - %s' % (self.title, self.notes) +@python_2_unicode_compatible class AuthorMeeting(models.Model): name = models.CharField(max_length=100) authors = models.ManyToManyField(Author) created = models.DateField(editable=False) - def __unicode__(self): + def __str__(self): return self.name class CustomPrimaryKey(models.Model): @@ -77,19 +84,21 @@ class CustomPrimaryKey(models.Model): # models for inheritance tests. +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=50) city = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Owner(models.Model): auto_id = models.AutoField(primary_key=True) name = models.CharField(max_length=100) place = models.ForeignKey(Place) - def __unicode__(self): + def __str__(self): return "%s at %s" % (self.name, self.place) class Location(models.Model): @@ -98,30 +107,34 @@ class Location(models.Model): lat = models.CharField(max_length=100) lon = models.CharField(max_length=100) +@python_2_unicode_compatible class OwnerProfile(models.Model): owner = models.OneToOneField(Owner, primary_key=True) age = models.PositiveIntegerField() - def __unicode__(self): + def __str__(self): return "%s is %d" % (self.owner.name, self.age) +@python_2_unicode_compatible class Restaurant(Place): serves_pizza = models.BooleanField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Product(models.Model): slug = models.SlugField(unique=True) - def __unicode__(self): + def __str__(self): return self.slug +@python_2_unicode_compatible class Price(models.Model): price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField() - def __unicode__(self): + def __str__(self): return "%s for %s" % (self.quantity, self.price) class Meta: @@ -136,12 +149,14 @@ class ClassyMexicanRestaurant(MexicanRestaurant): # models for testing unique_together validation when a fk is involved and # using inlineformset_factory. +@python_2_unicode_compatible class Repository(models.Model): name = models.CharField(max_length=25) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Revision(models.Model): repository = models.ForeignKey(Repository) revision = models.CharField(max_length=40) @@ -149,7 +164,7 @@ class Revision(models.Model): class Meta: unique_together = (("repository", "revision"),) - def __unicode__(self): + def __str__(self): return "%s (%s)" % (self.revision, six.text_type(self.repository)) # models for testing callable defaults (see bug #7975). If you define a model @@ -167,32 +182,36 @@ class Membership(models.Model): class Team(models.Model): name = models.CharField(max_length=100) +@python_2_unicode_compatible class Player(models.Model): team = models.ForeignKey(Team, null=True) name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name # Models for testing custom ModelForm save methods in formsets and inline formsets +@python_2_unicode_compatible class Poet(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Poem(models.Model): poet = models.ForeignKey(Poet) name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Post(models.Model): title = models.CharField(max_length=50, unique_for_date='posted', blank=True) slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) posted = models.DateField() - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index 37ca603021..2101f394f7 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -14,11 +14,13 @@ Both styles are demonstrated here. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible # # Abstract base classes # +@python_2_unicode_compatible class CommonInfo(models.Model): name = models.CharField(max_length=50) age = models.PositiveIntegerField() @@ -27,7 +29,7 @@ class CommonInfo(models.Model): abstract = True ordering = ['name'] - def __unicode__(self): + def __str__(self): return '%s %s' % (self.__class__.__name__, self.name) class Worker(CommonInfo): @@ -49,6 +51,7 @@ class StudentWorker(Student, Worker): class Post(models.Model): title = models.CharField(max_length=50) +@python_2_unicode_compatible class Attachment(models.Model): post = models.ForeignKey(Post, related_name='attached_%(class)s_set') content = models.TextField() @@ -56,7 +59,7 @@ class Attachment(models.Model): class Meta: abstract = True - def __unicode__(self): + def __str__(self): return self.content class Comment(Attachment): @@ -69,17 +72,19 @@ class Link(Attachment): # Multi-table inheritance # +@python_2_unicode_compatible class Chef(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return "%s the chef" % self.name +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) - def __unicode__(self): + def __str__(self): return "%s the place" % self.name class Rating(models.Model): @@ -89,6 +94,7 @@ class Rating(models.Model): abstract = True ordering = ['-rating'] +@python_2_unicode_compatible class Restaurant(Place, Rating): serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() @@ -97,27 +103,30 @@ class Restaurant(Place, Rating): class Meta(Rating.Meta): db_table = 'my_restaurant' - def __unicode__(self): + def __str__(self): return "%s the restaurant" % self.name +@python_2_unicode_compatible class ItalianRestaurant(Restaurant): serves_gnocchi = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the italian restaurant" % self.name +@python_2_unicode_compatible class Supplier(Place): customers = models.ManyToManyField(Restaurant, related_name='provider') - def __unicode__(self): + def __str__(self): return "%s the supplier" % self.name +@python_2_unicode_compatible class ParkingLot(Place): # An explicit link to the parent (we can control the attribute name). parent = models.OneToOneField(Place, primary_key=True, parent_link=True) main_site = models.ForeignKey(Place, related_name='lot') - def __unicode__(self): + def __str__(self): return "%s the parking lot" % self.name # @@ -139,10 +148,11 @@ class NamedURL(models.Model): class Meta: abstract = True +@python_2_unicode_compatible class Copy(NamedURL): content = models.TextField() - def __unicode__(self): + def __str__(self): return self.content class Mixin(object): diff --git a/tests/modeltests/model_inheritance_same_model_name/models.py b/tests/modeltests/model_inheritance_same_model_name/models.py index de7541694a..801724df18 100644 --- a/tests/modeltests/model_inheritance_same_model_name/models.py +++ b/tests/modeltests/model_inheritance_same_model_name/models.py @@ -11,12 +11,14 @@ from __future__ import absolute_import from django.db import models from ..model_inheritance.models import NamedURL +from django.utils.encoding import python_2_unicode_compatible # # Abstract base classes with related models # +@python_2_unicode_compatible class Copy(NamedURL): content = models.TextField() - def __unicode__(self): + def __str__(self): return self.content diff --git a/tests/modeltests/one_to_one/models.py b/tests/modeltests/one_to_one/models.py index 8c6d56a795..9599496cb7 100644 --- a/tests/modeltests/one_to_one/models.py +++ b/tests/modeltests/one_to_one/models.py @@ -8,28 +8,32 @@ In this example, a ``Place`` optionally can be a ``Restaurant``. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) - def __unicode__(self): + def __str__(self): return "%s the place" % self.name +@python_2_unicode_compatible class Restaurant(models.Model): place = models.OneToOneField(Place, primary_key=True) serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the restaurant" % self.place.name +@python_2_unicode_compatible class Waiter(models.Model): restaurant = models.ForeignKey(Restaurant) name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return "%s the waiter at %s" % (self.name, self.restaurant) class ManualPrimaryKey(models.Model): @@ -40,10 +44,11 @@ class RelatedModel(models.Model): link = models.OneToOneField(ManualPrimaryKey) name = models.CharField(max_length = 50) +@python_2_unicode_compatible class MultiModel(models.Model): link1 = models.OneToOneField(Place) link2 = models.OneToOneField(ManualPrimaryKey) name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return "Multimodel %s" % self.name diff --git a/tests/modeltests/or_lookups/models.py b/tests/modeltests/or_lookups/models.py index 0037b41afb..f146b2e72d 100644 --- a/tests/modeltests/or_lookups/models.py +++ b/tests/modeltests/or_lookups/models.py @@ -10,8 +10,10 @@ clauses using the variable ``django.db.models.Q`` (or any object with an """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=50) pub_date = models.DateTimeField() @@ -19,5 +21,5 @@ class Article(models.Model): class Meta: ordering = ('pub_date',) - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/order_with_respect_to/models.py b/tests/modeltests/order_with_respect_to/models.py index a4e20c2fe0..06bb56b141 100644 --- a/tests/modeltests/order_with_respect_to/models.py +++ b/tests/modeltests/order_with_respect_to/models.py @@ -4,11 +4,13 @@ Tests for the order_with_respect_to Meta attribute. from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class Question(models.Model): text = models.CharField(max_length=200) +@python_2_unicode_compatible class Answer(models.Model): text = models.CharField(max_length=200) question = models.ForeignKey(Question) @@ -16,9 +18,10 @@ class Answer(models.Model): class Meta: order_with_respect_to = 'question' - def __unicode__(self): + def __str__(self): return six.text_type(self.text) +@python_2_unicode_compatible class Post(models.Model): title = models.CharField(max_length=200) parent = models.ForeignKey("self", related_name="children", null=True) @@ -26,5 +29,5 @@ class Post(models.Model): class Meta: order_with_respect_to = "parent" - def __unicode__(self): + def __str__(self): return self.title diff --git a/tests/modeltests/ordering/models.py b/tests/modeltests/ordering/models.py index bfb4b97107..67126e1bda 100644 --- a/tests/modeltests/ordering/models.py +++ b/tests/modeltests/ordering/models.py @@ -14,22 +14,25 @@ undefined -- not random, just undefined. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() class Meta: ordering = ('-pub_date', 'headline') - def __unicode__(self): + def __str__(self): return self.headline +@python_2_unicode_compatible class ArticlePKOrdering(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() class Meta: ordering = ('-pk',) - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index 48484dd59b..779d3029ba 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -7,11 +7,13 @@ objects into easily readable pages. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/modeltests/pagination/tests.py b/tests/modeltests/pagination/tests.py index cba8825ec4..12ce0e3ecb 100644 --- a/tests/modeltests/pagination/tests.py +++ b/tests/modeltests/pagination/tests.py @@ -28,7 +28,7 @@ class PaginationTests(TestCase): paginator = Paginator(Article.objects.all(), 5) self.assertEqual(9, paginator.count) self.assertEqual(2, paginator.num_pages) - self.assertEqual([1, 2], paginator.page_range) + self.assertEqual([1, 2], list(paginator.page_range)) def test_first_page(self): paginator = Paginator(Article.objects.all(), 5) @@ -78,13 +78,13 @@ class PaginationTests(TestCase): paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True) self.assertEqual(0, paginator.count) self.assertEqual(1, paginator.num_pages) - self.assertEqual([1], paginator.page_range) + self.assertEqual([1], list(paginator.page_range)) # Empty paginators with allow_empty_first_page=False. paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False) self.assertEqual(0, paginator.count) self.assertEqual(0, paginator.num_pages) - self.assertEqual([], paginator.page_range) + self.assertEqual([], list(paginator.page_range)) def test_invalid_page(self): paginator = Paginator(Article.objects.all(), 5) @@ -108,7 +108,7 @@ class PaginationTests(TestCase): paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 3) self.assertEqual(9, paginator.count) self.assertEqual(3, paginator.num_pages) - self.assertEqual([1, 2, 3], paginator.page_range) + self.assertEqual([1, 2, 3], list(paginator.page_range)) p = paginator.page(2) self.assertEqual("<Page 2 of 3>", six.text_type(p)) self.assertEqual([4, 5, 6], p.object_list) @@ -125,10 +125,10 @@ class PaginationTests(TestCase): paginator = Paginator(CountContainer(), 10) self.assertEqual(42, paginator.count) self.assertEqual(5, paginator.num_pages) - self.assertEqual([1, 2, 3, 4, 5], paginator.page_range) + self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range)) # Paginator can be passed other objects that implement __len__. paginator = Paginator(LenContainer(), 10) self.assertEqual(42, paginator.count) self.assertEqual(5, paginator.num_pages) - self.assertEqual([1, 2, 3, 4, 5], paginator.page_range) + self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range)) diff --git a/tests/modeltests/prefetch_related/models.py b/tests/modeltests/prefetch_related/models.py index 589f78c7d3..85488f0879 100644 --- a/tests/modeltests/prefetch_related/models.py +++ b/tests/modeltests/prefetch_related/models.py @@ -1,16 +1,18 @@ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.encoding import python_2_unicode_compatible ## Basic tests +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=50, unique=True) first_book = models.ForeignKey('Book', related_name='first_time_authors') favorite_authors = models.ManyToManyField( 'self', through='FavoriteAuthors', symmetrical=False, related_name='favors_me') - def __unicode__(self): + def __str__(self): return self.name class Meta: @@ -30,6 +32,7 @@ class FavoriteAuthors(models.Model): ordering = ['id'] +@python_2_unicode_compatible class AuthorAddress(models.Model): author = models.ForeignKey(Author, to_field='name', related_name='addresses') address = models.TextField() @@ -37,15 +40,16 @@ class AuthorAddress(models.Model): class Meta: ordering = ['id'] - def __unicode__(self): + def __str__(self): return self.address +@python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=255) authors = models.ManyToManyField(Author, related_name='books') - def __unicode__(self): + def __str__(self): return self.title class Meta: @@ -58,11 +62,12 @@ class BookWithYear(Book): AuthorWithAge, related_name='books_with_year') +@python_2_unicode_compatible class Reader(models.Model): name = models.CharField(max_length=50) books_read = models.ManyToManyField(Book, related_name='read_by') - def __unicode__(self): + def __str__(self): return self.name class Meta: @@ -86,13 +91,14 @@ class TeacherManager(models.Manager): return super(TeacherManager, self).get_query_set().prefetch_related('qualifications') +@python_2_unicode_compatible class Teacher(models.Model): name = models.CharField(max_length=50) qualifications = models.ManyToManyField(Qualification) objects = TeacherManager() - def __unicode__(self): + def __str__(self): return "%s (%s)" % (self.name, ", ".join(q.name for q in self.qualifications.all())) class Meta: @@ -109,6 +115,7 @@ class Department(models.Model): ## GenericRelation/GenericForeignKey tests +@python_2_unicode_compatible class TaggedItem(models.Model): tag = models.SlugField() content_type = models.ForeignKey(ContentType, related_name="taggeditem_set2") @@ -119,7 +126,7 @@ class TaggedItem(models.Model): created_by_fkey = models.PositiveIntegerField(null=True) created_by = generic.GenericForeignKey('created_by_ct', 'created_by_fkey',) - def __unicode__(self): + def __str__(self): return self.tag @@ -169,12 +176,13 @@ class Person(models.Model): ## Models for nullable FK tests +@python_2_unicode_compatible class Employee(models.Model): name = models.CharField(max_length=50) boss = models.ForeignKey('self', null=True, related_name='serfs') - def __unicode__(self): + def __str__(self): return self.name class Meta: diff --git a/tests/modeltests/prefetch_related/tests.py b/tests/modeltests/prefetch_related/tests.py index 8ae0a1e298..614a5fc1d6 100644 --- a/tests/modeltests/prefetch_related/tests.py +++ b/tests/modeltests/prefetch_related/tests.py @@ -352,7 +352,7 @@ class MultiTableInheritanceTest(TestCase): with self.assertNumQueries(2): qs = BookReview.objects.prefetch_related('book') titles = [obj.book.title for obj in qs] - self.assertEquals(titles, ["Poems", "More poems"]) + self.assertEqual(titles, ["Poems", "More poems"]) def test_m2m_to_inheriting_model(self): qs = AuthorWithAge.objects.prefetch_related('books_with_year') diff --git a/tests/modeltests/proxy_model_inheritance/tests.py b/tests/modeltests/proxy_model_inheritance/tests.py index 061cdc7e91..39fee7ee6d 100644 --- a/tests/modeltests/proxy_model_inheritance/tests.py +++ b/tests/modeltests/proxy_model_inheritance/tests.py @@ -24,7 +24,8 @@ class ProxyModelInheritanceTests(TransactionTestCase): def setUp(self): self.old_sys_path = sys.path[:] sys.path.append(os.path.dirname(os.path.abspath(__file__))) - map(load_app, settings.INSTALLED_APPS) + for app in settings.INSTALLED_APPS: + load_app(app) def tearDown(self): sys.path = self.old_sys_path diff --git a/tests/modeltests/proxy_models/models.py b/tests/modeltests/proxy_models/models.py index 49fd87deff..6c962aadc8 100644 --- a/tests/modeltests/proxy_models/models.py +++ b/tests/modeltests/proxy_models/models.py @@ -5,6 +5,7 @@ than using a new table of their own. This allows them to act as simple proxies, providing a modified interface to the data from the base class. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible # A couple of managers for testing managing overriding in proxy model cases. @@ -16,6 +17,7 @@ class SubManager(models.Manager): def get_query_set(self): return super(SubManager, self).get_query_set().exclude(name="wilma") +@python_2_unicode_compatible class Person(models.Model): """ A simple concrete base class. @@ -24,7 +26,7 @@ class Person(models.Model): objects = PersonManager() - def __unicode__(self): + def __str__(self): return self.name class Abstract(models.Model): @@ -82,10 +84,11 @@ class MyPersonProxy(MyPerson): class LowerStatusPerson(MyPersonProxy): status = models.CharField(max_length=80) +@python_2_unicode_compatible class User(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name class UserProxy(User): @@ -100,11 +103,12 @@ class UserProxyProxy(UserProxy): class Country(models.Model): name = models.CharField(max_length=50) +@python_2_unicode_compatible class State(models.Model): name = models.CharField(max_length=50) country = models.ForeignKey(Country) - def __unicode__(self): + def __str__(self): return self.name class StateProxy(State): @@ -124,11 +128,12 @@ class ProxyTrackerUser(TrackerUser): proxy = True +@python_2_unicode_compatible class Issue(models.Model): summary = models.CharField(max_length=255) assignee = models.ForeignKey(TrackerUser) - def __unicode__(self): + def __str__(self): return ':'.join((self.__class__.__name__,self.summary,)) class Bug(Issue): diff --git a/tests/modeltests/reserved_names/models.py b/tests/modeltests/reserved_names/models.py index 010649e681..8e942b20e8 100644 --- a/tests/modeltests/reserved_names/models.py +++ b/tests/modeltests/reserved_names/models.py @@ -8,8 +8,10 @@ reserved-name usage. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Thing(models.Model): when = models.CharField(max_length=1, primary_key=True) join = models.CharField(max_length=1) @@ -22,5 +24,5 @@ class Thing(models.Model): class Meta: db_table = 'select' - def __unicode__(self): - return self.when
\ No newline at end of file + def __str__(self): + return self.when diff --git a/tests/modeltests/reverse_lookup/models.py b/tests/modeltests/reverse_lookup/models.py index bb7a163327..ed58177770 100644 --- a/tests/modeltests/reverse_lookup/models.py +++ b/tests/modeltests/reverse_lookup/models.py @@ -5,25 +5,29 @@ This demonstrates the reverse lookup features of the database API. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class User(models.Model): name = models.CharField(max_length=200) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Poll(models.Model): question = models.CharField(max_length=200) creator = models.ForeignKey(User) - def __unicode__(self): + def __str__(self): return self.question +@python_2_unicode_compatible class Choice(models.Model): name = models.CharField(max_length=100) poll = models.ForeignKey(Poll, related_name="poll_choice") related_poll = models.ForeignKey(Poll, related_name="related_choice") - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/save_delete_hooks/models.py b/tests/modeltests/save_delete_hooks/models.py index bae60e4b86..a6e1abfb77 100644 --- a/tests/modeltests/save_delete_hooks/models.py +++ b/tests/modeltests/save_delete_hooks/models.py @@ -7,8 +7,10 @@ the methods. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) @@ -17,7 +19,7 @@ class Person(models.Model): super(Person, self).__init__(*args, **kwargs) self.data = [] - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) def save(self, *args, **kwargs): diff --git a/tests/modeltests/select_related/models.py b/tests/modeltests/select_related/models.py index 3c2e7721fd..ec41957adf 100644 --- a/tests/modeltests/select_related/models.py +++ b/tests/modeltests/select_related/models.py @@ -8,52 +8,61 @@ the select-related behavior will traverse. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible # Who remembers high school biology? +@python_2_unicode_compatible class Domain(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Kingdom(models.Model): name = models.CharField(max_length=50) domain = models.ForeignKey(Domain) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Phylum(models.Model): name = models.CharField(max_length=50) kingdom = models.ForeignKey(Kingdom) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Klass(models.Model): name = models.CharField(max_length=50) phylum = models.ForeignKey(Phylum) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Order(models.Model): name = models.CharField(max_length=50) klass = models.ForeignKey(Klass) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Family(models.Model): name = models.CharField(max_length=50) order = models.ForeignKey(Order) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Genus(models.Model): name = models.CharField(max_length=50) family = models.ForeignKey(Family) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Species(models.Model): name = models.CharField(max_length=50) genus = models.ForeignKey(Genus) - def __unicode__(self): - return self.name
\ No newline at end of file + def __str__(self): + return self.name diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py index 9da099c027..569f2cec49 100644 --- a/tests/modeltests/serializers/models.py +++ b/tests/modeltests/serializers/models.py @@ -11,28 +11,32 @@ from decimal import Decimal from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=20) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=20) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Article(models.Model): author = models.ForeignKey(Author) headline = models.CharField(max_length=50) @@ -42,28 +46,31 @@ class Article(models.Model): class Meta: ordering = ('pub_date',) - def __unicode__(self): + def __str__(self): return self.headline +@python_2_unicode_compatible class AuthorProfile(models.Model): author = models.OneToOneField(Author, primary_key=True) date_of_birth = models.DateField() - def __unicode__(self): + def __str__(self): return "Profile of %s" % self.author +@python_2_unicode_compatible class Actor(models.Model): name = models.CharField(max_length=20, primary_key=True) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Movie(models.Model): actor = models.ForeignKey(Actor) title = models.CharField(max_length=50) @@ -72,7 +79,7 @@ class Movie(models.Model): class Meta: ordering = ('title',) - def __unicode__(self): + def __str__(self): return self.title @@ -80,13 +87,11 @@ class Score(models.Model): score = models.FloatField() +@python_2_unicode_compatible class Team(object): def __init__(self, title): self.title = title - def __unicode__(self): - raise NotImplementedError("Not so simple") - def __str__(self): raise NotImplementedError("Not so simple") @@ -94,8 +99,7 @@ class Team(object): return "%s" % self.title -class TeamField(models.CharField): - __metaclass__ = models.SubfieldBase +class TeamField(six.with_metaclass(models.SubfieldBase, models.CharField)): def __init__(self): super(TeamField, self).__init__(max_length=100) @@ -112,10 +116,11 @@ class TeamField(models.CharField): return self._get_val_from_obj(obj).to_string() +@python_2_unicode_compatible class Player(models.Model): name = models.CharField(max_length=50) rank = models.IntegerField() team = TeamField() - def __unicode__(self): + def __str__(self): return '%s (%d) playing for %s' % (self.name, self.rank, self.team.to_string()) diff --git a/tests/modeltests/serializers/tests.py b/tests/modeltests/serializers/tests.py index ec3cc132db..e39f17b68a 100644 --- a/tests/modeltests/serializers/tests.py +++ b/tests/modeltests/serializers/tests.py @@ -114,8 +114,8 @@ class SerializersTestBase(object): Tests the ability to create new objects by modifying serialized content. """ - old_headline = b"Poker has no place on ESPN" - new_headline = b"Poker has no place on television" + old_headline = "Poker has no place on ESPN" + new_headline = "Poker has no place on television" serial_str = serializers.serialize(self.serializer_name, Article.objects.all()) serial_str = serial_str.replace(old_headline, new_headline) @@ -285,7 +285,7 @@ class SerializersTransactionTestBase(object): class XmlSerializerTestCase(SerializersTestBase, TestCase): serializer_name = "xml" - pkless_str = b"""<?xml version="1.0" encoding="utf-8"?> + pkless_str = """<?xml version="1.0" encoding="utf-8"?> <django-objects version="1.0"> <object model="serializers.category"> <field type="CharField" name="name">Reference</field> @@ -331,7 +331,7 @@ class XmlSerializerTestCase(SerializersTestBase, TestCase): class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): serializer_name = "xml" - fwd_ref_str = b"""<?xml version="1.0" encoding="utf-8"?> + fwd_ref_str = """<?xml version="1.0" encoding="utf-8"?> <django-objects version="1.0"> <object pk="1" model="serializers.article"> <field to="serializers.author" name="author" rel="ManyToOneRel">1</field> @@ -351,7 +351,7 @@ class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, Transacti class JsonSerializerTestCase(SerializersTestBase, TestCase): serializer_name = "json" - pkless_str = b"""[{"pk": null, "model": "serializers.category", "fields": {"name": "Reference"}}]""" + pkless_str = """[{"pk": null, "model": "serializers.category", "fields": {"name": "Reference"}}]""" @staticmethod def _validate_output(serial_str): @@ -381,7 +381,7 @@ class JsonSerializerTestCase(SerializersTestBase, TestCase): class JsonSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): serializer_name = "json" - fwd_ref_str = b"""[ + fwd_ref_str = """[ { "pk": 1, "model": "serializers.article", @@ -414,7 +414,7 @@ except ImportError: else: class YamlSerializerTestCase(SerializersTestBase, TestCase): serializer_name = "yaml" - fwd_ref_str = b"""- fields: + fwd_ref_str = """- fields: headline: Forward references pose no problem pub_date: 2006-06-16 15:00:00 categories: [1] @@ -430,7 +430,7 @@ else: pk: 1 model: serializers.author""" - pkless_str = b"""- fields: + pkless_str = """- fields: name: Reference pk: null model: serializers.category""" @@ -470,7 +470,7 @@ else: class YamlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): serializer_name = "yaml" - fwd_ref_str = b"""- fields: + fwd_ref_str = """- fields: headline: Forward references pose no problem pub_date: 2006-06-16 15:00:00 categories: [1] diff --git a/tests/modeltests/signals/models.py b/tests/modeltests/signals/models.py index 18ca467655..e54a22fce9 100644 --- a/tests/modeltests/signals/models.py +++ b/tests/modeltests/signals/models.py @@ -4,18 +4,21 @@ Testing signals before/after saving and deleting. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) +@python_2_unicode_compatible class Car(models.Model): make = models.CharField(max_length=20) model = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return "%s %s" % (self.make, self.model) diff --git a/tests/modeltests/str/models.py b/tests/modeltests/str/models.py index 6f6b877c88..488012e861 100644 --- a/tests/modeltests/str/models.py +++ b/tests/modeltests/str/models.py @@ -15,6 +15,7 @@ if you prefer. You must be careful to encode the results correctly, though. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Article(models.Model): @@ -26,9 +27,10 @@ class Article(models.Model): # in ASCII. return self.headline +@python_2_unicode_compatible class InternationalArticle(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() - def __unicode__(self): - return self.headline
\ No newline at end of file + def __str__(self): + return self.headline diff --git a/tests/modeltests/str/tests.py b/tests/modeltests/str/tests.py index bed9ea719b..2c11ac8c78 100644 --- a/tests/modeltests/str/tests.py +++ b/tests/modeltests/str/tests.py @@ -4,23 +4,32 @@ from __future__ import absolute_import, unicode_literals import datetime from django.test import TestCase +from django.utils import six +from django.utils.unittest import skipIf from .models import Article, InternationalArticle class SimpleTests(TestCase): + + @skipIf(six.PY3, "tests a __str__ method returning unicode under Python 2") def test_basic(self): a = Article.objects.create( headline=b'Area man programs in Python', pub_date=datetime.datetime(2005, 7, 28) ) - self.assertEqual(str(a), b'Area man programs in Python') - self.assertEqual(repr(a), b'<Article: Area man programs in Python>') + self.assertEqual(str(a), str('Area man programs in Python')) + self.assertEqual(repr(a), str('<Article: Area man programs in Python>')) def test_international(self): a = InternationalArticle.objects.create( headline='Girl wins €12.500 in lottery', pub_date=datetime.datetime(2005, 7, 28) ) - # The default str() output will be the UTF-8 encoded output of __unicode__(). - self.assertEqual(str(a), b'Girl wins \xe2\x82\xac12.500 in lottery') + if six.PY3: + self.assertEqual(str(a), 'Girl wins €12.500 in lottery') + else: + # On Python 2, the default str() output will be the UTF-8 encoded + # output of __unicode__() -- or __str__() when the + # python_2_unicode_compatible decorator is used. + self.assertEqual(str(a), b'Girl wins \xe2\x82\xac12.500 in lottery') diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 399d2906a8..1d9c999f21 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -91,7 +91,7 @@ class ClientTest(TestCase): content_type="text/xml") self.assertEqual(response.status_code, 200) self.assertEqual(response.templates[0].name, "Book template") - self.assertEqual(response.content, "Blink - Malcolm Gladwell") + self.assertEqual(response.content, b"Blink - Malcolm Gladwell") def test_redirect(self): "GET a URL that redirects elsewhere" diff --git a/tests/modeltests/timezones/tests.py b/tests/modeltests/timezones/tests.py index a38e4b3f75..4621f0a60c 100644 --- a/tests/modeltests/timezones/tests.py +++ b/tests/modeltests/timezones/tests.py @@ -467,6 +467,7 @@ class NewDatabaseTests(TestCase): [event], transform=lambda d: d) + @requires_tz_support def test_filter_date_field_with_aware_datetime(self): # Regression test for #17742 day = datetime.date(2011, 9, 1) diff --git a/tests/modeltests/transactions/models.py b/tests/modeltests/transactions/models.py index 7a51b9ac1c..0f8d6b16ec 100644 --- a/tests/modeltests/transactions/models.py +++ b/tests/modeltests/transactions/models.py @@ -9,8 +9,10 @@ manually. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) @@ -19,5 +21,5 @@ class Reporter(models.Model): class Meta: ordering = ('first_name', 'last_name') - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) diff --git a/tests/modeltests/unmanaged_models/models.py b/tests/modeltests/unmanaged_models/models.py index 00303cf17a..0eef69977c 100644 --- a/tests/modeltests/unmanaged_models/models.py +++ b/tests/modeltests/unmanaged_models/models.py @@ -4,9 +4,11 @@ is generated for the table on various manage.py operations. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible # All of these models are created in the database by Django. +@python_2_unicode_compatible class A01(models.Model): f_a = models.CharField(max_length=10, db_index=True) f_b = models.IntegerField() @@ -14,9 +16,10 @@ class A01(models.Model): class Meta: db_table = 'a01' - def __unicode__(self): + def __str__(self): return self.f_a +@python_2_unicode_compatible class B01(models.Model): fk_a = models.ForeignKey(A01) f_a = models.CharField(max_length=10, db_index=True) @@ -27,9 +30,10 @@ class B01(models.Model): # 'managed' is True by default. This tests we can set it explicitly. managed = True - def __unicode__(self): + def __str__(self): return self.f_a +@python_2_unicode_compatible class C01(models.Model): mm_a = models.ManyToManyField(A01, db_table='d01') f_a = models.CharField(max_length=10, db_index=True) @@ -38,13 +42,14 @@ class C01(models.Model): class Meta: db_table = 'c01' - def __unicode__(self): + def __str__(self): return self.f_a # All of these models use the same tables as the previous set (they are shadows # of possibly a subset of the columns). There should be no creation errors, # since we have told Django they aren't managed by Django. +@python_2_unicode_compatible class A02(models.Model): f_a = models.CharField(max_length=10, db_index=True) @@ -52,9 +57,10 @@ class A02(models.Model): db_table = 'a01' managed = False - def __unicode__(self): + def __str__(self): return self.f_a +@python_2_unicode_compatible class B02(models.Model): class Meta: db_table = 'b01' @@ -64,11 +70,12 @@ class B02(models.Model): f_a = models.CharField(max_length=10, db_index=True) f_b = models.IntegerField() - def __unicode__(self): + def __str__(self): return self.f_a # To re-use the many-to-many intermediate table, we need to manually set up # things up. +@python_2_unicode_compatible class C02(models.Model): mm_a = models.ManyToManyField(A02, through="Intermediate") f_a = models.CharField(max_length=10, db_index=True) @@ -78,7 +85,7 @@ class C02(models.Model): db_table = 'c01' managed = False - def __unicode__(self): + def __str__(self): return self.f_a class Intermediate(models.Model): diff --git a/tests/modeltests/update/models.py b/tests/modeltests/update/models.py index b93e4a7aae..08472d98b1 100644 --- a/tests/modeltests/update/models.py +++ b/tests/modeltests/update/models.py @@ -5,21 +5,24 @@ updates. from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class DataPoint(models.Model): name = models.CharField(max_length=20) value = models.CharField(max_length=20) another_value = models.CharField(max_length=20, blank=True) - def __unicode__(self): + def __str__(self): return six.text_type(self.name) +@python_2_unicode_compatible class RelatedPoint(models.Model): name = models.CharField(max_length=20) data = models.ForeignKey(DataPoint) - def __unicode__(self): + def __str__(self): return six.text_type(self.name) diff --git a/tests/modeltests/update_only_fields/models.py b/tests/modeltests/update_only_fields/models.py index 968dba9916..bf5dd99166 100644 --- a/tests/modeltests/update_only_fields/models.py +++ b/tests/modeltests/update_only_fields/models.py @@ -1,5 +1,6 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible GENDER_CHOICES = ( ('M', 'Male'), @@ -10,11 +11,13 @@ class Account(models.Model): num = models.IntegerField() +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=20) gender = models.CharField(max_length=1, choices=GENDER_CHOICES) + pid = models.IntegerField(null=True, default=None) - def __unicode__(self): + def __str__(self): return self.name @@ -24,11 +27,12 @@ class Employee(Person): accounts = models.ManyToManyField('Account', related_name='employees', blank=True, null=True) +@python_2_unicode_compatible class Profile(models.Model): name = models.CharField(max_length=200) salary = models.FloatField(default=1000.0) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/modeltests/update_only_fields/tests.py b/tests/modeltests/update_only_fields/tests.py index bce53ca621..c904c97236 100644 --- a/tests/modeltests/update_only_fields/tests.py +++ b/tests/modeltests/update_only_fields/tests.py @@ -18,6 +18,107 @@ class UpdateOnlyFieldsTests(TestCase): self.assertEqual(s.gender, 'F') self.assertEqual(s.name, 'Ian') + def test_update_fields_deferred(self): + s = Person.objects.create(name='Sara', gender='F', pid=22) + self.assertEqual(s.gender, 'F') + + s1 = Person.objects.defer("gender", "pid").get(pk=s.pk) + s1.name = "Emily" + s1.gender = "M" + + with self.assertNumQueries(1): + s1.save() + + s2 = Person.objects.get(pk=s1.pk) + self.assertEqual(s2.name, "Emily") + self.assertEqual(s2.gender, "M") + + def test_update_fields_only_1(self): + s = Person.objects.create(name='Sara', gender='F') + self.assertEqual(s.gender, 'F') + + s1 = Person.objects.only('name').get(pk=s.pk) + s1.name = "Emily" + s1.gender = "M" + + with self.assertNumQueries(1): + s1.save() + + s2 = Person.objects.get(pk=s1.pk) + self.assertEqual(s2.name, "Emily") + self.assertEqual(s2.gender, "M") + + def test_update_fields_only_2(self): + s = Person.objects.create(name='Sara', gender='F', pid=22) + self.assertEqual(s.gender, 'F') + + s1 = Person.objects.only('name').get(pk=s.pk) + s1.name = "Emily" + s1.gender = "M" + + with self.assertNumQueries(2): + s1.save(update_fields=['pid']) + + s2 = Person.objects.get(pk=s1.pk) + self.assertEqual(s2.name, "Sara") + self.assertEqual(s2.gender, "F") + + def test_update_fields_only_repeated(self): + s = Person.objects.create(name='Sara', gender='F') + self.assertEqual(s.gender, 'F') + + s1 = Person.objects.only('name').get(pk=s.pk) + s1.gender = 'M' + with self.assertNumQueries(1): + s1.save() + # Test that the deferred class does not remember that gender was + # set, instead the instace should remember this. + s1 = Person.objects.only('name').get(pk=s.pk) + with self.assertNumQueries(1): + s1.save() + + def test_update_fields_inheritance_defer(self): + profile_boss = Profile.objects.create(name='Boss', salary=3000) + e1 = Employee.objects.create(name='Sara', gender='F', + employee_num=1, profile=profile_boss) + e1 = Employee.objects.only('name').get(pk=e1.pk) + e1.name = 'Linda' + with self.assertNumQueries(1): + e1.save() + self.assertEqual(Employee.objects.get(pk=e1.pk).name, + 'Linda') + + def test_update_fields_fk_defer(self): + profile_boss = Profile.objects.create(name='Boss', salary=3000) + profile_receptionist = Profile.objects.create(name='Receptionist', salary=1000) + e1 = Employee.objects.create(name='Sara', gender='F', + employee_num=1, profile=profile_boss) + e1 = Employee.objects.only('profile').get(pk=e1.pk) + e1.profile = profile_receptionist + with self.assertNumQueries(1): + e1.save() + self.assertEqual(Employee.objects.get(pk=e1.pk).profile, profile_receptionist) + e1.profile_id = profile_boss.pk + with self.assertNumQueries(1): + e1.save() + self.assertEqual(Employee.objects.get(pk=e1.pk).profile, profile_boss) + + def test_select_related_only_interaction(self): + profile_boss = Profile.objects.create(name='Boss', salary=3000) + e1 = Employee.objects.create(name='Sara', gender='F', + employee_num=1, profile=profile_boss) + e1 = Employee.objects.only('profile__salary').select_related('profile').get(pk=e1.pk) + profile_boss.name = 'Clerk' + profile_boss.salary = 1000 + profile_boss.save() + # The loaded salary of 3000 gets saved, the name of 'Clerk' isn't + # overwritten. + with self.assertNumQueries(1): + e1.profile.save() + reloaded_profile = Profile.objects.get(pk=profile_boss.pk) + self.assertEqual(reloaded_profile.name, profile_boss.name) + self.assertEqual(reloaded_profile.salary, 3000) + def test_update_fields_m2m(self): profile_boss = Profile.objects.create(name='Boss', salary=3000) e1 = Employee.objects.create(name='Sara', gender='F', diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py index 26fff4b863..db083290fb 100644 --- a/tests/modeltests/validation/models.py +++ b/tests/modeltests/validation/models.py @@ -4,6 +4,7 @@ from datetime import datetime from django.core.exceptions import ValidationError from django.db import models +from django.utils.encoding import python_2_unicode_compatible def validate_answer_to_universe(value): @@ -66,13 +67,14 @@ class Article(models.Model): if self.pub_date is None: self.pub_date = datetime.now() +@python_2_unicode_compatible class Post(models.Model): title = models.CharField(max_length=50, unique_for_date='posted', blank=True) slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) posted = models.DateField() - def __unicode__(self): + def __str__(self): return self.name class FlexibleDatePost(models.Model): @@ -89,6 +91,8 @@ class GenericIPAddressTestModel(models.Model): generic_ip = models.GenericIPAddressField(blank=True, null=True, unique=True) v4_ip = models.GenericIPAddressField(blank=True, null=True, protocol="ipv4") v6_ip = models.GenericIPAddressField(blank=True, null=True, protocol="ipv6") + ip_verbose_name = models.GenericIPAddressField("IP Address Verbose", + blank=True, null=True) class GenericIPAddrUnpackUniqueTest(models.Model): generic_v4unpack_ip = models.GenericIPAddressField(blank=True, unique=True, unpack_ipv4=True) diff --git a/tests/modeltests/validation/tests.py b/tests/modeltests/validation/tests.py index 4f658203df..58b7b94e7f 100644 --- a/tests/modeltests/validation/tests.py +++ b/tests/modeltests/validation/tests.py @@ -79,7 +79,7 @@ class ModelFormsTests(TestCase): 'pub_date': '2010-1-10 14:49:00' } form = ArticleForm(data) - self.assertEqual(form.errors.keys(), []) + self.assertEqual(list(form.errors), []) article = form.save(commit=False) article.author = self.author article.save() @@ -95,7 +95,7 @@ class ModelFormsTests(TestCase): } article = Article(author_id=self.author.id) form = ArticleForm(data, instance=article) - self.assertEqual(form.errors.keys(), []) + self.assertEqual(list(form.errors), []) self.assertNotEqual(form.instance.pub_date, None) article = form.save() @@ -108,7 +108,7 @@ class ModelFormsTests(TestCase): } article = Article(author_id=self.author.id) form = ArticleForm(data, instance=article) - self.assertEqual(form.errors.keys(), ['pub_date']) + self.assertEqual(list(form.errors), ['pub_date']) class GenericIPAddressFieldTests(ValidationTestCase): diff --git a/tests/regressiontests/admin_changelist/models.py b/tests/regressiontests/admin_changelist/models.py index 487db50689..4ba2f9c503 100644 --- a/tests/regressiontests/admin_changelist/models.py +++ b/tests/regressiontests/admin_changelist/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Event(models.Model): # Oracle can have problems with a column named "date" @@ -20,17 +21,19 @@ class Band(models.Model): nr_of_members = models.PositiveIntegerField() genres = models.ManyToManyField(Genre) +@python_2_unicode_compatible class Musician(models.Model): name = models.CharField(max_length=30) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Group(models.Model): name = models.CharField(max_length=30) members = models.ManyToManyField(Musician, through='Membership') - def __unicode__(self): + def __str__(self): return self.name class Membership(models.Model): diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index 1ed963aaf2..be88c9a161 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -317,7 +317,7 @@ class ChangeListTests(TestCase): m.list_editable, m) self.assertEqual(cl.query_set.count(), 60) self.assertEqual(cl.paginator.count, 60) - self.assertEqual(cl.paginator.page_range, [1, 2, 3, 4, 5, 6]) + self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6]) # Test custom queryset m = FilteredChildAdmin(Child, admin.site) @@ -327,7 +327,7 @@ class ChangeListTests(TestCase): m.list_editable, m) self.assertEqual(cl.query_set.count(), 30) self.assertEqual(cl.paginator.count, 30) - self.assertEqual(cl.paginator.page_range, [1, 2, 3]) + self.assertEqual(list(cl.paginator.page_range), [1, 2, 3]) def test_computed_list_display_localization(self): """ diff --git a/tests/regressiontests/admin_custom_urls/fixtures/actions.json b/tests/regressiontests/admin_custom_urls/fixtures/actions.json index d803393a12..a63cf8135c 100644 --- a/tests/regressiontests/admin_custom_urls/fixtures/actions.json +++ b/tests/regressiontests/admin_custom_urls/fixtures/actions.json @@ -40,5 +40,12 @@ "fields": { "description": "An action with a name suspected of being a XSS attempt" } + }, + { + "pk": "The name of an action", + "model": "admin_custom_urls.action", + "fields": { + "description": "A generic action" + } } -]
\ No newline at end of file +] diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py index facb4b52e7..a5b4983b09 100644 --- a/tests/regressiontests/admin_custom_urls/models.py +++ b/tests/regressiontests/admin_custom_urls/models.py @@ -2,13 +2,15 @@ from functools import update_wrapper from django.contrib import admin from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Action(models.Model): name = models.CharField(max_length=50, primary_key=True) description = models.CharField(max_length=70) - def __unicode__(self): + def __str__(self): return self.name @@ -26,7 +28,7 @@ class ActionAdmin(admin.ModelAdmin): Remove all entries named 'name' from the ModelAdmin instance URL patterns list """ - return filter(lambda e: e.name != name, super(ActionAdmin, self).get_urls()) + return [url for url in super(ActionAdmin, self).get_urls() if url.name != name] def get_urls(self): # Add the URL of our custom 'add_view' view to the front of the URLs diff --git a/tests/regressiontests/admin_custom_urls/tests.py b/tests/regressiontests/admin_custom_urls/tests.py index 4a6235e7d5..64ff9f6692 100644 --- a/tests/regressiontests/admin_custom_urls/tests.py +++ b/tests/regressiontests/admin_custom_urls/tests.py @@ -10,6 +10,12 @@ from .models import Action @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomUrlsTest(TestCase): + """ + Remember that: + * The Action model has a CharField PK. + * The ModelAdmin for Action customizes the add_view URL, it's + '<app name>/<model name>/!add/' + """ fixtures = ['users.json', 'actions.json'] def setUp(self): @@ -20,23 +26,24 @@ class AdminCustomUrlsTest(TestCase): def testBasicAddGet(self): """ - A smoke test to ensure GET on the add_view works. + Ensure GET on the add_view works. """ response = self.client.get('/custom_urls/admin/admin_custom_urls/action/!add/') self.assertIsInstance(response, TemplateResponse) self.assertEqual(response.status_code, 200) def testAddWithGETArgs(self): + """ + Ensure GET on the add_view plus specifying a field value in the query + string works. + """ response = self.client.get('/custom_urls/admin/admin_custom_urls/action/!add/', {'name': 'My Action'}) self.assertEqual(response.status_code, 200) - self.assertTrue( - 'value="My Action"' in response.content, - "Couldn't find an input with the right value in the response." - ) + self.assertContains(response, 'value="My Action"') def testBasicAddPost(self): """ - A smoke test to ensure POST on add_view works. + Ensure POST on add_view works. """ post_data = { '_popup': '1', @@ -50,8 +57,7 @@ class AdminCustomUrlsTest(TestCase): def testAdminUrlsNoClash(self): """ - Test that some admin URLs work correctly. The model has a CharField - PK and the add_view URL has been customized. + Test that some admin URLs work correctly. """ # Should get the change_view for model instance with PK 'add', not show # the add_view @@ -60,17 +66,28 @@ class AdminCustomUrlsTest(TestCase): self.assertContains(response, 'Change action') # Ditto, but use reverse() to build the URL - path = reverse('admin:%s_action_change' % Action._meta.app_label, + url = reverse('admin:%s_action_change' % Action._meta.app_label, args=('add',)) - response = self.client.get(path) + response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Change action') # Should correctly get the change_view for the model instance with the - # funny-looking PK - path = reverse('admin:%s_action_change' % Action._meta.app_label, + # funny-looking PK (the one wth a 'path/to/html/document.html' value) + url = reverse('admin:%s_action_change' % Action._meta.app_label, args=("path/to/html/document.html",)) - response = self.client.get(path) + response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Change action') self.assertContains(response, 'value="path/to/html/document.html"') + + def testChangeViewHistoryButton(self): + url = reverse('admin:%s_action_change' % Action._meta.app_label, + args=('The name of an action',)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + expected_link = reverse('admin:%s_action_history' % + Action._meta.app_label, + args=('The name of an action',)) + self.assertContains(response, '<a href="%s" class="historylink"' % + expected_link) diff --git a/tests/regressiontests/admin_filters/models.py b/tests/regressiontests/admin_filters/models.py index 371c67061f..e0b8bde2de 100644 --- a/tests/regressiontests/admin_filters/models.py +++ b/tests/regressiontests/admin_filters/models.py @@ -2,8 +2,10 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) year = models.PositiveIntegerField(null=True, blank=True) @@ -13,20 +15,22 @@ class Book(models.Model): date_registered = models.DateField(null=True) no = models.IntegerField(verbose_name='number', blank=True, null=True) # This field is intentionally 2 characters long. See #16080. - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Department(models.Model): code = models.CharField(max_length=4, unique=True) description = models.CharField(max_length=50, blank=True, null=True) - def __unicode__(self): + def __str__(self): return self.description +@python_2_unicode_compatible class Employee(models.Model): department = models.ForeignKey(Department, to_field="code") name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py index 5a0f4d84b9..5b703a7481 100644 --- a/tests/regressiontests/admin_inlines/models.py +++ b/tests/regressiontests/admin_inlines/models.py @@ -7,22 +7,26 @@ from __future__ import unicode_literals from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Parent(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Teacher(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Child(models.Model): name = models.CharField(max_length=50) teacher = models.ForeignKey(Teacher) @@ -31,7 +35,7 @@ class Child(models.Model): object_id = models.PositiveIntegerField() parent = generic.GenericForeignKey() - def __unicode__(self): + def __str__(self): return 'I am %s, a child of %s' % (self.name, self.parent) diff --git a/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-18091-non-ascii-template.txt b/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-18091-non-ascii-template.txt new file mode 100644 index 0000000000..873eaded88 --- /dev/null +++ b/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-18091-non-ascii-template.txt @@ -0,0 +1,2 @@ +Some non-ASCII text for testing ticket #18091: +üäö € diff --git a/tests/regressiontests/admin_scripts/models.py b/tests/regressiontests/admin_scripts/models.py index da803d0174..cd96052098 100644 --- a/tests/regressiontests/admin_scripts/models.py +++ b/tests/regressiontests/admin_scripts/models.py @@ -1,11 +1,13 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() - def __unicode__(self): + def __str__(self): return self.headline class Meta: diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 546fa7d79c..2710f80929 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -1,8 +1,10 @@ +# -*- coding: utf-8 -*- """ A series of tests to establish that the command-line managment tools work as advertised - especially with regards to the handling of the DJANGO_SETTINGS_MODULE and default settings.py files. """ +from __future__ import unicode_literals import os import re @@ -10,6 +12,7 @@ import shutil import socket import subprocess import sys +import codecs from django import conf, bin, get_version from django.conf import settings @@ -71,6 +74,10 @@ class AdminScriptTestCase(unittest.TestCase): os.remove(full_name + 'c') except OSError: pass + # Also remove a __pycache__ directory, if it exists + cache_name = os.path.join(test_dir, '__pycache__') + if os.path.isdir(cache_name): + shutil.rmtree(cache_name) def _ext_backend_paths(self): """ @@ -110,14 +117,11 @@ class AdminScriptTestCase(unittest.TestCase): python_path.extend(ext_backend_base_dirs) os.environ[python_path_var_name] = os.pathsep.join(python_path) - # Silence the DeprecationWarning caused by having a locale directory - # in the project directory. - cmd = [sys.executable, '-Wignore:::django.utils.translation', script] - # Move to the test directory and run os.chdir(test_dir) - out, err = subprocess.Popen(cmd + args, - stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() + out, err = subprocess.Popen([sys.executable, script] + args, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True).communicate() # Restore the old environment if old_django_settings_module: @@ -1572,3 +1576,17 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase): self.assertOutput(err, "Destination directory '%s' does not exist, please create it first." % testproject_dir) self.assertFalse(os.path.exists(testproject_dir)) + + def test_custom_project_template_with_non_ascii_templates(self): + "Ticket 18091: Make sure the startproject management command is able to render templates with non-ASCII content" + template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template') + args = ['startproject', '--template', template_path, '--extension=txt', 'customtestproject'] + testproject_dir = os.path.join(test_dir, 'customtestproject') + + out, err = self.run_django_admin(args) + self.addCleanup(shutil.rmtree, testproject_dir) + self.assertNoOutput(err) + self.assertTrue(os.path.isdir(testproject_dir)) + path = os.path.join(testproject_dir, 'ticket-18091-non-ascii-template.txt') + self.assertEqual(codecs.open(path, 'r', 'utf-8').read(), + 'Some non-ASCII text for testing ticket #18091:\nüäö €\n') diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py index 5541097022..b3504a1fa4 100644 --- a/tests/regressiontests/admin_util/models.py +++ b/tests/regressiontests/admin_util/models.py @@ -1,5 +1,6 @@ from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class Article(models.Model): @@ -18,11 +19,12 @@ class Article(models.Model): return "nothing" test_from_model_with_override.short_description = "not What you Expect" +@python_2_unicode_compatible class Count(models.Model): num = models.PositiveSmallIntegerField() parent = models.ForeignKey('self', null=True) - def __unicode__(self): + def __str__(self): return six.text_type(self.num) class Event(models.Model): diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index 6b6dad4336..d04740ce95 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -171,7 +171,7 @@ class UtilTests(unittest.TestCase): ) self.assertEqual( label_for_field("__str__", Article), - b"article" + str("article") ) self.assertRaises( diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py index 5e080a9232..d23849a2a8 100644 --- a/tests/regressiontests/admin_validation/models.py +++ b/tests/regressiontests/admin_validation/models.py @@ -3,12 +3,14 @@ Tests of ModelAdmin validation logic. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Album(models.Model): title = models.CharField(max_length=150) +@python_2_unicode_compatible class Song(models.Model): title = models.CharField(max_length=150) album = models.ForeignKey(Album) @@ -17,7 +19,7 @@ class Song(models.Model): class Meta: ordering = ('title',) - def __unicode__(self): + def __str__(self): return self.title def readonly_method_on_model(self): diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index aee193b572..0d5e327ecf 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -10,6 +10,7 @@ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.core.files.storage import FileSystemStorage from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Section(models.Model): @@ -20,6 +21,7 @@ class Section(models.Model): name = models.CharField(max_length=100) +@python_2_unicode_compatible class Article(models.Model): """ A simple article to test admin views. Test backwards compatibility. @@ -29,7 +31,7 @@ class Article(models.Model): date = models.DateTimeField() section = models.ForeignKey(Section, null=True, blank=True) - def __unicode__(self): + def __str__(self): return self.title def model_year(self): @@ -38,30 +40,33 @@ class Article(models.Model): model_year.short_description = '' +@python_2_unicode_compatible class Book(models.Model): """ A simple book that has chapters. """ name = models.CharField(max_length=100, verbose_name='¿Name?') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Promo(models.Model): name = models.CharField(max_length=100, verbose_name='¿Name?') book = models.ForeignKey(Book) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Chapter(models.Model): title = models.CharField(max_length=100, verbose_name='¿Title?') content = models.TextField() book = models.ForeignKey(Book) - def __unicode__(self): + def __str__(self): return self.title class Meta: @@ -69,19 +74,21 @@ class Chapter(models.Model): verbose_name = '¿Chapter?' +@python_2_unicode_compatible class ChapterXtra1(models.Model): chap = models.OneToOneField(Chapter, verbose_name='¿Chap?') xtra = models.CharField(max_length=100, verbose_name='¿Xtra?') - def __unicode__(self): + def __str__(self): return '¿Xtra1: %s' % self.xtra +@python_2_unicode_compatible class ChapterXtra2(models.Model): chap = models.OneToOneField(Chapter, verbose_name='¿Chap?') xtra = models.CharField(max_length=100, verbose_name='¿Xtra?') - def __unicode__(self): + def __str__(self): return '¿Xtra2: %s' % self.xtra @@ -94,20 +101,22 @@ class CustomArticle(models.Model): date = models.DateTimeField() +@python_2_unicode_compatible class ModelWithStringPrimaryKey(models.Model): string_pk = models.CharField(max_length=255, primary_key=True) - def __unicode__(self): + def __str__(self): return self.string_pk def get_absolute_url(self): return '/dummy/%s/' % self.string_pk +@python_2_unicode_compatible class Color(models.Model): value = models.CharField(max_length=10) warm = models.BooleanField() - def __unicode__(self): + def __str__(self): return self.value # we replicate Color to register with another ModelAdmin @@ -115,29 +124,33 @@ class Color2(Color): class Meta: proxy = True +@python_2_unicode_compatible class Thing(models.Model): title = models.CharField(max_length=20) color = models.ForeignKey(Color, limit_choices_to={'warm': True}) - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Actor(models.Model): name = models.CharField(max_length=50) age = models.IntegerField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Inquisition(models.Model): expected = models.BooleanField() leader = models.ForeignKey(Actor) country = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return "by %s from %s" % (self.leader, self.country) +@python_2_unicode_compatible class Sketch(models.Model): title = models.CharField(max_length=100) inquisition = models.ForeignKey(Inquisition, limit_choices_to={'leader__name': 'Palin', @@ -145,7 +158,7 @@ class Sketch(models.Model): 'expected': False, }) - def __unicode__(self): + def __str__(self): return self.title @@ -161,6 +174,7 @@ class Fabric(models.Model): surface = models.CharField(max_length=20, choices=NG_CHOICES) +@python_2_unicode_compatible class Person(models.Model): GENDER_CHOICES = ( (1, "Male"), @@ -171,20 +185,22 @@ class Person(models.Model): age = models.IntegerField(default=21) alive = models.BooleanField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Persona(models.Model): """ A simple persona associated with accounts, to test inlining of related accounts which inherit from a common accounts class. """ name = models.CharField(blank=False, max_length=80) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Account(models.Model): """ A simple, generic account encapsulating the information shared by all @@ -194,7 +210,7 @@ class Account(models.Model): persona = models.ForeignKey(Persona, related_name="accounts") servicename = 'generic service' - def __unicode__(self): + def __str__(self): return "%s: %s" % (self.servicename, self.username) @@ -208,11 +224,12 @@ class BarAccount(Account): servicename = 'bar' +@python_2_unicode_compatible class Subscriber(models.Model): name = models.CharField(blank=False, max_length=80) email = models.EmailField(blank=False, max_length=175) - def __unicode__(self): + def __str__(self): return "%s (%s)" % (self.name, self.email) @@ -249,8 +266,9 @@ class Child(models.Model): name = models.CharField(max_length=30, blank=True) +@python_2_unicode_compatible class EmptyModel(models.Model): - def __unicode__(self): + def __str__(self): return "Primary key = %s" % self.id @@ -332,6 +350,7 @@ class FancyDoodad(Doodad): expensive = models.BooleanField(default=True) +@python_2_unicode_compatible class Category(models.Model): collector = models.ForeignKey(Collector) order = models.PositiveIntegerField() @@ -339,7 +358,7 @@ class Category(models.Model): class Meta: ordering = ('order',) - def __unicode__(self): + def __str__(self): return '%s:o%s' % (self.id, self.order) @@ -376,17 +395,19 @@ class Post(models.Model): return "Very awesome." +@python_2_unicode_compatible class Gadget(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Villain(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name @@ -394,6 +415,7 @@ class SuperVillain(Villain): pass +@python_2_unicode_compatible class FunkyTag(models.Model): "Because we all know there's only one real use case for GFKs." name = models.CharField(max_length=25) @@ -401,59 +423,65 @@ class FunkyTag(models.Model): object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey('content_type', 'object_id') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Plot(models.Model): name = models.CharField(max_length=100) team_leader = models.ForeignKey(Villain, related_name='lead_plots') contact = models.ForeignKey(Villain, related_name='contact_plots') tags = generic.GenericRelation(FunkyTag) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class PlotDetails(models.Model): details = models.CharField(max_length=100) plot = models.OneToOneField(Plot) - def __unicode__(self): + def __str__(self): return self.details +@python_2_unicode_compatible class SecretHideout(models.Model): """ Secret! Not registered with the admin! """ location = models.CharField(max_length=100) villain = models.ForeignKey(Villain) - def __unicode__(self): + def __str__(self): return self.location +@python_2_unicode_compatible class SuperSecretHideout(models.Model): """ Secret! Not registered with the admin! """ location = models.CharField(max_length=100) supervillain = models.ForeignKey(SuperVillain) - def __unicode__(self): + def __str__(self): return self.location +@python_2_unicode_compatible class CyclicOne(models.Model): name = models.CharField(max_length=25) two = models.ForeignKey('CyclicTwo') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class CyclicTwo(models.Model): name = models.CharField(max_length=25) one = models.ForeignKey(CyclicOne) - def __unicode__(self): + def __str__(self): return self.name @@ -484,11 +512,12 @@ class Question(models.Model): question = models.CharField(max_length=20) +@python_2_unicode_compatible class Answer(models.Model): question = models.ForeignKey(Question, on_delete=models.PROTECT) answer = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return self.answer @@ -523,11 +552,12 @@ class Paper(models.Model): author = models.CharField(max_length=30, blank=True, null=True) +@python_2_unicode_compatible class CoverLetter(models.Model): author = models.CharField(max_length=30) date_written = models.DateField(null=True, blank=True) - def __unicode__(self): + def __str__(self): return self.author @@ -575,10 +605,11 @@ class AdminOrderedCallable(models.Model): order = models.IntegerField() stuff = models.CharField(max_length=200) +@python_2_unicode_compatible class Report(models.Model): title = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.title diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 09be55baba..cf7d4855fb 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -30,7 +30,7 @@ from django.template.response import TemplateResponse from django.test import TestCase from django.utils import formats, translation, unittest from django.utils.cache import get_max_age -from django.utils.encoding import iri_to_uri +from django.utils.encoding import iri_to_uri, force_bytes from django.utils.html import escape from django.utils.http import urlencode from django.utils import six @@ -78,12 +78,22 @@ class AdminViewBasicTest(TestCase): self.client.logout() formats.reset_format_cache() + def assertContentBefore(self, response, text1, text2, failing_msg=None): + """ + Testing utility asserting that text1 appears before text2 in response + content. + """ + self.assertEqual(response.status_code, 200) + self.assertTrue(response.content.index(force_bytes(text1)) < response.content.index(force_bytes(text2)), + failing_msg + ) + def testTrailingSlashRequired(self): """ If you leave off the trailing slash, app should redirect and add it. """ - request = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit) - self.assertRedirects(request, + response = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit) + self.assertRedirects(response, '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301 ) @@ -219,12 +229,10 @@ class AdminViewBasicTest(TestCase): (column 2 is callable_year in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': 2}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index('Oldest content') < response.content.index('Middle content') and - response.content.index('Middle content') < response.content.index('Newest content'), - "Results of sorting on callable are out of order." - ) + self.assertContentBefore(response, 'Oldest content', 'Middle content', + "Results of sorting on callable are out of order.") + self.assertContentBefore(response, 'Middle content', 'Newest content', + "Results of sorting on callable are out of order.") def testChangeListSortingModel(self): """ @@ -232,12 +240,10 @@ class AdminViewBasicTest(TestCase): (colunn 3 is 'model_year' in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '-3'}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index('Newest content') < response.content.index('Middle content') and - response.content.index('Middle content') < response.content.index('Oldest content'), - "Results of sorting on Model method are out of order." - ) + self.assertContentBefore(response, 'Newest content', 'Middle content', + "Results of sorting on Model method are out of order.") + self.assertContentBefore(response, 'Middle content', 'Oldest content', + "Results of sorting on Model method are out of order.") def testChangeListSortingModelAdmin(self): """ @@ -245,12 +251,10 @@ class AdminViewBasicTest(TestCase): (colunn 4 is 'modeladmin_year' in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'o': '4'}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index('Oldest content') < response.content.index('Middle content') and - response.content.index('Middle content') < response.content.index('Newest content'), - "Results of sorting on ModelAdmin method are out of order." - ) + self.assertContentBefore(response, 'Oldest content', 'Middle content', + "Results of sorting on ModelAdmin method are out of order.") + self.assertContentBefore(response, 'Middle content', 'Newest content', + "Results of sorting on ModelAdmin method are out of order.") def testChangeListSortingMultiple(self): p1 = Person.objects.create(name="Chris", gender=1, alive=True) @@ -262,19 +266,13 @@ class AdminViewBasicTest(TestCase): # This hard-codes the URL because it'll fail if it runs against the # 'admin2' custom admin (which doesn't have the Person model). response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '1.2'}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % p3.id) < response.content.index(link % p1.id) and - response.content.index(link % p1.id) < response.content.index(link % p2.id) - ) + self.assertContentBefore(response, link % p3.id, link % p1.id) + self.assertContentBefore(response, link % p1.id, link % p2.id) # Sort by gender descending, name response = self.client.get('/test_admin/admin/admin_views/person/', {'o': '-2.1'}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % p2.id) < response.content.index(link % p3.id) and - response.content.index(link % p3.id) < response.content.index(link % p1.id) - ) + self.assertContentBefore(response, link % p2.id, link % p3.id) + self.assertContentBefore(response, link % p3.id, link % p1.id) def testChangeListSortingPreserveQuerySetOrdering(self): """ @@ -291,11 +289,8 @@ class AdminViewBasicTest(TestCase): # This hard-codes the URL because it'll fail if it runs against the # 'admin2' custom admin (which doesn't have the Person model). response = self.client.get('/test_admin/admin/admin_views/person/', {}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % p3.id) < response.content.index(link % p2.id) and - response.content.index(link % p2.id) < response.content.index(link % p1.id) - ) + self.assertContentBefore(response, link % p3.id, link % p2.id) + self.assertContentBefore(response, link % p2.id, link % p1.id) def testChangeListSortingModelMeta(self): # Test ordering on Model Meta is respected @@ -305,17 +300,11 @@ class AdminViewBasicTest(TestCase): link = '<a href="%s/' response = self.client.get('/test_admin/admin/admin_views/language/', {}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % l2.pk) < response.content.index(link % l1.pk) - ) + self.assertContentBefore(response, link % l2.pk, link % l1.pk) # Test we can override with query string response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % l1.pk) < response.content.index(link % l2.pk) - ) + self.assertContentBefore(response, link % l1.pk, link % l2.pk) def testChangeListSortingOverrideModelAdmin(self): # Test ordering on Model Admin is respected, and overrides Model Meta @@ -325,10 +314,7 @@ class AdminViewBasicTest(TestCase): link = '<a href="%s/' response = self.client.get('/test_admin/admin/admin_views/podcast/', {}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % p1.pk) < response.content.index(link % p2.pk) - ) + self.assertContentBefore(response, link % p1.pk, link % p2.pk) def testMultipleSortSameField(self): # Check that we get the columns we expect if we have two columns @@ -339,17 +325,13 @@ class AdminViewBasicTest(TestCase): link = '<a href="%s/' response = self.client.get('/test_admin/admin/admin_views/podcast/', {}) - self.assertEqual(response.status_code, 200) - self.assertTrue( - response.content.index(link % p1.pk) < response.content.index(link % p2.pk) - ) + self.assertContentBefore(response, link % p1.pk, link % p2.pk) p1 = ComplexSortedPerson.objects.create(name="Bob", age=10) p2 = ComplexSortedPerson.objects.create(name="Amy", age=20) link = '<a href="%s/' response = self.client.get('/test_admin/admin/admin_views/complexsortedperson/', {}) - self.assertEqual(response.status_code, 200) # Should have 5 columns (including action checkbox col) self.assertContains(response, '<th scope="col"', count=5) @@ -357,14 +339,10 @@ class AdminViewBasicTest(TestCase): self.assertContains(response, 'Colored name') # Check order - self.assertTrue( - response.content.index('Name') < response.content.index('Colored name') - ) + self.assertContentBefore(response, 'Name', 'Colored name') # Check sorting - should be by name - self.assertTrue( - response.content.index(link % p2.id) < response.content.index(link % p1.id) - ) + self.assertContentBefore(response, link % p2.id, link % p1.id) def testSortIndicatorsAdminOrder(self): """ @@ -389,8 +367,8 @@ class AdminViewBasicTest(TestCase): # the implicit 'action_checkbox' and 1 being the column 'stuff'. self.assertEqual(response.context['cl'].get_ordering_field_columns(), {2: 'asc'}) # Check order of records. - self.assertTrue(response.content.index('The First Item') < - response.content.index('The Middle Item') < response.content.index('The Last Item')) + self.assertContentBefore(response, 'The First Item', 'The Middle Item') + self.assertContentBefore(response, 'The Middle Item', 'The Last Item') def testLimitedFilter(self): """Ensure admin changelist filters do not contain objects excluded via limit_choices_to. @@ -480,12 +458,9 @@ class AdminViewBasicTest(TestCase): has been used in the choices option of a field. """ response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit) - self.assertEqual(response.status_code, 200) - self.assertTrue( - '<a href="1/">Horizontal</a>' in response.content and - '<a href="2/">Vertical</a>' in response.content, - "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group." - ) + fail_msg = "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group." + self.assertContains(response, '<a href="1/">Horizontal</a>', msg_prefix=fail_msg, html=True) + self.assertContains(response, '<a href="2/">Vertical</a>', msg_prefix=fail_msg, html=True) def testNamedGroupFieldChoicesFilter(self): """ @@ -493,13 +468,12 @@ class AdminViewBasicTest(TestCase): been used in the choices option of a model field. """ response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit) - self.assertEqual(response.status_code, 200) + fail_msg = "Changelist filter isn't showing options contained inside a model field 'choices' option named group." self.assertContains(response, '<div id="changelist-filter">') - self.assertTrue( - '<a href="?surface__exact=x">Horizontal</a>' in response.content and - '<a href="?surface__exact=y">Vertical</a>' in response.content, - "Changelist filter isn't showing options contained inside a model field 'choices' option named group." - ) + self.assertContains(response, + '<a href="?surface__exact=x">Horizontal</a>', msg_prefix=fail_msg, html=True) + self.assertContains(response, + '<a href="?surface__exact=y">Vertical</a>', msg_prefix=fail_msg, html=True) def testChangeListNullBooleanDisplay(self): Post.objects.create(public=None) @@ -590,8 +564,8 @@ class AdminViewBasicTest(TestCase): user.save() response = self.client.get('/test_admin/admin/') - self.assertFalse(reverse('admin:password_change') in response.content, - msg='The "change password" link should not be displayed if a user does not have a usable password.') + self.assertNotContains(response, reverse('admin:password_change'), + msg_prefix='The "change password" link should not be displayed if a user does not have a usable password.') def test_change_view_with_show_delete_extra_context(self): """ @@ -618,7 +592,7 @@ class AdminViewFormUrlTest(TestCase): def testChangeFormUrlHasCorrectValue(self): """ - Tests whether change_view has form_url in request.context + Tests whether change_view has form_url in response.context """ response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit) self.assertTrue('form_url' in response.context, msg='form_url not present in response.context') @@ -755,39 +729,39 @@ class CustomModelAdminTest(AdminViewBasicTest): def testCustomAdminSiteLoginTemplate(self): self.client.logout() - request = self.client.get('/test_admin/admin2/') - self.assertIsInstance(request, TemplateResponse) - self.assertTemplateUsed(request, 'custom_admin/login.html') - self.assertTrue('Hello from a custom login template' in request.content) + response = self.client.get('/test_admin/admin2/') + self.assertIsInstance(response, TemplateResponse) + self.assertTemplateUsed(response, 'custom_admin/login.html') + self.assertContains(response, 'Hello from a custom login template') def testCustomAdminSiteLogoutTemplate(self): - request = self.client.get('/test_admin/admin2/logout/') - self.assertIsInstance(request, TemplateResponse) - self.assertTemplateUsed(request, 'custom_admin/logout.html') - self.assertTrue('Hello from a custom logout template' in request.content) + response = self.client.get('/test_admin/admin2/logout/') + self.assertIsInstance(response, TemplateResponse) + self.assertTemplateUsed(response, 'custom_admin/logout.html') + self.assertContains(response, 'Hello from a custom logout template') def testCustomAdminSiteIndexViewAndTemplate(self): - request = self.client.get('/test_admin/admin2/') - self.assertIsInstance(request, TemplateResponse) - self.assertTemplateUsed(request, 'custom_admin/index.html') - self.assertTrue('Hello from a custom index template *bar*' in request.content) + response = self.client.get('/test_admin/admin2/') + self.assertIsInstance(response, TemplateResponse) + self.assertTemplateUsed(response, 'custom_admin/index.html') + self.assertContains(response, 'Hello from a custom index template *bar*') def testCustomAdminSitePasswordChangeTemplate(self): - request = self.client.get('/test_admin/admin2/password_change/') - self.assertIsInstance(request, TemplateResponse) - self.assertTemplateUsed(request, 'custom_admin/password_change_form.html') - self.assertTrue('Hello from a custom password change form template' in request.content) + response = self.client.get('/test_admin/admin2/password_change/') + self.assertIsInstance(response, TemplateResponse) + self.assertTemplateUsed(response, 'custom_admin/password_change_form.html') + self.assertContains(response, 'Hello from a custom password change form template') def testCustomAdminSitePasswordChangeDoneTemplate(self): - request = self.client.get('/test_admin/admin2/password_change/done/') - self.assertIsInstance(request, TemplateResponse) - self.assertTemplateUsed(request, 'custom_admin/password_change_done.html') - self.assertTrue('Hello from a custom password change done template' in request.content) + response = self.client.get('/test_admin/admin2/password_change/done/') + self.assertIsInstance(response, TemplateResponse) + self.assertTemplateUsed(response, 'custom_admin/password_change_done.html') + self.assertContains(response, 'Hello from a custom password change done template') def testCustomAdminSiteView(self): self.client.login(username='super', password='secret') response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) - self.assertEqual(response.content, "Django is a magical pony!") + self.assertEqual(response.content, b"Django is a magical pony!") def get_perm(Model, perm): """Return the permission object, for the Model""" @@ -885,16 +859,16 @@ class AdminViewPermissionsTest(TestCase): a 200 status code. """ # Super User - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_login) self.assertRedirects(login, '/test_admin/admin/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Test if user enters e-mail address - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) self.assertContains(login, "Your e-mail address is not your username") # only correct passwords get a username hint @@ -907,47 +881,47 @@ class AdminViewPermissionsTest(TestCase): self.assertContains(login, ERROR_MESSAGE) # Add User - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.adduser_login) self.assertRedirects(login, '/test_admin/admin/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Change User - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.changeuser_login) self.assertRedirects(login, '/test_admin/admin/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Delete User - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.deleteuser_login) self.assertRedirects(login, '/test_admin/admin/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Regular User should not be able to login. - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.joepublic_login) self.assertEqual(login.status_code, 200) self.assertContains(login, ERROR_MESSAGE) # Requests without username should not return 500 errors. - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.no_username_login) self.assertEqual(login.status_code, 200) form = login.context[0].get('form') self.assertEqual(form.errors['username'][0], 'This field is required.') def testLoginSuccessfullyRedirectsToOriginalUrl(self): - request = self.client.get('/test_admin/admin/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/') + self.assertEqual(response.status_code, 200) query_string = 'the-answer=42' redirect_url = '/test_admin/admin/?%s' % query_string new_next = {REDIRECT_FIELD_NAME: redirect_url} @@ -967,8 +941,8 @@ class AdminViewPermissionsTest(TestCase): self.client.post('/test_admin/admin/', self.changeuser_login) # make sure the view removes test cookie self.assertEqual(self.client.session.test_cookie_worked(), False) - request = self.client.get('/test_admin/admin/admin_views/article/add/') - self.assertEqual(request.status_code, 403) + response = self.client.get('/test_admin/admin/admin_views/article/add/') + self.assertEqual(response.status_code, 403) # Try POST just to make sure post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) self.assertEqual(post.status_code, 403) @@ -979,10 +953,9 @@ class AdminViewPermissionsTest(TestCase): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.adduser_login) addpage = self.client.get('/test_admin/admin/admin_views/article/add/') - self.assertEqual(addpage.status_code, 200) change_list_link = '› <a href="/test_admin/admin/admin_views/article/">Articles</a>' - self.assertFalse(change_list_link in addpage.content, - 'User restricted to add permission is given link to change list view in breadcrumbs.') + self.assertNotContains(addpage, change_list_link, + msg_prefix='User restricted to add permission is given link to change list view in breadcrumbs.') post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) self.assertRedirects(post, '/test_admin/admin/') self.assertEqual(Article.objects.all().count(), 4) @@ -994,9 +967,8 @@ class AdminViewPermissionsTest(TestCase): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.super_login) addpage = self.client.get('/test_admin/admin/admin_views/article/add/') - self.assertEqual(addpage.status_code, 200) - self.assertFalse(change_list_link not in addpage.content, - 'Unrestricted user is not given link to change list view in breadcrumbs.') + self.assertContains(addpage, change_list_link, + msg_prefix='Unrestricted user is not given link to change list view in breadcrumbs.') post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) self.assertRedirects(post, '/test_admin/admin/admin_views/article/') self.assertEqual(Article.objects.all().count(), 5) @@ -1022,10 +994,10 @@ class AdminViewPermissionsTest(TestCase): # add user shoud not be able to view the list of article or change any of them self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.adduser_login) - request = self.client.get('/test_admin/admin/admin_views/article/') - self.assertEqual(request.status_code, 403) - request = self.client.get('/test_admin/admin/admin_views/article/1/') - self.assertEqual(request.status_code, 403) + response = self.client.get('/test_admin/admin/admin_views/article/') + self.assertEqual(response.status_code, 403) + response = self.client.get('/test_admin/admin/admin_views/article/1/') + self.assertEqual(response.status_code, 403) post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) self.assertEqual(post.status_code, 403) self.client.get('/test_admin/admin/logout/') @@ -1033,10 +1005,10 @@ class AdminViewPermissionsTest(TestCase): # change user can view all items and edit them self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.changeuser_login) - request = self.client.get('/test_admin/admin/admin_views/article/') - self.assertEqual(request.status_code, 200) - request = self.client.get('/test_admin/admin/admin_views/article/1/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/admin_views/article/') + self.assertEqual(response.status_code, 200) + response = self.client.get('/test_admin/admin/admin_views/article/1/') + self.assertEqual(response.status_code, 200) post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) self.assertRedirects(post, '/test_admin/admin/admin_views/article/') self.assertEqual(Article.objects.get(pk=1).content, '<p>edited article</p>') @@ -1058,33 +1030,33 @@ class AdminViewPermissionsTest(TestCase): RowLevelChangePermissionModel.objects.create(id=2, name="even id") for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]: self.client.post('/test_admin/admin/', login_dict) - request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/') - self.assertEqual(request.status_code, 403) - request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'}) + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/') + self.assertEqual(response.status_code, 403) + response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'}) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id') - self.assertEqual(request.status_code, 403) - request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/') - self.assertEqual(request.status_code, 200) - request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed'}) + self.assertEqual(response.status_code, 403) + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/') + self.assertEqual(response.status_code, 200) + response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed'}) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') - self.assertRedirects(request, '/test_admin/admin/') + self.assertRedirects(response, '/test_admin/admin/') self.client.get('/test_admin/admin/logout/') for login_dict in [self.joepublic_login, self.no_username_login]: self.client.post('/test_admin/admin/', login_dict) - request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/') - self.assertEqual(request.status_code, 200) - self.assertContains(request, 'login-form') - request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'}) + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/') + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'login-form') + response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'}) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id') - self.assertEqual(request.status_code, 200) - self.assertContains(request, 'login-form') - request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/') - self.assertEqual(request.status_code, 200) - self.assertContains(request, 'login-form') - request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed again'}) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'login-form') + response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/') + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'login-form') + response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed again'}) self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') - self.assertEqual(request.status_code, 200) - self.assertContains(request, 'login-form') + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'login-form') self.client.get('/test_admin/admin/logout/') def testConditionallyShowAddSectionLink(self): @@ -1114,14 +1086,13 @@ class AdminViewPermissionsTest(TestCase): self.client.post('/test_admin/admin/', self.super_login) # Test custom change list template with custom extra context - request = self.client.get('/test_admin/admin/admin_views/customarticle/') - self.assertEqual(request.status_code, 200) - self.assertTrue("var hello = 'Hello!';" in request.content) - self.assertTemplateUsed(request, 'custom_admin/change_list.html') + response = self.client.get('/test_admin/admin/admin_views/customarticle/') + self.assertContains(response, "var hello = 'Hello!';") + self.assertTemplateUsed(response, 'custom_admin/change_list.html') # Test custom add form template - request = self.client.get('/test_admin/admin/admin_views/customarticle/add/') - self.assertTemplateUsed(request, 'custom_admin/add_form.html') + response = self.client.get('/test_admin/admin/admin_views/customarticle/add/') + self.assertTemplateUsed(response, 'custom_admin/add_form.html') # Add an article so we can test delete, change, and history views post = self.client.post('/test_admin/admin/admin_views/customarticle/add/', { @@ -1135,18 +1106,18 @@ class AdminViewPermissionsTest(TestCase): # Test custom delete, change, and object history templates # Test custom change form template - request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/' % article_pk) - self.assertTemplateUsed(request, 'custom_admin/change_form.html') - request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/delete/' % article_pk) - self.assertTemplateUsed(request, 'custom_admin/delete_confirmation.html') - request = self.client.post('/test_admin/admin/admin_views/customarticle/', data={ + response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/' % article_pk) + self.assertTemplateUsed(response, 'custom_admin/change_form.html') + response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/delete/' % article_pk) + self.assertTemplateUsed(response, 'custom_admin/delete_confirmation.html') + response = self.client.post('/test_admin/admin/admin_views/customarticle/', data={ 'index': 0, 'action': ['delete_selected'], '_selected_action': ['1'], }) - self.assertTemplateUsed(request, 'custom_admin/delete_selected_confirmation.html') - request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/history/' % article_pk) - self.assertTemplateUsed(request, 'custom_admin/object_history.html') + self.assertTemplateUsed(response, 'custom_admin/delete_selected_confirmation.html') + response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/history/' % article_pk) + self.assertTemplateUsed(response, 'custom_admin/object_history.html') self.client.get('/test_admin/admin/logout/') @@ -1158,8 +1129,8 @@ class AdminViewPermissionsTest(TestCase): # add user shoud not be able to delete articles self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.adduser_login) - request = self.client.get('/test_admin/admin/admin_views/article/1/delete/') - self.assertEqual(request.status_code, 403) + response = self.client.get('/test_admin/admin/admin_views/article/1/delete/') + self.assertEqual(response.status_code, 403) post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) self.assertEqual(post.status_code, 403) self.assertEqual(Article.objects.all().count(), 3) @@ -1246,9 +1217,9 @@ class AdminViewDeletedObjectsTest(TestCase): Objects should be nested to display the relationships that cause them to be scheduled for deletion. """ - pattern = re.compile(r"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""") + pattern = re.compile(br"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""") response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) - self.assertTrue(pattern.search(response.content)) + self.assertRegexpMatches(response.content, pattern) def test_cyclic(self): """ @@ -1407,9 +1378,9 @@ class AdminViewStringPrimaryKeyTest(TestCase): logentry.content_type = None logentry.save() - counted_presence_before = response.content.count(should_contain) + counted_presence_before = response.content.count(force_bytes(should_contain)) response = self.client.get('/test_admin/admin/') - counted_presence_after = response.content.count(should_contain) + counted_presence_after = response.content.count(force_bytes(should_contain)) self.assertEqual(counted_presence_before - 1, counted_presence_after) @@ -1512,8 +1483,8 @@ class SecureViewTests(TestCase): self.assertTemplateUsed(response, 'admin/login.html') def test_secure_view_login_successfully_redirects_to_original_url(self): - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) query_string = 'the-answer=42' redirect_url = '/test_admin/admin/secure-view/?%s' % query_string new_next = {REDIRECT_FIELD_NAME: redirect_url} @@ -1529,8 +1500,8 @@ class SecureViewTests(TestCase): a 200 status code. """ # Super User - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') self.assertFalse(login.context) @@ -1539,8 +1510,8 @@ class SecureViewTests(TestCase): self.assertEqual(self.client.session.test_cookie_worked(), False) # Test if user enters e-mail address - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) self.assertContains(login, "Your e-mail address is not your username") # only correct passwords get a username hint @@ -1553,32 +1524,32 @@ class SecureViewTests(TestCase): self.assertContains(login, ERROR_MESSAGE) # Add User - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.adduser_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Change User - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.changeuser_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Delete User - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.deleteuser_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Regular User should not be able to login. - request = self.client.get('/test_admin/admin/secure-view/') - self.assertEqual(request.status_code, 200) + response = self.client.get('/test_admin/admin/secure-view/') + self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.joepublic_login) self.assertEqual(login.status_code, 200) # Login.context is a list of context dicts we just need to check the first one. @@ -2107,7 +2078,7 @@ class AdminInheritedInlinesTest(TestCase): foo_user = "foo username" bar_user = "bar username" - name_re = re.compile('name="(.*?)"') + name_re = re.compile(b'name="(.*?)"') # test the add case response = self.client.get('/test_admin/admin/admin_views/persona/add/') @@ -2214,7 +2185,7 @@ class AdminActionsTest(TestCase): confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) self.assertIsInstance(confirmation, TemplateResponse) self.assertContains(confirmation, "Are you sure you want to delete the selected subscribers?") - self.assertTrue(confirmation.content.count(ACTION_CHECKBOX_NAME) == 2) + self.assertContains(confirmation, ACTION_CHECKBOX_NAME, count=2) response = self.client.post('/test_admin/admin/admin_views/subscriber/', delete_confirmation_data) self.assertEqual(Subscriber.objects.count(), 0) @@ -2390,7 +2361,7 @@ class AdminActionsTest(TestCase): def test_popup_actions(self): """ Actions should not be shown in popups. """ response = self.client.get('/test_admin/admin/admin_views/subscriber/') - self.assertNotEquals(response.context["action_form"], None) + self.assertNotEqual(response.context["action_form"], None) response = self.client.get( '/test_admin/admin/admin_views/subscriber/?%s' % IS_POPUP_VAR) self.assertEqual(response.context["action_form"], None) @@ -2514,7 +2485,7 @@ class AdminInlineFileUploadTest(TestCase): # to use a NamedTemporaryFile. tdir = tempfile.gettempdir() file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir) - file1.write('a' * (2 ** 21)) + file1.write(b'a' * (2 ** 21)) filename = file1.name file1.close() self.gallery = Gallery(name="Test Gallery") @@ -3197,9 +3168,9 @@ class RawIdFieldsTest(TestCase): country="Spain") response = self.client.get('/test_admin/admin/admin_views/sketch/add/') # Find the link - m = re.search(r'<a href="([^"]*)"[^>]* id="lookup_id_inquisition"', response.content) + m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_inquisition"', response.content) self.assertTrue(m) # Got a match - popup_url = m.groups()[0].replace("&", "&") + popup_url = m.groups()[0].decode().replace("&", "&") # Handle relative links popup_url = urljoin(response.request['PATH_INFO'], popup_url) diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py index 81a4ec7aba..2e452798b7 100644 --- a/tests/regressiontests/admin_widgets/models.py +++ b/tests/regressiontests/admin_widgets/models.py @@ -2,39 +2,44 @@ from __future__ import unicode_literals from django.db import models from django.contrib.auth.models import User +from django.utils.encoding import python_2_unicode_compatible class MyFileField(models.FileField): pass +@python_2_unicode_compatible class Member(models.Model): name = models.CharField(max_length=100) birthdate = models.DateTimeField(blank=True, null=True) gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')]) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Band(models.Model): name = models.CharField(max_length=100) members = models.ManyToManyField(Member) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Album(models.Model): band = models.ForeignKey(Band) name = models.CharField(max_length=100) cover_art = models.FileField(upload_to='albums') backside_art = MyFileField(upload_to='albums_back', null=True) - def __unicode__(self): + def __str__(self): return self.name class HiddenInventoryManager(models.Manager): def get_query_set(self): return super(HiddenInventoryManager, self).get_query_set().filter(hidden=False) +@python_2_unicode_compatible class Inventory(models.Model): barcode = models.PositiveIntegerField(unique=True) parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) @@ -45,7 +50,7 @@ class Inventory(models.Model): default_manager = models.Manager() objects = HiddenInventoryManager() - def __unicode__(self): + def __str__(self): return self.name class Event(models.Model): @@ -56,12 +61,13 @@ class Event(models.Model): link = models.URLField(blank=True) min_age = models.IntegerField(blank=True, null=True) +@python_2_unicode_compatible class Car(models.Model): owner = models.ForeignKey(User) make = models.CharField(max_length=30) model = models.CharField(max_length=30) - def __unicode__(self): + def __str__(self): return "%s %s" % (self.make, self.model) class CarTire(models.Model): @@ -103,19 +109,21 @@ class Advisor(models.Model): companies = models.ManyToManyField(Company) +@python_2_unicode_compatible class Student(models.Model): name = models.CharField(max_length=255) - def __unicode__(self): + def __str__(self): return self.name class Meta: ordering = ('name',) +@python_2_unicode_compatible class School(models.Model): name = models.CharField(max_length=255) students = models.ManyToManyField(Student, related_name='current_schools') alumni = models.ManyToManyField(Student, related_name='previous_schools') - def __unicode__(self): - return self.name
\ No newline at end of file + def __str__(self): + return self.name diff --git a/tests/regressiontests/aggregation_regress/models.py b/tests/regressiontests/aggregation_regress/models.py index ccef9a5fc8..dd4ff50aec 100644 --- a/tests/regressiontests/aggregation_regress/models.py +++ b/tests/regressiontests/aggregation_regress/models.py @@ -1,24 +1,28 @@ # coding: utf-8 from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=100) age = models.IntegerField() friends = models.ManyToManyField('self', blank=True) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Publisher(models.Model): name = models.CharField(max_length=255) num_awards = models.IntegerField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Book(models.Model): isbn = models.CharField(max_length=9) name = models.CharField(max_length=255) @@ -33,17 +37,18 @@ class Book(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Store(models.Model): name = models.CharField(max_length=255) books = models.ManyToManyField(Book) original_opening = models.DateTimeField() friday_night_closing = models.TimeField() - def __unicode__(self): + def __str__(self): return self.name class Entries(models.Model): @@ -58,8 +63,9 @@ class Clues(models.Model): Clue = models.CharField(max_length=150) +@python_2_unicode_compatible class HardbackBook(Book): weight = models.FloatField() - def __unicode__(self): + def __str__(self): return "%s (hardback): %s" % (self.name, self.weight) diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index e24eb43b87..b9f3ab27eb 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -866,3 +866,15 @@ class AggregationTests(TestCase): ['Peter Norvig'], lambda b: b.name ) + + def test_type_conversion(self): + # The database backend convert_values function should not try to covert + # CharFields to float. Refs #13844. + from django.db.models import CharField + from django.db import connection + testData = 'not_a_float_value' + testField = CharField() + self.assertEqual( + connection.ops.convert_values(testData, testField), + testData + ) diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index af4952dce8..344cf4c798 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -3,21 +3,24 @@ from __future__ import unicode_literals from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models, connection +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Square(models.Model): root = models.IntegerField() square = models.PositiveIntegerField() - def __unicode__(self): + def __str__(self): return "%s ** 2 == %s" % (self.root, self.square) +@python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return '%s %s' % (self.first_name, self.last_name) @@ -55,18 +58,20 @@ class Post(models.Model): db_table = 'CaseSensitive_Post' +@python_2_unicode_compatible class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() reporter = models.ForeignKey(Reporter) - def __unicode__(self): + def __str__(self): return self.headline diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index a6425c5591..dc92189344 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -584,7 +584,7 @@ class ThreadTests(TestCase): connections['default'] = main_thread_connection try: models.Person.objects.get(first_name="John", last_name="Doe") - except DatabaseError as e: + except Exception as e: exceptions.append(e) t = threading.Thread(target=runner, args=[connections['default']]) t.start() @@ -594,21 +594,21 @@ class ThreadTests(TestCase): exceptions = [] do_thread() # Forbidden! - self.assertTrue(isinstance(exceptions[0], DatabaseError)) + self.assertIsInstance(exceptions[0], DatabaseError) # If explicitly setting allow_thread_sharing to False connections['default'].allow_thread_sharing = False exceptions = [] do_thread() # Forbidden! - self.assertTrue(isinstance(exceptions[0], DatabaseError)) + self.assertIsInstance(exceptions[0], DatabaseError) # If explicitly setting allow_thread_sharing to True connections['default'].allow_thread_sharing = True exceptions = [] do_thread() # All good - self.assertEqual(len(exceptions), 0) + self.assertEqual(exceptions, []) def test_closing_non_shared_connections(self): """ diff --git a/tests/regressiontests/builtin_server/tests.py b/tests/regressiontests/builtin_server/tests.py index 4a3b44176b..c8dc77e42e 100644 --- a/tests/regressiontests/builtin_server/tests.py +++ b/tests/regressiontests/builtin_server/tests.py @@ -1,5 +1,8 @@ +from __future__ import unicode_literals + +from io import BytesIO + from django.core.servers.basehttp import ServerHandler -from django.utils.six import StringIO from django.utils.unittest import TestCase # @@ -23,12 +26,12 @@ class FileWrapperHandler(ServerHandler): return True def wsgi_app(environ, start_response): - start_response('200 OK', [('Content-Type', 'text/plain')]) - return ['Hello World!'] + start_response(str('200 OK'), [(str('Content-Type'), str('text/plain'))]) + return [b'Hello World!'] def wsgi_app_file_wrapper(environ, start_response): - start_response('200 OK', [('Content-Type', 'text/plain')]) - return environ['wsgi.file_wrapper'](StringIO('foo')) + start_response(str('200 OK'), [(str('Content-Type'), str('text/plain'))]) + return environ['wsgi.file_wrapper'](BytesIO(b'foo')) class WSGIFileWrapperTests(TestCase): """ @@ -37,15 +40,16 @@ class WSGIFileWrapperTests(TestCase): def test_file_wrapper_uses_sendfile(self): env = {'SERVER_PROTOCOL': 'HTTP/1.0'} - err = StringIO() - handler = FileWrapperHandler(None, StringIO(), err, env) + handler = FileWrapperHandler(None, BytesIO(), BytesIO(), env) handler.run(wsgi_app_file_wrapper) self.assertTrue(handler._used_sendfile) + self.assertEqual(handler.stdout.getvalue(), b'') + self.assertEqual(handler.stderr.getvalue(), b'') def test_file_wrapper_no_sendfile(self): env = {'SERVER_PROTOCOL': 'HTTP/1.0'} - err = StringIO() - handler = FileWrapperHandler(None, StringIO(), err, env) + handler = FileWrapperHandler(None, BytesIO(), BytesIO(), env) handler.run(wsgi_app) self.assertFalse(handler._used_sendfile) - self.assertEqual(handler.stdout.getvalue().splitlines()[-1],'Hello World!') + self.assertEqual(handler.stdout.getvalue().splitlines()[-1], b'Hello World!') + self.assertEqual(handler.stderr.getvalue(), b'') diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index 8fc749aaa2..a6534c5f54 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -6,7 +6,9 @@ from __future__ import absolute_import, unicode_literals import hashlib import os +import random import re +import string import tempfile import time import warnings @@ -364,25 +366,25 @@ class BaseCacheTests(object): # Binary strings should be cacheable from zlib import compress, decompress value = 'value_to_be_compressed' - compressed_value = compress(value) + compressed_value = compress(value.encode()) # Test set self.cache.set('binary1', compressed_value) compressed_result = self.cache.get('binary1') self.assertEqual(compressed_value, compressed_result) - self.assertEqual(value, decompress(compressed_result)) + self.assertEqual(value, decompress(compressed_result).decode()) # Test add self.cache.add('binary1-add', compressed_value) compressed_result = self.cache.get('binary1-add') self.assertEqual(compressed_value, compressed_result) - self.assertEqual(value, decompress(compressed_result)) + self.assertEqual(value, decompress(compressed_result).decode()) # Test set_many self.cache.set_many({'binary1-set_many': compressed_value}) compressed_result = self.cache.get('binary1-set_many') self.assertEqual(compressed_value, compressed_result) - self.assertEqual(value, decompress(compressed_result)) + self.assertEqual(value, decompress(compressed_result).decode()) def test_set_many(self): # Multiple keys can be set using set_many @@ -774,13 +776,13 @@ class BaseCacheTests(object): get_cache_data = fetch_middleware.process_request(request) self.assertNotEqual(get_cache_data, None) - self.assertEqual(get_cache_data.content, content) + self.assertEqual(get_cache_data.content, content.encode('utf-8')) self.assertEqual(get_cache_data.cookies, response.cookies) update_middleware.process_response(request, get_cache_data) get_cache_data = fetch_middleware.process_request(request) self.assertNotEqual(get_cache_data, None) - self.assertEqual(get_cache_data.content, content) + self.assertEqual(get_cache_data.content, content.encode('utf-8')) self.assertEqual(get_cache_data.cookies, response.cookies) def custom_key_func(key, key_prefix, version): @@ -932,21 +934,24 @@ class LocMemCacheTests(unittest.TestCase, BaseCacheTests): # memcached backend isn't guaranteed to be available. # To check the memcached backend, the test settings file will -# need to contain a cache backend setting that points at +# need to contain at least one cache backend setting that points at # your memcache server. @unittest.skipUnless( - settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND'].startswith('django.core.cache.backends.memcached.'), + any(cache['BACKEND'].startswith('django.core.cache.backends.memcached.') + for cache in settings.CACHES.values()), "memcached not available") class MemcachedCacheTests(unittest.TestCase, BaseCacheTests): - backend_name = 'django.core.cache.backends.memcached.MemcachedCache' def setUp(self): - name = settings.CACHES[DEFAULT_CACHE_ALIAS]['LOCATION'] - self.cache = get_cache(self.backend_name, LOCATION=name) - self.prefix_cache = get_cache(self.backend_name, LOCATION=name, KEY_PREFIX='cacheprefix') - self.v2_cache = get_cache(self.backend_name, LOCATION=name, VERSION=2) - self.custom_key_cache = get_cache(self.backend_name, LOCATION=name, KEY_FUNCTION=custom_key_func) - self.custom_key_cache2 = get_cache(self.backend_name, LOCATION=name, KEY_FUNCTION='regressiontests.cache.tests.custom_key_func') + for cache_key, cache in settings.CACHES.items(): + if cache['BACKEND'].startswith('django.core.cache.backends.memcached.'): + break + random_prefix = ''.join(random.choice(string.ascii_letters) for x in range(10)) + self.cache = get_cache(cache_key) + self.prefix_cache = get_cache(cache_key, KEY_PREFIX=random_prefix) + self.v2_cache = get_cache(cache_key, VERSION=2) + self.custom_key_cache = get_cache(cache_key, KEY_FUNCTION=custom_key_func) + self.custom_key_cache2 = get_cache(cache_key, KEY_FUNCTION='regressiontests.cache.tests.custom_key_func') def tearDown(self): self.cache.clear() @@ -988,7 +993,7 @@ class FileBasedCacheTests(unittest.TestCase, BaseCacheTests): """Test that keys are hashed into subdirectories correctly""" self.cache.set("foo", "bar") key = self.cache.make_key("foo") - keyhash = hashlib.md5(key).hexdigest() + keyhash = hashlib.md5(key.encode()).hexdigest() keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) self.assertTrue(os.path.exists(keypath)) @@ -998,7 +1003,7 @@ class FileBasedCacheTests(unittest.TestCase, BaseCacheTests): """ self.cache.set("foo", "bar") key = self.cache.make_key("foo") - keyhash = hashlib.md5(key).hexdigest() + keyhash = hashlib.md5(key.encode()).hexdigest() keypath = os.path.join(self.dirname, keyhash[:2], keyhash[2:4], keyhash[4:]) self.assertTrue(os.path.exists(keypath)) @@ -1222,7 +1227,7 @@ class CacheHEADTest(TestCase): request = self._get_request('HEAD') get_cache_data = FetchFromCacheMiddleware().process_request(request) self.assertNotEqual(get_cache_data, None) - self.assertEqual(test_content, get_cache_data.content) + self.assertEqual(test_content.encode(), get_cache_data.content) def test_head_with_cached_get(self): test_content = 'test content' @@ -1233,7 +1238,7 @@ class CacheHEADTest(TestCase): request = self._get_request('HEAD') get_cache_data = FetchFromCacheMiddleware().process_request(request) self.assertNotEqual(get_cache_data, None) - self.assertEqual(test_content, get_cache_data.content) + self.assertEqual(test_content.encode(), get_cache_data.content) @override_settings( @@ -1308,7 +1313,7 @@ class CacheI18nTest(TestCase): # This is tightly coupled to the implementation, # but it's the most straightforward way to test the key. tz = force_text(timezone.get_current_timezone_name(), errors='ignore') - tz = tz.encode('ascii', 'ignore').replace(' ', '_') + tz = tz.encode('ascii', 'ignore').decode('ascii').replace(' ', '_') response = HttpResponse() key = learn_cache_key(request, response) self.assertIn(tz, key, "Cache keys should include the time zone name when time zones are active") @@ -1320,7 +1325,7 @@ class CacheI18nTest(TestCase): request = self._get_request() lang = translation.get_language() tz = force_text(timezone.get_current_timezone_name(), errors='ignore') - tz = tz.encode('ascii', 'ignore').replace(' ', '_') + tz = tz.encode('ascii', 'ignore').decode('ascii').replace(' ', '_') response = HttpResponse() key = learn_cache_key(request, response) self.assertNotIn(lang, key, "Cache keys shouldn't include the language name when i18n isn't active") @@ -1373,7 +1378,7 @@ class CacheI18nTest(TestCase): get_cache_data = FetchFromCacheMiddleware().process_request(request) # cache must return content self.assertNotEqual(get_cache_data, None) - self.assertEqual(get_cache_data.content, content) + self.assertEqual(get_cache_data.content, content.encode()) # different QUERY_STRING, cache must be empty request = self._get_request_cache(query_string='foo=bar&somethingelse=true') get_cache_data = FetchFromCacheMiddleware().process_request(request) @@ -1388,7 +1393,7 @@ class CacheI18nTest(TestCase): get_cache_data = FetchFromCacheMiddleware().process_request(request) # Check that we can recover the cache self.assertNotEqual(get_cache_data, None) - self.assertEqual(get_cache_data.content, en_message) + self.assertEqual(get_cache_data.content, en_message.encode()) # Check that we use etags self.assertTrue(get_cache_data.has_header('ETag')) # Check that we can disable etags @@ -1404,11 +1409,11 @@ class CacheI18nTest(TestCase): translation.activate('en') # retrieve the content from cache get_cache_data = FetchFromCacheMiddleware().process_request(request) - self.assertEqual(get_cache_data.content, en_message) + self.assertEqual(get_cache_data.content, en_message.encode()) # change again the language translation.activate('es') get_cache_data = FetchFromCacheMiddleware().process_request(request) - self.assertEqual(get_cache_data.content, es_message) + self.assertEqual(get_cache_data.content, es_message.encode()) # reset the language translation.deactivate() @@ -1513,8 +1518,8 @@ class CacheMiddlewareTest(TestCase): # Repeating the request should result in a cache hit result = middleware.process_request(request) - self.assertNotEquals(result, None) - self.assertEqual(result.content, 'Hello World 1') + self.assertNotEqual(result, None) + self.assertEqual(result.content, b'Hello World 1') # The same request through a different middleware won't hit result = prefix_middleware.process_request(request) @@ -1522,8 +1527,8 @@ class CacheMiddlewareTest(TestCase): # The same request with a timeout _will_ hit result = timeout_middleware.process_request(request) - self.assertNotEquals(result, None) - self.assertEqual(result.content, 'Hello World 1') + self.assertNotEqual(result, None) + self.assertEqual(result.content, b'Hello World 1') @override_settings(CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True) def test_cache_middleware_anonymous_only_wont_cause_session_access(self): @@ -1592,43 +1597,43 @@ class CacheMiddlewareTest(TestCase): # Request the view once response = default_view(request, '1') - self.assertEqual(response.content, 'Hello World 1') + self.assertEqual(response.content, b'Hello World 1') # Request again -- hit the cache response = default_view(request, '2') - self.assertEqual(response.content, 'Hello World 1') + self.assertEqual(response.content, b'Hello World 1') # Requesting the same view with the explicit cache should yield the same result response = explicit_default_view(request, '3') - self.assertEqual(response.content, 'Hello World 1') + self.assertEqual(response.content, b'Hello World 1') # Requesting with a prefix will hit a different cache key response = explicit_default_with_prefix_view(request, '4') - self.assertEqual(response.content, 'Hello World 4') + self.assertEqual(response.content, b'Hello World 4') # Hitting the same view again gives a cache hit response = explicit_default_with_prefix_view(request, '5') - self.assertEqual(response.content, 'Hello World 4') + self.assertEqual(response.content, b'Hello World 4') # And going back to the implicit cache will hit the same cache response = default_with_prefix_view(request, '6') - self.assertEqual(response.content, 'Hello World 4') + self.assertEqual(response.content, b'Hello World 4') # Requesting from an alternate cache won't hit cache response = other_view(request, '7') - self.assertEqual(response.content, 'Hello World 7') + self.assertEqual(response.content, b'Hello World 7') # But a repeated hit will hit cache response = other_view(request, '8') - self.assertEqual(response.content, 'Hello World 7') + self.assertEqual(response.content, b'Hello World 7') # And prefixing the alternate cache yields yet another cache entry response = other_with_prefix_view(request, '9') - self.assertEqual(response.content, 'Hello World 9') + self.assertEqual(response.content, b'Hello World 9') # Request from the alternate cache with a new prefix and a custom timeout response = other_with_timeout_view(request, '10') - self.assertEqual(response.content, 'Hello World 10') + self.assertEqual(response.content, b'Hello World 10') # But if we wait a couple of seconds... time.sleep(2) @@ -1636,38 +1641,38 @@ class CacheMiddlewareTest(TestCase): # ... the default cache will still hit cache = get_cache('default') response = default_view(request, '11') - self.assertEqual(response.content, 'Hello World 1') + self.assertEqual(response.content, b'Hello World 1') # ... the default cache with a prefix will still hit response = default_with_prefix_view(request, '12') - self.assertEqual(response.content, 'Hello World 4') + self.assertEqual(response.content, b'Hello World 4') # ... the explicit default cache will still hit response = explicit_default_view(request, '13') - self.assertEqual(response.content, 'Hello World 1') + self.assertEqual(response.content, b'Hello World 1') # ... the explicit default cache with a prefix will still hit response = explicit_default_with_prefix_view(request, '14') - self.assertEqual(response.content, 'Hello World 4') + self.assertEqual(response.content, b'Hello World 4') # .. but a rapidly expiring cache won't hit response = other_view(request, '15') - self.assertEqual(response.content, 'Hello World 15') + self.assertEqual(response.content, b'Hello World 15') # .. even if it has a prefix response = other_with_prefix_view(request, '16') - self.assertEqual(response.content, 'Hello World 16') + self.assertEqual(response.content, b'Hello World 16') # ... but a view with a custom timeout will still hit response = other_with_timeout_view(request, '17') - self.assertEqual(response.content, 'Hello World 10') + self.assertEqual(response.content, b'Hello World 10') # And if we wait a few more seconds time.sleep(2) - # the custom timeouot cache will miss + # the custom timeout cache will miss response = other_with_timeout_view(request, '18') - self.assertEqual(response.content, 'Hello World 18') + self.assertEqual(response.content, b'Hello World 18') @override_settings( diff --git a/tests/regressiontests/comment_tests/models.py b/tests/regressiontests/comment_tests/models.py index 4093af14b5..472b66decd 100644 --- a/tests/regressiontests/comment_tests/models.py +++ b/tests/regressiontests/comment_tests/models.py @@ -3,9 +3,13 @@ Comments may be attached to any object. See the comment documentation for more information. """ +from __future__ import unicode_literals + from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) @@ -13,6 +17,7 @@ class Author(models.Model): def __str__(self): return '%s %s' % (self.first_name, self.last_name) +@python_2_unicode_compatible class Article(models.Model): author = models.ForeignKey(Author) headline = models.CharField(max_length=100) @@ -20,6 +25,7 @@ class Article(models.Model): def __str__(self): return self.headline +@python_2_unicode_compatible class Entry(models.Model): title = models.CharField(max_length=250) body = models.TextField() diff --git a/tests/regressiontests/conditional_processing/models.py b/tests/regressiontests/conditional_processing/models.py index d1a8ac605b..d0838e153d 100644 --- a/tests/regressiontests/conditional_processing/models.py +++ b/tests/regressiontests/conditional_processing/models.py @@ -1,4 +1,6 @@ # -*- coding:utf-8 -*- +from __future__ import unicode_literals + from datetime import datetime from django.test import TestCase @@ -20,7 +22,7 @@ class ConditionalGet(TestCase): def assertFullResponse(self, response, check_last_modified=True, check_etag=True): self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, FULL_RESPONSE) + self.assertEqual(response.content, FULL_RESPONSE.encode()) if check_last_modified: self.assertEqual(response['Last-Modified'], LAST_MODIFIED_STR) if check_etag: @@ -28,7 +30,7 @@ class ConditionalGet(TestCase): def assertNotModified(self, response): self.assertEqual(response.status_code, 304) - self.assertEqual(response.content, '') + self.assertEqual(response.content, b'') def testWithoutConditions(self): response = self.client.get('/condition/') diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py index 52fd3ea1c7..c5b66a31d1 100644 --- a/tests/regressiontests/csrf_tests/tests.py +++ b/tests/regressiontests/csrf_tests/tests.py @@ -216,7 +216,7 @@ class CsrfViewMiddlewareTest(TestCase): """ req = self._get_GET_no_csrf_cookie_request() resp = token_view(req) - self.assertEqual("", resp.content) + self.assertEqual(resp.content, b'') def test_token_node_empty_csrf_cookie(self): """ diff --git a/tests/regressiontests/custom_columns_regress/models.py b/tests/regressiontests/custom_columns_regress/models.py index c768c12772..169b2246c3 100644 --- a/tests/regressiontests/custom_columns_regress/models.py +++ b/tests/regressiontests/custom_columns_regress/models.py @@ -8,26 +8,29 @@ table creation or queries. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): Article_ID = models.AutoField(primary_key=True, db_column='Article ID') headline = models.CharField(max_length=100) authors = models.ManyToManyField('Author', db_table='my m2m table') primary_author = models.ForeignKey('Author', db_column='Author ID', related_name='primary_set') - def __unicode__(self): + def __str__(self): return self.headline class Meta: ordering = ('headline',) +@python_2_unicode_compatible class Author(models.Model): Author_ID = models.AutoField(primary_key=True, db_column='Author ID') first_name = models.CharField(max_length=30, db_column='first name') last_name = models.CharField(max_length=30, db_column='last name') - def __unicode__(self): + def __str__(self): return '%s %s' % (self.first_name, self.last_name) class Meta: diff --git a/tests/regressiontests/custom_managers_regress/models.py b/tests/regressiontests/custom_managers_regress/models.py index 3c4e621769..71073f0fe7 100644 --- a/tests/regressiontests/custom_managers_regress/models.py +++ b/tests/regressiontests/custom_managers_regress/models.py @@ -3,6 +3,7 @@ Regression tests for custom manager classes. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class RestrictedManager(models.Manager): @@ -12,12 +13,14 @@ class RestrictedManager(models.Manager): def get_query_set(self): return super(RestrictedManager, self).get_query_set().filter(is_public=True) +@python_2_unicode_compatible class RelatedModel(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class RestrictedModel(models.Model): name = models.CharField(max_length=50) is_public = models.BooleanField(default=False) @@ -26,9 +29,10 @@ class RestrictedModel(models.Model): objects = RestrictedManager() plain_manager = models.Manager() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class OneToOneRestrictedModel(models.Model): name = models.CharField(max_length=50) is_public = models.BooleanField(default=False) @@ -37,5 +41,5 @@ class OneToOneRestrictedModel(models.Model): objects = RestrictedManager() plain_manager = models.Manager() - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/regressiontests/datatypes/models.py b/tests/regressiontests/datatypes/models.py index 332ce84307..8e6027dd0f 100644 --- a/tests/regressiontests/datatypes/models.py +++ b/tests/regressiontests/datatypes/models.py @@ -4,8 +4,10 @@ types, which in the past were problematic for some database backends. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Donut(models.Model): name = models.CharField(max_length=100) is_frosted = models.BooleanField(default=False) diff --git a/tests/regressiontests/dates/models.py b/tests/regressiontests/dates/models.py index e1fc1e74fe..e4bffb7199 100644 --- a/tests/regressiontests/dates/models.py +++ b/tests/regressiontests/dates/models.py @@ -1,22 +1,25 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Article(models.Model): title = models.CharField(max_length=100) pub_date = models.DateField() categories = models.ManyToManyField("Category", related_name="articles") - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Comment(models.Model): article = models.ForeignKey(Article, related_name="comments") text = models.TextField() pub_date = models.DateField() approval_date = models.DateField(null=True) - def __unicode__(self): + def __str__(self): return 'Comment to %s (%s)' % (self.article.title, self.pub_date) class Category(models.Model): diff --git a/tests/regressiontests/decorators/tests.py b/tests/regressiontests/decorators/tests.py index dceaa62abc..7dc36aa479 100644 --- a/tests/regressiontests/decorators/tests.py +++ b/tests/regressiontests/decorators/tests.py @@ -220,7 +220,7 @@ class MethodDecoratorTests(TestCase): self.assertEqual(getattr(Test.method, 'myattr2', False), True) self.assertEqual(Test.method.__doc__, 'A method') - self.assertEqual(Test.method.__func__.__name__, 'method') + self.assertEqual(Test.method.__name__, 'method') class XFrameOptionsDecoratorsTests(TestCase): diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index 2852572d32..bdf3c867c6 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -9,6 +9,7 @@ from django.test import TestCase from django.utils import six from django.utils import unittest, translation from django.utils.safestring import SafeData +from django.utils.encoding import python_2_unicode_compatible class DefaultFiltersTests(TestCase): @@ -456,10 +457,11 @@ class DefaultFiltersTests(TestCase): 'Lawrence</li>\n\t\t\t<li>Topeka</li>\n\t\t</ul>\n\t\t</li>'\ '\n\t\t<li>Illinois</li>\n\t</ul>\n\t</li>') + @python_2_unicode_compatible class ULItem(object): def __init__(self, title): self.title = title - def __unicode__(self): + def __str__(self): return 'ulitem-%s' % str(self.title) a = ULItem('a') diff --git a/tests/regressiontests/defer_regress/models.py b/tests/regressiontests/defer_regress/models.py index bd4f845f27..d02adccde9 100644 --- a/tests/regressiontests/defer_regress/models.py +++ b/tests/regressiontests/defer_regress/models.py @@ -3,15 +3,17 @@ Regression tests for defer() / only() behavior. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Item(models.Model): name = models.CharField(max_length=15) text = models.TextField(default="xyzzy") value = models.IntegerField() other_value = models.IntegerField(default=0) - def __unicode__(self): + def __str__(self): return self.name class RelatedItem(models.Model): @@ -21,13 +23,14 @@ class Child(models.Model): name = models.CharField(max_length=10) value = models.IntegerField() +@python_2_unicode_compatible class Leaf(models.Model): name = models.CharField(max_length=10) child = models.ForeignKey(Child) second_child = models.ForeignKey(Child, related_name="other", null=True) value = models.IntegerField(default=42) - def __unicode__(self): + def __str__(self): return self.name class ResolveThis(models.Model): @@ -38,11 +41,12 @@ class Proxy(Item): class Meta: proxy = True +@python_2_unicode_compatible class SimpleItem(models.Model): name = models.CharField(max_length=15) value = models.IntegerField() - def __unicode__(self): + def __str__(self): return self.name class Feature(models.Model): diff --git a/tests/regressiontests/dispatch/tests/test_saferef.py b/tests/regressiontests/dispatch/tests/test_saferef.py index cfe6c5df85..30eaddfe18 100644 --- a/tests/regressiontests/dispatch/tests/test_saferef.py +++ b/tests/regressiontests/dispatch/tests/test_saferef.py @@ -54,10 +54,8 @@ class SaferefTests(unittest.TestCase): sd[s] = 1 for t in self.ts: if hasattr(t, 'x'): - self.assertTrue(sd.has_key(safeRef(t.x))) self.assertTrue(safeRef(t.x) in sd) else: - self.assertTrue(sd.has_key(safeRef(t))) self.assertTrue(safeRef(t) in sd) def testRepresentation(self): diff --git a/tests/regressiontests/expressions_regress/models.py b/tests/regressiontests/expressions_regress/models.py index f3b6999377..711329031d 100644 --- a/tests/regressiontests/expressions_regress/models.py +++ b/tests/regressiontests/expressions_regress/models.py @@ -1,15 +1,17 @@ from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible """ Model for testing arithmetic expressions. """ from django.db import models +@python_2_unicode_compatible class Number(models.Model): integer = models.IntegerField(db_column='the_integer') float = models.FloatField(null=True, db_column='the_float') - def __unicode__(self): + def __str__(self): return '%i, %.3f' % (self.integer, self.float) class Experiment(models.Model): diff --git a/tests/regressiontests/extra_regress/models.py b/tests/regressiontests/extra_regress/models.py index 7d2a6fab34..11996438b4 100644 --- a/tests/regressiontests/extra_regress/models.py +++ b/tests/regressiontests/extra_regress/models.py @@ -5,14 +5,16 @@ import datetime from django.contrib.auth.models import User from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class RevisionableModel(models.Model): base = models.ForeignKey('self', null=True) title = models.CharField(blank=True, max_length=255) when = models.DateTimeField(default=datetime.datetime.now) - def __unicode__(self): + def __str__(self): return "%s (%s, %s)" % (self.title, self.id, self.base.id) def save(self, *args, **kwargs): @@ -32,11 +34,12 @@ class Order(models.Model): created_by = models.ForeignKey(User) text = models.TextField() +@python_2_unicode_compatible class TestObject(models.Model): first = models.CharField(max_length=20) second = models.CharField(max_length=20) third = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return 'TestObject: %s,%s,%s' % (self.first,self.second,self.third) diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index 31e33d915c..87b509e320 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -21,6 +21,7 @@ from django.core.files.images import get_image_dimensions from django.core.files.storage import FileSystemStorage, get_storage_class from django.core.files.uploadedfile import UploadedFile from django.test import SimpleTestCase +from django.utils import six from django.utils import unittest from ..servers.tests import LiveServerBase @@ -127,7 +128,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile(b'custom contents') + f = ContentFile('custom contents') f_name = self.storage.save('test.file', f) atime = self.storage.accessed_time(f_name) @@ -143,7 +144,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile(b'custom contents') + f = ContentFile('custom contents') f_name = self.storage.save('test.file', f) ctime = self.storage.created_time(f_name) @@ -160,7 +161,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile(b'custom contents') + f = ContentFile('custom contents') f_name = self.storage.save('test.file', f) mtime = self.storage.modified_time(f_name) @@ -177,7 +178,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile(b'custom contents') + f = ContentFile('custom contents') f.name = 'test.file' storage_f_name = self.storage.save(None, f) @@ -194,11 +195,11 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('path/to')) self.storage.save('path/to/test.file', - ContentFile(b'file saved with path')) + ContentFile('file saved with path')) self.assertTrue(self.storage.exists('path/to')) - self.assertEqual(self.storage.open('path/to/test.file').read(), - b'file saved with path') + with self.storage.open('path/to/test.file') as f: + self.assertEqual(f.read(), b'file saved with path') self.assertTrue(os.path.exists( os.path.join(self.temp_dir, 'path', 'to', 'test.file'))) @@ -211,7 +212,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile(b'custom contents') + f = ContentFile('custom contents') f_name = self.storage.save('test.file', f) self.assertEqual(self.storage.path(f_name), @@ -246,8 +247,8 @@ class FileStorageTests(unittest.TestCase): self.assertFalse(self.storage.exists('storage_test_2')) self.assertFalse(self.storage.exists('storage_dir_1')) - f = self.storage.save('storage_test_1', ContentFile(b'custom content')) - f = self.storage.save('storage_test_2', ContentFile(b'custom content')) + f = self.storage.save('storage_test_1', ContentFile('custom content')) + f = self.storage.save('storage_test_2', ContentFile('custom content')) os.mkdir(os.path.join(self.temp_dir, 'storage_dir_1')) dirs, files = self.storage.listdir('') @@ -304,18 +305,18 @@ class FileStorageTests(unittest.TestCase): os.makedirs = fake_makedirs self.storage.save('normal/test.file', - ContentFile(b'saved normally')) - self.assertEqual(self.storage.open('normal/test.file').read(), - b'saved normally') + ContentFile('saved normally')) + with self.storage.open('normal/test.file') as f: + self.assertEqual(f.read(), b'saved normally') self.storage.save('raced/test.file', - ContentFile(b'saved with race')) - self.assertEqual(self.storage.open('raced/test.file').read(), - b'saved with race') + ContentFile('saved with race')) + with self.storage.open('raced/test.file') as f: + self.assertEqual(f.read(), b'saved with race') # Check that OSErrors aside from EEXIST are still raised. self.assertRaises(OSError, - self.storage.save, 'error/test.file', ContentFile(b'not saved')) + self.storage.save, 'error/test.file', ContentFile('not saved')) finally: os.makedirs = real_makedirs @@ -341,20 +342,31 @@ class FileStorageTests(unittest.TestCase): try: os.remove = fake_remove - self.storage.save('normal.file', ContentFile(b'delete normally')) + self.storage.save('normal.file', ContentFile('delete normally')) self.storage.delete('normal.file') self.assertFalse(self.storage.exists('normal.file')) - self.storage.save('raced.file', ContentFile(b'delete with race')) + self.storage.save('raced.file', ContentFile('delete with race')) self.storage.delete('raced.file') self.assertFalse(self.storage.exists('normal.file')) # Check that OSErrors aside from ENOENT are still raised. - self.storage.save('error.file', ContentFile(b'delete with error')) + self.storage.save('error.file', ContentFile('delete with error')) self.assertRaises(OSError, self.storage.delete, 'error.file') finally: os.remove = real_remove + def test_file_chunks_error(self): + """ + Test behaviour when file.chunks() is raising an error + """ + f1 = ContentFile('chunks fails') + def failing_chunks(): + raise IOError + f1.chunks = failing_chunks + with self.assertRaises(IOError): + self.storage.save('error.file', f1) + class CustomStorage(FileSystemStorage): def get_available_name(self, name): @@ -374,9 +386,9 @@ class CustomStorageTests(FileStorageTests): storage_class = CustomStorage def test_custom_get_available_name(self): - first = self.storage.save('custom_storage', ContentFile(b'custom contents')) + first = self.storage.save('custom_storage', ContentFile('custom contents')) self.assertEqual(first, 'custom_storage') - second = self.storage.save('custom_storage', ContentFile(b'more contents')) + second = self.storage.save('custom_storage', ContentFile('more contents')) self.assertEqual(second, 'custom_storage.2') self.storage.delete(first) self.storage.delete(second) @@ -433,7 +445,7 @@ class FileStoragePermissions(unittest.TestCase): shutil.rmtree(self.storage_dir) def test_file_upload_permissions(self): - name = self.storage.save("the_file", ContentFile(b"data")) + name = self.storage.save("the_file", ContentFile("data")) actual_mode = os.stat(self.storage.path(name))[0] & 0o777 self.assertEqual(actual_mode, 0o666) @@ -453,8 +465,8 @@ class FileStoragePathParsing(unittest.TestCase): sure we still mangle the file name instead of the directory name. """ - self.storage.save('dotted.path/test', ContentFile(b"1")) - self.storage.save('dotted.path/test', ContentFile(b"2")) + self.storage.save('dotted.path/test', ContentFile("1")) + self.storage.save('dotted.path/test', ContentFile("2")) self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path'))) self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test'))) @@ -465,8 +477,8 @@ class FileStoragePathParsing(unittest.TestCase): File names with a dot as their first character don't have an extension, and the underscore should get added to the end. """ - self.storage.save('dotted.path/.test', ContentFile(b"1")) - self.storage.save('dotted.path/.test', ContentFile(b"2")) + self.storage.save('dotted.path/.test', ContentFile("1")) + self.storage.save('dotted.path/.test', ContentFile("2")) self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test'))) self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1'))) @@ -538,16 +550,25 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): self.assertEqual(size_1, size_2) class ContentFileTestCase(unittest.TestCase): - """ - Test that the constructor of ContentFile accepts 'name' (#16590). - """ + def test_content_file_default_name(self): self.assertEqual(ContentFile(b"content").name, None) def test_content_file_custom_name(self): + """ + Test that the constructor of ContentFile accepts 'name' (#16590). + """ name = "I can have a name too!" self.assertEqual(ContentFile(b"content", name=name).name, name) + def test_content_file_input_type(self): + """ + Test that ContentFile can accept both bytes and unicode and that the + retrieved content is of the same type. + """ + self.assertTrue(isinstance(ContentFile(b"content").read(), bytes)) + self.assertTrue(isinstance(ContentFile("español").read(), six.text_type)) + class NoNameFileTestCase(unittest.TestCase): """ Other examples of unnamed files may be tempfile.SpooledTemporaryFile or diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py index d2362d7197..a545ed649e 100644 --- a/tests/regressiontests/file_uploads/tests.py +++ b/tests/regressiontests/file_uploads/tests.py @@ -12,6 +12,7 @@ from django.core.files import temp as tempfile from django.core.files.uploadedfile import SimpleUploadedFile from django.http.multipartparser import MultiPartParser from django.test import TestCase, client +from django.utils.encoding import force_bytes from django.utils.six import StringIO from django.utils import unittest @@ -48,12 +49,12 @@ class FileUploadTests(TestCase): 'file_field2': file2, } - for key in post_data.keys(): + for key in list(post_data): try: post_data[key + '_hash'] = hashlib.sha1(post_data[key].read()).hexdigest() post_data[key].seek(0) except AttributeError: - post_data[key + '_hash'] = hashlib.sha1(post_data[key]).hexdigest() + post_data[key + '_hash'] = hashlib.sha1(force_bytes(post_data[key])).hexdigest() response = self.client.post('/file_uploads/verify/', post_data) @@ -67,7 +68,7 @@ class FileUploadTests(TestCase): 'Content-Type: application/octet-stream', 'Content-Transfer-Encoding: base64', '', - base64.b64encode(test_string), + base64.b64encode(force_bytes(test_string)).decode('ascii'), '--' + client.BOUNDARY + '--', '', ]).encode('utf-8') @@ -79,7 +80,7 @@ class FileUploadTests(TestCase): 'wsgi.input': client.FakePayload(payload), } response = self.client.request(**r) - received = json.loads(response.content) + received = json.loads(response.content.decode('utf-8')) self.assertEqual(received['file'], test_string) @@ -87,7 +88,7 @@ class FileUploadTests(TestCase): tdir = tempfile.gettempdir() # This file contains chinese symbols and an accented char in the name. - with open(os.path.join(tdir, UNICODE_FILENAME.encode('utf-8')), 'w+b') as file1: + with open(os.path.join(tdir, UNICODE_FILENAME), 'w+b') as file1: file1.write(b'b' * (2 ** 10)) file1.seek(0) @@ -150,7 +151,7 @@ class FileUploadTests(TestCase): response = self.client.request(**r) # The filenames should have been sanitized by the time it got to the view. - recieved = json.loads(response.content) + recieved = json.loads(response.content.decode('utf-8')) for i, name in enumerate(scary_file_names): got = recieved["file%s" % i] self.assertEqual(got, "hax0rd.txt") @@ -174,7 +175,7 @@ class FileUploadTests(TestCase): 'REQUEST_METHOD': 'POST', 'wsgi.input': client.FakePayload(payload), } - got = json.loads(self.client.request(**r).content) + got = json.loads(self.client.request(**r).content.decode('utf-8')) self.assertTrue(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) def test_truncated_multipart_handled_gracefully(self): @@ -200,7 +201,7 @@ class FileUploadTests(TestCase): 'REQUEST_METHOD': 'POST', 'wsgi.input': client.FakePayload(payload), } - got = json.loads(self.client.request(**r).content) + got = json.loads(self.client.request(**r).content.decode('utf-8')) self.assertEqual(got, {}) def test_empty_multipart_handled_gracefully(self): @@ -215,7 +216,7 @@ class FileUploadTests(TestCase): 'REQUEST_METHOD': 'POST', 'wsgi.input': client.FakePayload(b''), } - got = json.loads(self.client.request(**r).content) + got = json.loads(self.client.request(**r).content.decode('utf-8')) self.assertEqual(got, {}) def test_custom_upload_handler(self): @@ -231,12 +232,12 @@ class FileUploadTests(TestCase): # Small file posting should work. response = self.client.post('/file_uploads/quota/', {'f': smallfile}) - got = json.loads(response.content) + got = json.loads(response.content.decode('utf-8')) self.assertTrue('f' in got) # Large files don't go through. response = self.client.post("/file_uploads/quota/", {'f': bigfile}) - got = json.loads(response.content) + got = json.loads(response.content.decode('utf-8')) self.assertTrue('f' not in got) def test_broken_custom_upload_handler(self): @@ -274,7 +275,7 @@ class FileUploadTests(TestCase): 'field5': 'test7', 'file2': (file2, file2a) }) - got = json.loads(response.content) + got = json.loads(response.content.decode('utf-8')) self.assertEqual(got.get('file1'), 1) self.assertEqual(got.get('file2'), 2) @@ -299,8 +300,8 @@ class FileUploadTests(TestCase): # it raises when there is an attempt to read more than the available bytes: try: client.FakePayload(b'a').read(2) - except Exception as reference_error: - pass + except Exception as err: + reference_error = err # install the custom handler that tries to access request.POST self.client.handler = POSTAccessingHandler() diff --git a/tests/regressiontests/file_uploads/views.py b/tests/regressiontests/file_uploads/views.py index c5d2720e1a..fcf32cecea 100644 --- a/tests/regressiontests/file_uploads/views.py +++ b/tests/regressiontests/file_uploads/views.py @@ -7,6 +7,7 @@ import os from django.core.files.uploadedfile import UploadedFile from django.http import HttpResponse, HttpResponseServerError from django.utils import six +from django.utils.encoding import force_bytes from .models import FileModel, UPLOAD_TO from .tests import UNICODE_FILENAME @@ -45,7 +46,7 @@ def file_upload_view_verify(request): if isinstance(value, UploadedFile): new_hash = hashlib.sha1(value.read()).hexdigest() else: - new_hash = hashlib.sha1(value).hexdigest() + new_hash = hashlib.sha1(force_bytes(value)).hexdigest() if new_hash != submitted_hash: return HttpResponseServerError() @@ -95,7 +96,7 @@ def file_upload_echo_content(request): """ Simple view to echo back the content of uploaded files for tests. """ - r = dict([(k, f.read()) for k, f in request.FILES.items()]) + r = dict([(k, f.read().decode('utf-8')) for k, f in request.FILES.items()]) return HttpResponse(json.dumps(r)) def file_upload_quota(request): diff --git a/tests/regressiontests/fixtures_regress/models.py b/tests/regressiontests/fixtures_regress/models.py index 7151cb0ed9..1cc30d2e51 100644 --- a/tests/regressiontests/fixtures_regress/models.py +++ b/tests/regressiontests/fixtures_regress/models.py @@ -3,8 +3,10 @@ from __future__ import absolute_import, unicode_literals from django.contrib.auth.models import User from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Animal(models.Model): name = models.CharField(max_length=150) latin_name = models.CharField(max_length=150) @@ -14,7 +16,7 @@ class Animal(models.Model): # use a non-default name for the default manager specimens = models.Manager() - def __unicode__(self): + def __str__(self): return self.name @@ -25,11 +27,12 @@ class Plant(models.Model): # For testing when upper case letter in app name; regression for #4057 db_table = "Fixtures_regress_plant" +@python_2_unicode_compatible class Stuff(models.Model): name = models.CharField(max_length=20, null=True) owner = models.ForeignKey(User, null=True) - def __unicode__(self): + def __str__(self): return six.text_type(self.name) + ' is owned by ' + six.text_type(self.owner) @@ -68,13 +71,14 @@ class Article(models.Model): # Models to regression test #11428 +@python_2_unicode_compatible class Widget(models.Model): name = models.CharField(max_length=255) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name @@ -89,6 +93,7 @@ class TestManager(models.Manager): return self.get(name=key) +@python_2_unicode_compatible class Store(models.Model): objects = TestManager() name = models.CharField(max_length=255) @@ -97,13 +102,14 @@ class Store(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name def natural_key(self): return (self.name,) +@python_2_unicode_compatible class Person(models.Model): objects = TestManager() name = models.CharField(max_length=255) @@ -111,7 +117,7 @@ class Person(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name # Person doesn't actually have a dependency on store, but we need to define @@ -121,6 +127,7 @@ class Person(models.Model): natural_key.dependencies = ['fixtures_regress.store'] +@python_2_unicode_compatible class Book(models.Model): name = models.CharField(max_length=255) author = models.ForeignKey(Person) @@ -129,7 +136,7 @@ class Book(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return '%s by %s (available at %s)' % ( self.name, self.author.name, @@ -142,6 +149,7 @@ class NKManager(models.Manager): return self.get(data=data) +@python_2_unicode_compatible class NKChild(Parent): data = models.CharField(max_length=10, unique=True) objects = NKManager() @@ -149,16 +157,17 @@ class NKChild(Parent): def natural_key(self): return self.data - def __unicode__(self): + def __str__(self): return 'NKChild %s:%s' % (self.name, self.data) +@python_2_unicode_compatible class RefToNKChild(models.Model): text = models.CharField(max_length=10) nk_fk = models.ForeignKey(NKChild, related_name='ref_fks') nk_m2m = models.ManyToManyField(NKChild, related_name='ref_m2ms') - def __unicode__(self): + def __str__(self): return '%s: Reference to %s [%s]' % ( self.text, self.nk_fk, diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index ab93341699..b3d0f42465 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -4,7 +4,6 @@ from __future__ import absolute_import, unicode_literals import os import re -from io import BytesIO from django.core import management from django.core.management.base import CommandError @@ -14,6 +13,7 @@ from django.db.models import signals from django.test import (TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature) from django.test.utils import override_settings +from django.utils.six import PY3, StringIO from .models import (Animal, Stuff, Absolute, Parent, Child, Article, Widget, Store, Person, Book, NKChild, RefToNKChild, Circle1, Circle2, Circle3, @@ -126,6 +126,20 @@ class TestFixtures(TestCase): commit=False, ) + @override_settings(SERIALIZATION_MODULES={'unkn': 'unexistent.path'}) + def test_unimportable_serializer(self): + """ + Test that failing serializer import raises the proper error + """ + with self.assertRaisesRegexp(ImportError, + "No module named unexistent.path"): + management.call_command( + 'loaddata', + 'bad_fixture1.unkn', + verbosity=0, + commit=False, + ) + def test_invalid_data(self): """ Test for ticket #4371 -- Loading a fixture file with invalid data @@ -244,7 +258,8 @@ class TestFixtures(TestCase): self.assertEqual( pre_save_checks, [ - ("Count = 42 (<type 'int'>)", "Weight = 1.2 (<type 'float'>)") + ("Count = 42 (<%s 'int'>)" % ('class' if PY3 else 'type'), + "Weight = 1.2 (<%s 'float'>)" % ('class' if PY3 else 'type')) ] ) finally: @@ -276,7 +291,7 @@ class TestFixtures(TestCase): ) animal.save() - stdout = BytesIO() + stdout = StringIO() management.call_command( 'dumpdata', 'fixtures_regress.animal', @@ -305,7 +320,7 @@ class TestFixtures(TestCase): """ Regression for #11428 - Proxy models aren't included when you dumpdata """ - stdout = BytesIO() + stdout = StringIO() # Create an instance of the concrete class widget = Widget.objects.create(name='grommet') management.call_command( @@ -380,7 +395,7 @@ class TestFixtures(TestCase): ) def test_loaddata_not_existant_fixture_file(self): - stdout_output = BytesIO() + stdout_output = StringIO() management.call_command( 'loaddata', 'this_fixture_doesnt_exist', @@ -465,7 +480,7 @@ class NaturalKeyFixtureTests(TestCase): commit=False ) - stdout = BytesIO() + stdout = StringIO() management.call_command( 'dumpdata', 'fixtures_regress.book', diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index 18e6ddce6d..2f3ee9fa31 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -7,6 +7,7 @@ import tempfile from django.core.files.storage import FileSystemStorage from django.db import models +from django.utils.encoding import python_2_unicode_compatible temp_storage_location = tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) @@ -36,6 +37,7 @@ class ChoiceModel(models.Model): name = models.CharField(max_length=10) +@python_2_unicode_compatible class ChoiceOptionModel(models.Model): """Destination for ChoiceFieldModel's ForeignKey. Can't reuse ChoiceModel because error_message tests require that it have no instances.""" @@ -44,7 +46,7 @@ class ChoiceOptionModel(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return 'ChoiceOption %d' % self.pk @@ -66,10 +68,11 @@ class FileModel(models.Model): file = models.FileField(storage=temp_storage, upload_to='tests') +@python_2_unicode_compatible class Group(models.Model): name = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return '%s' % self.name diff --git a/tests/regressiontests/forms/tests/error_messages.py b/tests/regressiontests/forms/tests/error_messages.py index f69b419483..b76e122bc0 100644 --- a/tests/regressiontests/forms/tests/error_messages.py +++ b/tests/regressiontests/forms/tests/error_messages.py @@ -5,6 +5,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.test import TestCase from django.utils.safestring import mark_safe +from django.utils.encoding import python_2_unicode_compatible class AssertFormErrorsMixin(object): @@ -213,8 +214,9 @@ class FormsErrorMessagesTestCase(TestCase, AssertFormErrorsMixin): def clean(self): raise ValidationError("I like to be awkward.") + @python_2_unicode_compatible class CustomErrorList(util.ErrorList): - def __unicode__(self): + def __str__(self): return self.as_divs() def as_divs(self): diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index e0e62858d0..2ab5d40942 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -8,8 +8,9 @@ from django.forms import * from django.forms.extras import SelectDateWidget from django.forms.util import ErrorList from django.test import TestCase +from django.utils import six from django.utils import translation -from django.utils.encoding import force_text, smart_text +from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible from .error_messages import AssertFormErrorsMixin @@ -553,14 +554,24 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): def test_smart_text(self): class Test: - def __str__(self): - return 'ŠĐĆŽćžšđ'.encode('utf-8') + if six.PY3: + def __str__(self): + return 'ŠĐĆŽćžšđ' + else: + def __str__(self): + return 'ŠĐĆŽćžšđ'.encode('utf-8') class TestU: - def __str__(self): - return 'Foo' - def __unicode__(self): - return '\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111' + if six.PY3: + def __str__(self): + return 'ŠĐĆŽćžšđ' + def __bytes__(self): + return b'Foo' + else: + def __str__(self): + return b'Foo' + def __unicode__(self): + return '\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111' self.assertEqual(smart_text(Test()), '\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111') self.assertEqual(smart_text(TestU()), '\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111') @@ -585,8 +596,9 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): self.assertEqual(f.cleaned_data['username'], 'sirrobin') def test_overriding_errorlist(self): + @python_2_unicode_compatible class DivErrorList(ErrorList): - def __unicode__(self): + def __str__(self): return self.as_divs() def as_divs(self): diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index feb2ade458..6efdb9682f 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -668,6 +668,18 @@ class FieldsTests(SimpleTestCase): # Valid IDN self.assertEqual(url, f.clean(url)) + def test_urlfield_10(self): + """Test URLField correctly validates IPv6 (#18779).""" + f = URLField() + urls = ( + 'http://::/', + 'http://6:21b4:92/', + 'http://[12:34:3a53]/', + 'http://[a34:9238::]:8080/', + ) + for url in urls: + self.assertEqual(url, f.clean(url)) + def test_urlfield_not_string(self): f = URLField(required=False) self.assertRaisesMessage(ValidationError, "'Enter a valid URL.'", f.clean, 23) diff --git a/tests/regressiontests/forms/tests/models.py b/tests/regressiontests/forms/tests/models.py index 7687335b48..c351509cee 100644 --- a/tests/regressiontests/forms/tests/models.py +++ b/tests/regressiontests/forms/tests/models.py @@ -178,7 +178,7 @@ class RelatedModelFormTests(TestCase): class Meta: model=A - self.assertRaises(ValueError, ModelFormMetaclass, b'Form', (ModelForm,), {'Meta': Meta}) + self.assertRaises(ValueError, ModelFormMetaclass, str('Form'), (ModelForm,), {'Meta': Meta}) class B(models.Model): pass @@ -196,4 +196,4 @@ class RelatedModelFormTests(TestCase): class Meta: model=A - self.assertTrue(issubclass(ModelFormMetaclass(b'Form', (ModelForm,), {'Meta': Meta}), ModelForm)) + self.assertTrue(issubclass(ModelFormMetaclass(str('Form'), (ModelForm,), {'Meta': Meta}), ModelForm)) diff --git a/tests/regressiontests/forms/tests/util.py b/tests/regressiontests/forms/tests/util.py index b7cc4ec809..c2d213ae23 100644 --- a/tests/regressiontests/forms/tests/util.py +++ b/tests/regressiontests/forms/tests/util.py @@ -7,6 +7,7 @@ from django.test import TestCase from django.utils.safestring import mark_safe from django.utils import six from django.utils.translation import ugettext_lazy +from django.utils.encoding import python_2_unicode_compatible class FormsUtilTestCase(TestCase): @@ -46,8 +47,9 @@ class FormsUtilTestCase(TestCase): self.assertHTMLEqual(str(ErrorList(ValidationError(["First error.", "Not \u03C0.", ugettext_lazy("Error.")]).messages)), '<ul class="errorlist"><li>First error.</li><li>Not π.</li><li>Error.</li></ul>') + @python_2_unicode_compatible class VeryBadError: - def __unicode__(self): return "A very bad error." + def __str__(self): return "A very bad error." # Can take a non-string. self.assertHTMLEqual(str(ErrorList(ValidationError(VeryBadError()).messages)), diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 3ea42cf549..c950aef719 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -13,6 +13,7 @@ from django.utils.safestring import mark_safe from django.utils import six from django.utils.translation import activate, deactivate from django.test import TestCase +from django.utils.encoding import python_2_unicode_compatible class FormsWidgetTestCase(TestCase): @@ -1096,6 +1097,7 @@ class WidgetTests(TestCase): self.assertFalse(form.is_valid()) +@python_2_unicode_compatible class FakeFieldFile(object): """ Quacks like a FieldFile (has a .url and unicode representation), but @@ -1104,7 +1106,7 @@ class FakeFieldFile(object): """ url = 'something' - def __unicode__(self): + def __str__(self): return self.url class ClearableFileInputTests(TestCase): @@ -1125,10 +1127,11 @@ class ClearableFileInputTests(TestCase): rendering HTML. Refs #15182. """ + @python_2_unicode_compatible class StrangeFieldFile(object): url = "something?chapter=1§=2©=3&lang=en" - def __unicode__(self): + def __str__(self): return '''something<div onclick="alert('oops')">.jpg''' widget = ClearableFileInput() diff --git a/tests/regressiontests/generic_inline_admin/models.py b/tests/regressiontests/generic_inline_admin/models.py index a1555666b3..dedc351075 100644 --- a/tests/regressiontests/generic_inline_admin/models.py +++ b/tests/regressiontests/generic_inline_admin/models.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Episode(models.Model): @@ -9,6 +10,7 @@ class Episode(models.Model): author = models.CharField(max_length=100, blank=True) +@python_2_unicode_compatible class Media(models.Model): """ Media that can associated to any object. @@ -20,7 +22,7 @@ class Media(models.Model): description = models.CharField(max_length=100, blank=True) keywords = models.CharField(max_length=100, blank=True) - def __unicode__(self): + def __str__(self): return self.url # diff --git a/tests/regressiontests/generic_inline_admin/tests.py b/tests/regressiontests/generic_inline_admin/tests.py index c97f4c4f03..fea30b4946 100644 --- a/tests/regressiontests/generic_inline_admin/tests.py +++ b/tests/regressiontests/generic_inline_admin/tests.py @@ -276,7 +276,7 @@ class GenericInlineModelAdminTest(TestCase): ma = EpisodeAdmin(Episode, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), + list(list(ma.get_formsets(request))[0]().forms[0].fields), ['keywords', 'id', 'DELETE']) def test_custom_form_meta_exclude(self): @@ -306,7 +306,7 @@ class GenericInlineModelAdminTest(TestCase): ma = EpisodeAdmin(Episode, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), + list(list(ma.get_formsets(request))[0]().forms[0].fields), ['url', 'keywords', 'id', 'DELETE']) # Then, only with `ModelForm` ----------------- @@ -322,5 +322,5 @@ class GenericInlineModelAdminTest(TestCase): ma = EpisodeAdmin(Episode, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), + list(list(ma.get_formsets(request))[0]().forms[0].fields), ['description', 'keywords', 'id', 'DELETE']) diff --git a/tests/regressiontests/generic_relations_regress/models.py b/tests/regressiontests/generic_relations_regress/models.py index 7ec752b02b..dacc9b380b 100644 --- a/tests/regressiontests/generic_relations_regress/models.py +++ b/tests/regressiontests/generic_relations_regress/models.py @@ -1,31 +1,36 @@ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.encoding import python_2_unicode_compatible __all__ = ('Link', 'Place', 'Restaurant', 'Person', 'Address', 'CharLink', 'TextLink', 'OddRelation1', 'OddRelation2', 'Contact', 'Organization', 'Note') +@python_2_unicode_compatible class Link(models.Model): content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey() - def __unicode__(self): + def __str__(self): return "Link to %s id=%s" % (self.content_type, self.object_id) +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=100) links = generic.GenericRelation(Link) - def __unicode__(self): + def __str__(self): return "Place: %s" % self.name +@python_2_unicode_compatible class Restaurant(Place): - def __unicode__(self): + def __str__(self): return "Restaurant: %s" % self.name +@python_2_unicode_compatible class Address(models.Model): street = models.CharField(max_length=80) city = models.CharField(max_length=50) @@ -35,15 +40,16 @@ class Address(models.Model): object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey() - def __unicode__(self): + def __str__(self): return '%s %s, %s %s' % (self.street, self.city, self.state, self.zipcode) +@python_2_unicode_compatible class Person(models.Model): account = models.IntegerField(primary_key=True) name = models.CharField(max_length=128) addresses = generic.GenericRelation(Address) - def __unicode__(self): + def __str__(self): return self.name class CharLink(models.Model): diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py index fc3dc05fd7..439b0d7327 100644 --- a/tests/regressiontests/generic_views/base.py +++ b/tests/regressiontests/generic_views/base.py @@ -69,7 +69,7 @@ class ViewTest(unittest.TestCase): def _assert_simple(self, response): self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'This is a simple view') + self.assertEqual(response.content, b'This is a simple view') def test_no_init_kwargs(self): """ @@ -173,15 +173,6 @@ class ViewTest(unittest.TestCase): """ self.assertTrue(DecoratedDispatchView.as_view().is_decorated) - def test_head_no_get(self): - """ - Test that a view class with no get responds to a HEAD request with HTTP - 405. - """ - request = self.rf.head('/') - view = PostOnlyView.as_view() - self.assertEqual(405, view(request).status_code) - def test_options(self): """ Test that views respond to HTTP OPTIONS requests with an Allow header @@ -275,7 +266,8 @@ class TemplateViewTest(TestCase): """ response = self.client.get('/template/simple/bar/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['params'], {'foo': 'bar'}) + self.assertEqual(response.context['foo'], 'bar') + self.assertTrue(isinstance(response.context['view'], View)) def test_extra_template_params(self): """ @@ -283,8 +275,9 @@ class TemplateViewTest(TestCase): """ response = self.client.get('/template/custom/bar/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['params'], {'foo': 'bar'}) + self.assertEqual(response.context['foo'], 'bar') self.assertEqual(response.context['key'], 'value') + self.assertTrue(isinstance(response.context['view'], View)) def test_cached_views(self): """ diff --git a/tests/regressiontests/generic_views/dates.py b/tests/regressiontests/generic_views/dates.py index ad65fd9f10..c2fa71b376 100644 --- a/tests/regressiontests/generic_views/dates.py +++ b/tests/regressiontests/generic_views/dates.py @@ -1,14 +1,27 @@ from __future__ import absolute_import +import time import datetime from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone +from django.utils.unittest import skipUnless from .models import Book, BookSigning +TZ_SUPPORT = hasattr(time, 'tzset') + +# On OSes that don't provide tzset (Windows), we can't set the timezone +# in which the program runs. As a consequence, we must skip tests that +# don't enforce a specific timezone (with timezone.override or equivalent), +# or attempt to interpret naive datetimes in the default timezone. + +requires_tz_support = skipUnless(TZ_SUPPORT, + "This test relies on the ability to run a program in an arbitrary " + "time zone, but your operating system isn't able to do that.") + class ArchiveIndexViewTests(TestCase): fixtures = ['generic-views-test-data.json'] @@ -47,7 +60,6 @@ class ArchiveIndexViewTests(TestCase): res = self.client.get('/dates/books/allow_empty/') self.assertEqual(res.status_code, 200) self.assertEqual(list(res.context['date_list']), []) - self.assertEqual(list(res.context['date_list']), []) self.assertTemplateUsed(res, 'generic_views/book_archive.html') def test_archive_view_template(self): @@ -67,6 +79,11 @@ class ArchiveIndexViewTests(TestCase): def test_archive_view_invalid(self): self.assertRaises(ImproperlyConfigured, self.client.get, '/dates/books/invalid/') + def test_archive_view_by_month(self): + res = self.client.get('/dates/books/by_month/') + self.assertEqual(res.status_code, 200) + self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'month')[::-1]) + def test_paginated_archive_view(self): self._make_books(20, base_date=datetime.date.today()) res = self.client.get('/dates/books/paginated/') @@ -100,6 +117,7 @@ class ArchiveIndexViewTests(TestCase): res = self.client.get('/dates/booksignings/') self.assertEqual(res.status_code, 200) + @requires_tz_support @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) @@ -480,7 +498,7 @@ class DayArchiveViewTests(TestCase): def test_next_prev_context(self): res = self.client.get('/dates/books/2008/oct/01/') - self.assertEqual(res.content, "Archive for Oct. 1, 2008. Previous day is May 1, 2006") + self.assertEqual(res.content, b"Archive for Oct. 1, 2008. Previous day is May 1, 2006") def test_custom_month_format(self): res = self.client.get('/dates/books/2008/10/01/') @@ -502,6 +520,7 @@ class DayArchiveViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/apr/2/') self.assertEqual(res.status_code, 200) + @requires_tz_support @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_day_view(self): bs = BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) @@ -578,6 +597,7 @@ class DateDetailViewTests(TestCase): res = self.client.get('/dates/booksignings/2008/apr/2/%d/' % bs.pk) self.assertEqual(res.status_code, 200) + @requires_tz_support @override_settings(USE_TZ=True, TIME_ZONE='Africa/Nairobi') def test_aware_datetime_date_detail(self): bs = BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0, tzinfo=timezone.utc)) diff --git a/tests/regressiontests/generic_views/detail.py b/tests/regressiontests/generic_views/detail.py index 60c5f7afe9..c8f1999a2a 100644 --- a/tests/regressiontests/generic_views/detail.py +++ b/tests/regressiontests/generic_views/detail.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from django.core.exceptions import ImproperlyConfigured from django.test import TestCase +from django.views.generic.base import View from .models import Artist, Author, Page @@ -14,6 +15,7 @@ class DetailViewTest(TestCase): res = self.client.get('/detail/obj/') self.assertEqual(res.status_code, 200) self.assertEqual(res.context['object'], {'foo': 'bar'}) + self.assertTrue(isinstance(res.context['view'], View)) self.assertTemplateUsed(res, 'generic_views/detail.html') def test_detail_by_pk(self): diff --git a/tests/regressiontests/generic_views/edit.py b/tests/regressiontests/generic_views/edit.py index 651e14fff3..16f4da8efe 100644 --- a/tests/regressiontests/generic_views/edit.py +++ b/tests/regressiontests/generic_views/edit.py @@ -5,6 +5,7 @@ from django.core.urlresolvers import reverse from django import forms from django.test import TestCase from django.utils.unittest import expectedFailure +from django.views.generic.base import View from django.views.generic.edit import FormMixin from . import views @@ -31,6 +32,7 @@ class CreateViewTests(TestCase): res = self.client.get('/edit/authors/create/') self.assertEqual(res.status_code, 200) self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) + self.assertTrue(isinstance(res.context['view'], View)) self.assertFalse('object' in res.context) self.assertFalse('author' in res.context) self.assertTemplateUsed(res, 'generic_views/author_form.html') @@ -101,6 +103,7 @@ class CreateViewTests(TestCase): self.assertEqual(res.status_code, 302) self.assertRedirects(res, 'http://testserver/accounts/login/?next=/edit/authors/create/restricted/') + class UpdateViewTests(TestCase): urls = 'regressiontests.generic_views.urls' @@ -226,6 +229,7 @@ class UpdateViewTests(TestCase): res = self.client.get('/edit/author/update/') self.assertEqual(res.status_code, 200) self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) + self.assertTrue(isinstance(res.context['view'], View)) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk)) self.assertTemplateUsed(res, 'generic_views/author_form.html') @@ -237,6 +241,7 @@ class UpdateViewTests(TestCase): self.assertRedirects(res, 'http://testserver/list/authors/') self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe (xkcd)>']) + class DeleteViewTests(TestCase): urls = 'regressiontests.generic_views.urls' diff --git a/tests/regressiontests/generic_views/list.py b/tests/regressiontests/generic_views/list.py index b925758524..14dc1d725d 100644 --- a/tests/regressiontests/generic_views/list.py +++ b/tests/regressiontests/generic_views/list.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from django.core.exceptions import ImproperlyConfigured from django.test import TestCase +from django.views.generic.base import View from .models import Author, Artist @@ -21,6 +22,7 @@ class ListViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertTemplateUsed(res, 'generic_views/author_list.html') self.assertEqual(list(res.context['object_list']), list(Author.objects.all())) + self.assertTrue(isinstance(res.context['view'], View)) self.assertIs(res.context['author_list'], res.context['object_list']) self.assertIsNone(res.context['paginator']) self.assertIsNone(res.context['page_obj']) diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py index 2355769d58..f59389ef78 100644 --- a/tests/regressiontests/generic_views/models.py +++ b/tests/regressiontests/generic_views/models.py @@ -1,6 +1,8 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Artist(models.Model): name = models.CharField(max_length=100) @@ -9,13 +11,14 @@ class Artist(models.Model): verbose_name = 'professional artist' verbose_name_plural = 'professional artists' - def __unicode__(self): + def __str__(self): return self.name @models.permalink def get_absolute_url(self): return ('artist_detail', (), {'pk': self.id}) +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=100) slug = models.SlugField() @@ -23,9 +26,10 @@ class Author(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Book(models.Model): name = models.CharField(max_length=300) slug = models.SlugField() @@ -36,7 +40,7 @@ class Book(models.Model): class Meta: ordering = ['-pubdate'] - def __unicode__(self): + def __str__(self): return self.name class Page(models.Model): diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py index 5e15c6c9c1..c72bfecb65 100644 --- a/tests/regressiontests/generic_views/urls.py +++ b/tests/regressiontests/generic_views/urls.py @@ -113,6 +113,8 @@ urlpatterns = patterns('', views.BookArchive.as_view(paginate_by=10)), (r'^dates/books/reverse/$', views.BookArchive.as_view(queryset=models.Book.objects.order_by('pubdate'))), + (r'^dates/books/by_month/$', + views.BookArchive.as_view(date_list_period='month')), (r'^dates/booksignings/$', views.BookSigningArchive.as_view()), diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 0f61c2d840..21ba198bc3 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -6,19 +6,22 @@ import pickle from django.core.exceptions import SuspiciousOperation from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, - HttpResponsePermanentRedirect, + HttpResponsePermanentRedirect, HttpResponseNotAllowed, + HttpResponseNotModified, SimpleCookie, BadHeaderError, parse_cookie) +from django.test import TestCase +from django.utils import six from django.utils import unittest class QueryDictTests(unittest.TestCase): def test_missing_key(self): - q = QueryDict('') + q = QueryDict(str('')) self.assertRaises(KeyError, q.__getitem__, 'foo') def test_immutability(self): - q = QueryDict('') + q = QueryDict(str('')) self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar') self.assertRaises(AttributeError, q.setlist, 'foo', ['bar']) self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar']) @@ -28,26 +31,26 @@ class QueryDictTests(unittest.TestCase): self.assertRaises(AttributeError, q.clear) def test_immutable_get_with_default(self): - q = QueryDict('') + q = QueryDict(str('')) self.assertEqual(q.get('foo', 'default'), 'default') def test_immutable_basic_operations(self): - q = QueryDict('') + q = QueryDict(str('')) self.assertEqual(q.getlist('foo'), []) - self.assertEqual(q.has_key('foo'), False) + if not six.PY3: + self.assertEqual(q.has_key('foo'), False) self.assertEqual('foo' in q, False) - self.assertEqual(q.items(), []) - self.assertEqual(q.lists(), []) - self.assertEqual(q.items(), []) - self.assertEqual(q.keys(), []) - self.assertEqual(q.values(), []) + self.assertEqual(list(six.iteritems(q)), []) + self.assertEqual(list(six.iterlists(q)), []) + self.assertEqual(list(six.iterkeys(q)), []) + self.assertEqual(list(six.itervalues(q)), []) self.assertEqual(len(q), 0) self.assertEqual(q.urlencode(), '') def test_single_key_value(self): """Test QueryDict with one key/value pair""" - q = QueryDict('foo=bar') + q = QueryDict(str('foo=bar')) self.assertEqual(q['foo'], 'bar') self.assertRaises(KeyError, q.__getitem__, 'bar') self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar') @@ -60,15 +63,17 @@ class QueryDictTests(unittest.TestCase): self.assertRaises(AttributeError, q.setlist, 'foo', ['bar']) self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar']) - self.assertTrue(q.has_key('foo')) + if not six.PY3: + self.assertTrue(q.has_key('foo')) self.assertTrue('foo' in q) - self.assertFalse(q.has_key('bar')) + if not six.PY3: + self.assertFalse(q.has_key('bar')) self.assertFalse('bar' in q) - self.assertEqual(q.items(), [('foo', 'bar')]) - self.assertEqual(q.lists(), [('foo', ['bar'])]) - self.assertEqual(q.keys(), ['foo']) - self.assertEqual(q.values(), ['bar']) + self.assertEqual(list(six.iteritems(q)), [('foo', 'bar')]) + self.assertEqual(list(six.iterlists(q)), [('foo', ['bar'])]) + self.assertEqual(list(six.iterkeys(q)), ['foo']) + self.assertEqual(list(six.itervalues(q)), ['bar']) self.assertEqual(len(q), 1) self.assertRaises(AttributeError, q.update, {'foo': 'bar'}) @@ -80,30 +85,30 @@ class QueryDictTests(unittest.TestCase): self.assertEqual(q.urlencode(), 'foo=bar') def test_urlencode(self): - q = QueryDict('', mutable=True) + q = QueryDict(str(''), mutable=True) q['next'] = '/a&b/' self.assertEqual(q.urlencode(), 'next=%2Fa%26b%2F') self.assertEqual(q.urlencode(safe='/'), 'next=/a%26b/') - q = QueryDict('', mutable=True) + q = QueryDict(str(''), mutable=True) q['next'] = '/t\xebst&key/' self.assertEqual(q.urlencode(), 'next=%2Ft%C3%ABst%26key%2F') self.assertEqual(q.urlencode(safe='/'), 'next=/t%C3%ABst%26key/') def test_mutable_copy(self): """A copy of a QueryDict is mutable.""" - q = QueryDict('').copy() + q = QueryDict(str('')).copy() self.assertRaises(KeyError, q.__getitem__, "foo") q['name'] = 'john' self.assertEqual(q['name'], 'john') def test_mutable_delete(self): - q = QueryDict('').copy() + q = QueryDict(str('')).copy() q['name'] = 'john' del q['name'] self.assertFalse('name' in q) def test_basic_mutable_operations(self): - q = QueryDict('').copy() + q = QueryDict(str('')).copy() q['name'] = 'john' self.assertEqual(q.get('foo', 'default'), 'default') self.assertEqual(q.get('name', 'default'), 'john') @@ -117,13 +122,14 @@ class QueryDictTests(unittest.TestCase): q.appendlist('foo', 'another') self.assertEqual(q.getlist('foo'), ['bar', 'baz', 'another']) self.assertEqual(q['foo'], 'another') - self.assertTrue(q.has_key('foo')) + if not six.PY3: + self.assertTrue(q.has_key('foo')) self.assertTrue('foo' in q) - self.assertEqual(q.items(), [('foo', 'another'), ('name', 'john')]) - self.assertEqual(q.lists(), [('foo', ['bar', 'baz', 'another']), ('name', ['john'])]) - self.assertEqual(q.keys(), ['foo', 'name']) - self.assertEqual(q.values(), ['another', 'john']) + self.assertEqual(list(six.iteritems(q)), [('foo', 'another'), ('name', 'john')]) + self.assertEqual(list(six.iterlists(q)), [('foo', ['bar', 'baz', 'another']), ('name', ['john'])]) + self.assertEqual(list(six.iterkeys(q)), ['foo', 'name']) + self.assertEqual(list(six.itervalues(q)), ['another', 'john']) self.assertEqual(len(q), 2) q.update({'foo': 'hello'}) @@ -144,7 +150,7 @@ class QueryDictTests(unittest.TestCase): def test_multiple_keys(self): """Test QueryDict with two key/value pairs with same keys.""" - q = QueryDict('vote=yes&vote=no') + q = QueryDict(str('vote=yes&vote=no')) self.assertEqual(q['vote'], 'no') self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar') @@ -158,14 +164,16 @@ class QueryDictTests(unittest.TestCase): self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz']) self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar']) - self.assertEqual(q.has_key('vote'), True) + if not six.PY3: + self.assertEqual(q.has_key('vote'), True) self.assertEqual('vote' in q, True) - self.assertEqual(q.has_key('foo'), False) + if not six.PY3: + self.assertEqual(q.has_key('foo'), False) self.assertEqual('foo' in q, False) - self.assertEqual(q.items(), [('vote', 'no')]) - self.assertEqual(q.lists(), [('vote', ['yes', 'no'])]) - self.assertEqual(q.keys(), ['vote']) - self.assertEqual(q.values(), ['no']) + self.assertEqual(list(six.iteritems(q)), [('vote', 'no')]) + self.assertEqual(list(six.iterlists(q)), [('vote', ['yes', 'no'])]) + self.assertEqual(list(six.iterkeys(q)), ['vote']) + self.assertEqual(list(six.itervalues(q)), ['no']) self.assertEqual(len(q), 1) self.assertRaises(AttributeError, q.update, {'foo': 'bar'}) @@ -175,45 +183,49 @@ class QueryDictTests(unittest.TestCase): self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar') self.assertRaises(AttributeError, q.__delitem__, 'vote') - def test_invalid_input_encoding(self): - """ - QueryDicts must be able to handle invalid input encoding (in this - case, bad UTF-8 encoding). - """ - q = QueryDict(b'foo=bar&foo=\xff') - self.assertEqual(q['foo'], '\ufffd') - self.assertEqual(q.getlist('foo'), ['bar', '\ufffd']) + if not six.PY3: + def test_invalid_input_encoding(self): + """ + QueryDicts must be able to handle invalid input encoding (in this + case, bad UTF-8 encoding). + + This test doesn't apply under Python 3 because the URL is a string + and not a bytestring. + """ + q = QueryDict(str(b'foo=bar&foo=\xff')) + self.assertEqual(q['foo'], '\ufffd') + self.assertEqual(q.getlist('foo'), ['bar', '\ufffd']) def test_pickle(self): - q = QueryDict('') + q = QueryDict(str('')) q1 = pickle.loads(pickle.dumps(q, 2)) self.assertEqual(q == q1, True) - q = QueryDict('a=b&c=d') + q = QueryDict(str('a=b&c=d')) q1 = pickle.loads(pickle.dumps(q, 2)) self.assertEqual(q == q1, True) - q = QueryDict('a=b&c=d&a=1') + q = QueryDict(str('a=b&c=d&a=1')) q1 = pickle.loads(pickle.dumps(q, 2)) self.assertEqual(q == q1, True) def test_update_from_querydict(self): """Regression test for #8278: QueryDict.update(QueryDict)""" - x = QueryDict("a=1&a=2", mutable=True) - y = QueryDict("a=3&a=4") + x = QueryDict(str("a=1&a=2"), mutable=True) + y = QueryDict(str("a=3&a=4")) x.update(y) self.assertEqual(x.getlist('a'), ['1', '2', '3', '4']) def test_non_default_encoding(self): """#13572 - QueryDict with a non-default encoding""" - q = QueryDict(b'sbb=one', encoding='rot_13') - self.assertEqual(q.encoding , 'rot_13' ) - self.assertEqual(q.items() , [('foo', 'bar')] ) - self.assertEqual(q.urlencode() , 'sbb=one' ) + q = QueryDict(str('cur=%A4'), encoding='iso-8859-15') + self.assertEqual(q.encoding, 'iso-8859-15') + self.assertEqual(list(six.iteritems(q)), [('cur', '€')]) + self.assertEqual(q.urlencode(), 'cur=%A4') q = q.copy() - self.assertEqual(q.encoding , 'rot_13' ) - self.assertEqual(q.items() , [('foo', 'bar')] ) - self.assertEqual(q.urlencode() , 'sbb=one' ) - self.assertEqual(copy.copy(q).encoding , 'rot_13' ) - self.assertEqual(copy.deepcopy(q).encoding , 'rot_13') + self.assertEqual(q.encoding, 'iso-8859-15') + self.assertEqual(list(six.iteritems(q)), [('cur', '€')]) + self.assertEqual(q.urlencode(), 'cur=%A4') + self.assertEqual(copy.copy(q).encoding, 'iso-8859-15') + self.assertEqual(copy.deepcopy(q).encoding, 'iso-8859-15') class HttpResponseTests(unittest.TestCase): def test_unicode_headers(self): @@ -260,45 +272,40 @@ class HttpResponseTests(unittest.TestCase): def test_non_string_content(self): #Bug 16494: HttpResponse should behave consistently with non-strings r = HttpResponse(12345) - self.assertEqual(r.content, '12345') + self.assertEqual(r.content, b'12345') #test content via property r = HttpResponse() r.content = 12345 - self.assertEqual(r.content, '12345') + self.assertEqual(r.content, b'12345') def test_iter_content(self): r = HttpResponse(['abc', 'def', 'ghi']) - self.assertEqual(r.content, 'abcdefghi') + self.assertEqual(r.content, b'abcdefghi') #test iter content via property r = HttpResponse() r.content = ['idan', 'alex', 'jacob'] - self.assertEqual(r.content, 'idanalexjacob') + self.assertEqual(r.content, b'idanalexjacob') r = HttpResponse() r.content = [1, 2, 3] - self.assertEqual(r.content, '123') + self.assertEqual(r.content, b'123') #test retrieval explicitly using iter and odd inputs r = HttpResponse() - r.content = ['1', '2', 3, unichr(1950)] - result = [] + r.content = ['1', '2', 3, '\u079e'] my_iter = r.__iter__() - while True: - try: - result.append(next(my_iter)) - except StopIteration: - break + result = list(my_iter) #'\xde\x9e' == unichr(1950).encode('utf-8') - self.assertEqual(result, ['1', '2', '3', b'\xde\x9e']) + self.assertEqual(result, [b'1', b'2', b'3', b'\xde\x9e']) self.assertEqual(r.content, b'123\xde\x9e') #with Content-Encoding header r = HttpResponse([1,1,2,4,8]) r['Content-Encoding'] = 'winning' - self.assertEqual(r.content, '11248') - r.content = [unichr(1950),] + self.assertEqual(r.content, b'11248') + r.content = ['\u079e',] self.assertRaises(UnicodeEncodeError, getattr, r, 'content') @@ -325,6 +332,33 @@ class HttpResponseTests(unittest.TestCase): HttpResponsePermanentRedirect, url) +class HttpResponseSubclassesTests(TestCase): + def test_redirect(self): + response = HttpResponseRedirect('/redirected/') + self.assertEqual(response.status_code, 302) + # Test that standard HttpResponse init args can be used + response = HttpResponseRedirect('/redirected/', + content='The resource has temporarily moved', + content_type='text/html') + self.assertContains(response, 'The resource has temporarily moved', status_code=302) + + def test_not_modified(self): + response = HttpResponseNotModified() + self.assertEqual(response.status_code, 304) + # 304 responses should not have content/content-type + with self.assertRaises(AttributeError): + response.content = "Hello dear" + self.assertNotIn('content-type', response) + + def test_not_allowed(self): + response = HttpResponseNotAllowed(['GET']) + self.assertEqual(response.status_code, 405) + # Test that standard HttpResponse init args can be used + response = HttpResponseNotAllowed(['GET'], + content='Only the GET method is allowed', + content_type='text/html') + self.assertContains(response, 'Only the GET method is allowed', status_code=405) + class CookieTests(unittest.TestCase): def test_encode(self): """ diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 9ca66bdb9b..bce3d617d2 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -18,7 +18,7 @@ from django.utils.formats import (get_format, date_format, time_format, number_format) from django.utils.importlib import import_module from django.utils.numberformat import format as nformat -from django.utils.safestring import mark_safe, SafeString, SafeUnicode +from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText from django.utils import six from django.utils.six import PY3 from django.utils.translation import (ugettext, ugettext_lazy, activate, @@ -232,12 +232,12 @@ class TranslationTests(TestCase): """ Translating a string requiring no auto-escaping shouldn't change the "safe" status. """ - s = mark_safe(b'Password') + s = mark_safe(str('Password')) self.assertEqual(SafeString, type(s)) with translation.override('de', deactivate=True): - self.assertEqual(SafeUnicode, type(ugettext(s))) - self.assertEqual('aPassword', SafeString('a') + s) - self.assertEqual('Passworda', s + SafeString('a')) + self.assertEqual(SafeText, type(ugettext(s))) + self.assertEqual('aPassword', SafeText('a') + s) + self.assertEqual('Passworda', s + SafeText('a')) self.assertEqual('Passworda', s + mark_safe('a')) self.assertEqual('aPassword', mark_safe('a') + s) self.assertEqual('as', mark_safe('a') + mark_safe('s')) @@ -808,13 +808,13 @@ class MiscTests(TestCase): r.META = {'HTTP_ACCEPT_LANGUAGE': 'de'} self.assertEqual(g(r), 'zh-cn') - def test_get_language_from_path(self): + def test_get_language_from_path_real(self): from django.utils.translation.trans_real import get_language_from_path as g self.assertEqual(g('/pl/'), 'pl') self.assertEqual(g('/pl'), 'pl') self.assertEqual(g('/xyz/'), None) - def test_get_language_from_path(self): + def test_get_language_from_path_null(self): from django.utils.translation.trans_null import get_language_from_path as g self.assertEqual(g('/pl/'), None) self.assertEqual(g('/pl'), None) @@ -897,9 +897,9 @@ class TestModels(TestCase): def test_safestr(self): c = Company(cents_paid=12, products_delivered=1) - c.name = SafeUnicode('Iñtërnâtiônàlizætiøn1') + c.name = SafeText('Iñtërnâtiônàlizætiøn1') c.save() - c.name = SafeString('Iñtërnâtiônàlizætiøn1'.encode('utf-8')) + c.name = SafeBytes('Iñtërnâtiônàlizætiøn1'.encode('utf-8')) c.save() diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py index d76eea758b..40c85fe739 100644 --- a/tests/regressiontests/inline_formsets/models.py +++ b/tests/regressiontests/inline_formsets/models.py @@ -1,5 +1,6 @@ # coding: utf-8 from django.db import models +from django.utils.encoding import python_2_unicode_compatible class School(models.Model): @@ -14,15 +15,17 @@ class Child(models.Model): school = models.ForeignKey(School) name = models.CharField(max_length=100) +@python_2_unicode_compatible class Poet(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Poem(models.Model): poet = models.ForeignKey(Poet) name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/regressiontests/inspectdb/models.py b/tests/regressiontests/inspectdb/models.py index 9f815855b6..4a6621402f 100644 --- a/tests/regressiontests/inspectdb/models.py +++ b/tests/regressiontests/inspectdb/models.py @@ -19,3 +19,12 @@ class DigitsInColumnName(models.Model): all_digits = models.CharField(max_length=11, db_column='123') leading_digit = models.CharField(max_length=11, db_column='4extra') leading_digits = models.CharField(max_length=11, db_column='45extra') + +class SpecialColumnName(models.Model): + field = models.IntegerField(db_column='field') + # Underscores + field_field_0 = models.IntegerField(db_column='Field_') + field_field_1 = models.IntegerField(db_column='Field__') + field_field_2 = models.IntegerField(db_column='__field') + # Other chars + prc_x = models.IntegerField(db_column='prc(%) x') diff --git a/tests/regressiontests/inspectdb/tests.py b/tests/regressiontests/inspectdb/tests.py index aae7bc5cc7..484e7f4060 100644 --- a/tests/regressiontests/inspectdb/tests.py +++ b/tests/regressiontests/inspectdb/tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.core.management import call_command from django.test import TestCase, skipUnlessDBFeature from django.utils.six import StringIO @@ -17,7 +19,6 @@ class InspectDBTestCase(TestCase): # the Django test suite, check that one of its tables hasn't been # inspected self.assertNotIn("class DjangoContentType(models.Model):", out.getvalue(), msg=error_message) - out.close() @skipUnlessDBFeature('can_introspect_foreign_keys') def test_attribute_name_not_python_keyword(self): @@ -27,15 +28,16 @@ class InspectDBTestCase(TestCase): call_command('inspectdb', table_name_filter=lambda tn:tn.startswith('inspectdb_'), stdout=out) + output = out.getvalue() error_message = "inspectdb generated an attribute name which is a python keyword" - self.assertNotIn("from = models.ForeignKey(InspectdbPeople)", out.getvalue(), msg=error_message) + self.assertNotIn("from = models.ForeignKey(InspectdbPeople)", output, msg=error_message) # As InspectdbPeople model is defined after InspectdbMessage, it should be quoted - self.assertIn("from_field = models.ForeignKey('InspectdbPeople')", out.getvalue()) + self.assertIn("from_field = models.ForeignKey('InspectdbPeople', db_column='from_id')", + output) self.assertIn("people_pk = models.ForeignKey(InspectdbPeople, primary_key=True)", - out.getvalue()) + output) self.assertIn("people_unique = models.ForeignKey(InspectdbPeople, unique=True)", - out.getvalue()) - out.close() + output) def test_digits_column_name_introspection(self): """Introspection of column names consist/start with digits (#16536/#17676)""" @@ -45,13 +47,27 @@ class InspectDBTestCase(TestCase): call_command('inspectdb', table_name_filter=lambda tn:tn.startswith('inspectdb_'), stdout=out) + output = out.getvalue() error_message = "inspectdb generated a model field name which is a number" - self.assertNotIn(" 123 = models.CharField", out.getvalue(), msg=error_message) - self.assertIn("number_123 = models.CharField", out.getvalue()) + self.assertNotIn(" 123 = models.CharField", output, msg=error_message) + self.assertIn("number_123 = models.CharField", output) error_message = "inspectdb generated a model field name which starts with a digit" - self.assertNotIn(" 4extra = models.CharField", out.getvalue(), msg=error_message) - self.assertIn("number_4extra = models.CharField", out.getvalue()) + self.assertNotIn(" 4extra = models.CharField", output, msg=error_message) + self.assertIn("number_4extra = models.CharField", output) + + self.assertNotIn(" 45extra = models.CharField", output, msg=error_message) + self.assertIn("number_45extra = models.CharField", output) - self.assertNotIn(" 45extra = models.CharField", out.getvalue(), msg=error_message) - self.assertIn("number_45extra = models.CharField", out.getvalue()) + def test_special_column_name_introspection(self): + """Introspection of column names containing special characters, + unsuitable for Python identifiers + """ + out = StringIO() + call_command('inspectdb', stdout=out) + output = out.getvalue() + self.assertIn("field = models.IntegerField()", output) + self.assertIn("field_field = models.IntegerField(db_column='Field_')", output) + self.assertIn("field_field_0 = models.IntegerField(db_column='Field__')", output) + self.assertIn("field_field_1 = models.IntegerField(db_column='__field')", output) + self.assertIn("prc_x = models.IntegerField(db_column='prc(%) x')", output) diff --git a/tests/regressiontests/introspection/models.py b/tests/regressiontests/introspection/models.py index 3ca80c5aab..6e5beba61d 100644 --- a/tests/regressiontests/introspection/models.py +++ b/tests/regressiontests/introspection/models.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Reporter(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) @@ -12,15 +14,16 @@ class Reporter(models.Model): class Meta: unique_together = ('first_name', 'last_name') - def __unicode__(self): + def __str__(self): return "%s %s" % (self.first_name, self.last_name) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() reporter = models.ForeignKey(Reporter) - def __unicode__(self): + def __str__(self): return self.headline class Meta: diff --git a/tests/regressiontests/introspection/tests.py b/tests/regressiontests/introspection/tests.py index 06736a68cb..a54e0c670b 100644 --- a/tests/regressiontests/introspection/tests.py +++ b/tests/regressiontests/introspection/tests.py @@ -4,6 +4,7 @@ from functools import update_wrapper from django.db import connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature +from django.utils import six from .models import Reporter, Article @@ -35,8 +36,7 @@ class IgnoreNotimplementedError(type): attrs[k] = ignore_not_implemented(v) return type.__new__(cls, name, bases, attrs) -class IntrospectionTests(TestCase): - __metaclass__ = IgnoreNotimplementedError +class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)): def test_table_names(self): tl = connection.introspection.table_names() @@ -89,6 +89,11 @@ class IntrospectionTests(TestCase): [datatype(r[1], r) for r in desc], ['IntegerField', 'CharField', 'CharField', 'CharField', 'BigIntegerField'] ) + # Check also length of CharFields + self.assertEqual( + [r[3] for r in desc if datatype(r[1], r) == 'CharField'], + [30, 30, 75] + ) # Oracle forces null=True under the hood in some cases (see # https://docs.djangoproject.com/en/dev/ref/databases/#null-and-empty-strings) diff --git a/tests/regressiontests/m2m_regress/models.py b/tests/regressiontests/m2m_regress/models.py index be8dcae9c1..7c1108456e 100644 --- a/tests/regressiontests/m2m_regress/models.py +++ b/tests/regressiontests/m2m_regress/models.py @@ -1,37 +1,42 @@ from django.contrib.auth import models as auth from django.db import models +from django.utils.encoding import python_2_unicode_compatible # No related name is needed here, since symmetrical relations are not # explicitly reversible. +@python_2_unicode_compatible class SelfRefer(models.Model): name = models.CharField(max_length=10) references = models.ManyToManyField('self') related = models.ManyToManyField('self') - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Tag(models.Model): name = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return self.name # Regression for #11956 -- a many to many to the base class +@python_2_unicode_compatible class TagCollection(Tag): tags = models.ManyToManyField(Tag, related_name='tag_collections') - def __unicode__(self): + def __str__(self): return self.name # A related_name is required on one of the ManyToManyField entries here because # they are both addressable as reverse relations from Tag. +@python_2_unicode_compatible class Entry(models.Model): name = models.CharField(max_length=10) topics = models.ManyToManyField(Tag) related = models.ManyToManyField(Tag, related_name="similar") - def __unicode__(self): + def __str__(self): return self.name # Two models both inheriting from a base model with a self-referential m2m field diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py index 1db3de1a4c..47c24ed5b2 100644 --- a/tests/regressiontests/m2m_through_regress/models.py +++ b/tests/regressiontests/m2m_through_regress/models.py @@ -2,40 +2,45 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.db import models +from django.utils.encoding import python_2_unicode_compatible # Forward declared intermediate model +@python_2_unicode_compatible class Membership(models.Model): person = models.ForeignKey('Person') group = models.ForeignKey('Group') price = models.IntegerField(default=100) - def __unicode__(self): + def __str__(self): return "%s is a member of %s" % (self.person.name, self.group.name) # using custom id column to test ticket #11107 +@python_2_unicode_compatible class UserMembership(models.Model): id = models.AutoField(db_column='usermembership_id', primary_key=True) user = models.ForeignKey(User) group = models.ForeignKey('Group') price = models.IntegerField(default=100) - def __unicode__(self): + def __str__(self): return "%s is a user and member of %s" % (self.user.username, self.group.name) +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=128) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Group(models.Model): name = models.CharField(max_length=128) # Membership object defined as a class members = models.ManyToManyField(Person, through=Membership) user_members = models.ManyToManyField(User, through='UserMembership') - def __unicode__(self): + def __str__(self): return self.name # A set of models that use an non-abstract inherited model as the 'through' model. @@ -55,22 +60,25 @@ class B(models.Model): # Using to_field on the through model +@python_2_unicode_compatible class Car(models.Model): make = models.CharField(max_length=20, unique=True) drivers = models.ManyToManyField('Driver', through='CarDriver') - def __unicode__(self, ): + def __str__(self): return self.make +@python_2_unicode_compatible class Driver(models.Model): name = models.CharField(max_length=20, unique=True) - def __unicode__(self, ): + def __str__(self): return self.name +@python_2_unicode_compatible class CarDriver(models.Model): car = models.ForeignKey('Car', to_field='make') driver = models.ForeignKey('Driver', to_field='name') - def __unicode__(self, ): + def __str__(self): return "pk=%s car=%s driver=%s" % (str(self.pk), self.car, self.driver) diff --git a/tests/regressiontests/m2m_through_regress/tests.py b/tests/regressiontests/m2m_through_regress/tests.py index 73a13654e7..458c194f89 100644 --- a/tests/regressiontests/m2m_through_regress/tests.py +++ b/tests/regressiontests/m2m_through_regress/tests.py @@ -1,10 +1,9 @@ from __future__ import absolute_import -from io import BytesIO - from django.core import management from django.contrib.auth.models import User from django.test import TestCase +from django.utils.six import StringIO from .models import (Person, Group, Membership, UserMembership, Car, Driver, CarDriver) @@ -70,11 +69,11 @@ class M2MThroughTestCase(TestCase): pks = {"p_pk": p.pk, "g_pk": g.pk, "m_pk": m.pk} - out = BytesIO() + out = StringIO() management.call_command("dumpdata", "m2m_through_regress", format="json", stdout=out) self.assertEqual(out.getvalue().strip(), """[{"pk": %(m_pk)s, "model": "m2m_through_regress.membership", "fields": {"person": %(p_pk)s, "price": 100, "group": %(g_pk)s}}, {"pk": %(p_pk)s, "model": "m2m_through_regress.person", "fields": {"name": "Bob"}}, {"pk": %(g_pk)s, "model": "m2m_through_regress.group", "fields": {"name": "Roll"}}]""" % pks) - out = BytesIO() + out = StringIO() management.call_command("dumpdata", "m2m_through_regress", format="xml", indent=2, stdout=out) self.assertEqual(out.getvalue().strip(), """ @@ -142,6 +141,6 @@ class ThroughLoadDataTestCase(TestCase): def test_sequence_creation(self): "Check that sequences on an m2m_through are created for the through model, not a phantom auto-generated m2m table. Refs #11107" - out = BytesIO() + out = StringIO() management.call_command("dumpdata", "m2m_through_regress", format="json", stdout=out) self.assertEqual(out.getvalue().strip(), """[{"pk": 1, "model": "m2m_through_regress.usermembership", "fields": {"price": 100, "group": 1, "user": 1}}, {"pk": 1, "model": "m2m_through_regress.person", "fields": {"name": "Guido"}}, {"pk": 1, "model": "m2m_through_regress.group", "fields": {"name": "Python Core Group"}}]""") diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index c948662bc3..3e9ae84650 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -553,6 +553,8 @@ class FileBackendTests(BaseEmailBackendTests, TestCase): msg.send() self.assertEqual(len(os.listdir(self.tmp_dir)), 3) + connection.close() + class ConsoleBackendTests(BaseEmailBackendTests, TestCase): email_backend = 'django.core.mail.backends.console.EmailBackend' diff --git a/tests/regressiontests/managers_regress/models.py b/tests/regressiontests/managers_regress/models.py index fb6c530722..892505f24a 100644 --- a/tests/regressiontests/managers_regress/models.py +++ b/tests/regressiontests/managers_regress/models.py @@ -3,6 +3,7 @@ Various edge-cases for model managers. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class OnlyFred(models.Manager): @@ -44,35 +45,40 @@ class AbstractBase3(models.Model): class Meta: abstract = True +@python_2_unicode_compatible class Parent(models.Model): name = models.CharField(max_length=50) manager = OnlyFred() - def __unicode__(self): + def __str__(self): return self.name # Managers from base classes are inherited and, if no manager is specified # *and* the parent has a manager specified, the first one (in the MRO) will # become the default. +@python_2_unicode_compatible class Child1(AbstractBase1): data = models.CharField(max_length=25) - def __unicode__(self): + def __str__(self): return self.data +@python_2_unicode_compatible class Child2(AbstractBase1, AbstractBase2): data = models.CharField(max_length=25) - def __unicode__(self): + def __str__(self): return self.data +@python_2_unicode_compatible class Child3(AbstractBase1, AbstractBase3): data = models.CharField(max_length=25) - def __unicode__(self): + def __str__(self): return self.data +@python_2_unicode_compatible class Child4(AbstractBase1): data = models.CharField(max_length=25) @@ -80,16 +86,17 @@ class Child4(AbstractBase1): # inherited. default = models.Manager() - def __unicode__(self): + def __str__(self): return self.data +@python_2_unicode_compatible class Child5(AbstractBase3): name = models.CharField(max_length=25) default = OnlyFred() objects = models.Manager() - def __unicode__(self): + def __str__(self): return self.name # Will inherit managers from AbstractBase1, but not Child4. diff --git a/tests/regressiontests/many_to_one_regress/models.py b/tests/regressiontests/many_to_one_regress/models.py index 1e59c4c8c8..f3727820f8 100644 --- a/tests/regressiontests/many_to_one_regress/models.py +++ b/tests/regressiontests/many_to_one_regress/models.py @@ -4,6 +4,7 @@ Regression tests for a few ForeignKey bugs. from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible # If ticket #1578 ever slips back in, these models will not be able to be # created (the field names being lower-cased versions of their opposite @@ -30,18 +31,20 @@ class Child(models.Model): # Multiple paths to the same model (#7110, #7125) +@python_2_unicode_compatible class Category(models.Model): name = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return self.name class Record(models.Model): category = models.ForeignKey(Category) +@python_2_unicode_compatible class Relation(models.Model): left = models.ForeignKey(Record, related_name='left_set') right = models.ForeignKey(Record, related_name='right_set') - def __unicode__(self): + def __str__(self): return "%s - %s" % (self.left.category.name, self.right.category.name) diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index 08a385e6cf..eb66f2bbf3 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -3,6 +3,7 @@ import gzip import re import random +from io import BytesIO from django.conf import settings from django.core import mail @@ -14,8 +15,9 @@ from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.test import TestCase, RequestFactory from django.test.utils import override_settings +from django.utils import six from django.utils.six.moves import xrange -from django.utils.six import StringIO + class CommonMiddlewareTest(TestCase): def setUp(self): @@ -506,9 +508,9 @@ class GZipMiddlewareTest(TestCase): """ Tests the GZip middleware. """ - short_string = "This string is too short to be worth compressing." - compressible_string = 'a' * 500 - uncompressible_string = ''.join(chr(random.randint(0, 255)) for _ in xrange(500)) + short_string = b"This string is too short to be worth compressing." + compressible_string = b'a' * 500 + uncompressible_string = b''.join(six.int2byte(random.randint(0, 255)) for _ in xrange(500)) def setUp(self): self.req = HttpRequest() @@ -526,7 +528,7 @@ class GZipMiddlewareTest(TestCase): @staticmethod def decompress(gzipped_string): - return gzip.GzipFile(mode='rb', fileobj=StringIO(gzipped_string)).read() + return gzip.GzipFile(mode='rb', fileobj=BytesIO(gzipped_string)).read() def test_compress_response(self): """ @@ -590,7 +592,7 @@ class ETagGZipMiddlewareTest(TestCase): """ Tests if the ETag middleware behaves correctly with GZip middleware. """ - compressible_string = 'a' * 500 + compressible_string = b'a' * 500 def setUp(self): self.rf = RequestFactory() diff --git a/tests/regressiontests/model_fields/models.py b/tests/regressiontests/model_fields/models.py index 4dcfb17bb3..d9c123fb39 100644 --- a/tests/regressiontests/model_fields/models.py +++ b/tests/regressiontests/model_fields/models.py @@ -69,6 +69,33 @@ class BooleanModel(models.Model): class RenamedField(models.Model): modelname = models.IntegerField(name="fieldname", choices=((1,'One'),)) +class VerboseNameField(models.Model): + id = models.AutoField("verbose pk", primary_key=True) + field1 = models.BigIntegerField("verbose field1") + field2 = models.BooleanField("verbose field2") + field3 = models.CharField("verbose field3", max_length=10) + field4 = models.CommaSeparatedIntegerField("verbose field4", max_length=99) + field5 = models.DateField("verbose field5") + field6 = models.DateTimeField("verbose field6") + field7 = models.DecimalField("verbose field7", max_digits=6, decimal_places=1) + field8 = models.EmailField("verbose field8") + field9 = models.FileField("verbose field9", upload_to="unused") + field10 = models.FilePathField("verbose field10") + field11 = models.FloatField("verbose field11") + # Don't want to depend on PIL in this test + #field_image = models.ImageField("verbose field") + field12 = models.IntegerField("verbose field12") + field13 = models.IPAddressField("verbose field13") + field14 = models.GenericIPAddressField("verbose field14", protocol="ipv4") + field15 = models.NullBooleanField("verbose field15") + field16 = models.PositiveIntegerField("verbose field16") + field17 = models.PositiveSmallIntegerField("verbose field17") + field18 = models.SlugField("verbose field18") + field19 = models.SmallIntegerField("verbose field19") + field20 = models.TextField("verbose field20") + field21 = models.TimeField("verbose field21") + field22 = models.URLField("verbose field22") + # This model isn't used in any test, just here to ensure it validates successfully. # See ticket #16570. class DecimalLessThanOne(models.Model): diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py index 7d6071accc..526aca4f98 100644 --- a/tests/regressiontests/model_fields/tests.py +++ b/tests/regressiontests/model_fields/tests.py @@ -12,7 +12,7 @@ from django.utils import six from django.utils import unittest from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, - NullBooleanModel, BooleanModel, Document, RenamedField) + NullBooleanModel, BooleanModel, Document, RenamedField, VerboseNameField) from .imagefield import (ImageFieldTests, ImageFieldTwoDimensionsTests, TwoImageFieldTests, ImageFieldNoDimensionsTests, @@ -64,6 +64,14 @@ class BasicFieldTests(test.TestCase): self.assertTrue(hasattr(instance, 'get_fieldname_display')) self.assertFalse(hasattr(instance, 'get_modelname_display')) + def test_field_verbose_name(self): + m = VerboseNameField + for i in range(1, 23): + self.assertEqual(m._meta.get_field('field%d' % i).verbose_name, + 'verbose field%d' % i) + + self.assertEqual(m._meta.get_field('id').verbose_name, 'verbose pk') + class DecimalFieldTests(test.TestCase): def test_to_python(self): f = models.DecimalField(max_digits=4, decimal_places=2) diff --git a/tests/regressiontests/model_forms_regress/models.py b/tests/regressiontests/model_forms_regress/models.py index 9259e260f0..f6e08d24dc 100644 --- a/tests/regressiontests/model_forms_regress/models.py +++ b/tests/regressiontests/model_forms_regress/models.py @@ -4,6 +4,7 @@ import os from django.core.exceptions import ValidationError from django.db import models +from django.utils.encoding import python_2_unicode_compatible class Person(models.Model): @@ -20,18 +21,20 @@ class Triple(models.Model): class FilePathModel(models.Model): path = models.FilePathField(path=os.path.dirname(__file__), match=".*\.py$", blank=True) +@python_2_unicode_compatible class Publication(models.Model): title = models.CharField(max_length=30) date_published = models.DateField() - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) publications = models.ManyToManyField(Publication) - def __unicode__(self): + def __str__(self): return self.headline class CustomFileField(models.FileField): diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py index 3cb129f84e..90c907f2a6 100644 --- a/tests/regressiontests/model_forms_regress/tests.py +++ b/tests/regressiontests/model_forms_regress/tests.py @@ -485,9 +485,8 @@ class CustomMetaclass(ModelFormMetaclass): new.base_fields = {} return new -class CustomMetaclassForm(forms.ModelForm): - __metaclass__ = CustomMetaclass - +class CustomMetaclassForm(six.with_metaclass(CustomMetaclass, forms.ModelForm)): + pass class CustomMetaclassTestCase(TestCase): def test_modelform_factory_metaclass(self): diff --git a/tests/regressiontests/model_formsets_regress/models.py b/tests/regressiontests/model_formsets_regress/models.py index 189ed8072e..f94ad51929 100644 --- a/tests/regressiontests/model_formsets_regress/models.py +++ b/tests/regressiontests/model_formsets_regress/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class User(models.Model): @@ -22,9 +23,10 @@ class Manager(models.Model): class Network(models.Model): name = models.CharField(max_length=15) +@python_2_unicode_compatible class Host(models.Model): network = models.ForeignKey(Network) hostname = models.CharField(max_length=25) - def __unicode__(self): + def __str__(self): return self.hostname diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py index 5481298963..811c8175bb 100644 --- a/tests/regressiontests/model_inheritance_regress/models.py +++ b/tests/regressiontests/model_inheritance_regress/models.py @@ -3,7 +3,9 @@ from __future__ import unicode_literals import datetime from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) @@ -11,28 +13,31 @@ class Place(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return "%s the place" % self.name +@python_2_unicode_compatible class Restaurant(Place): serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the restaurant" % self.name +@python_2_unicode_compatible class ItalianRestaurant(Restaurant): serves_gnocchi = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the italian restaurant" % self.name +@python_2_unicode_compatible class ParkingLot(Place): # An explicit link to the parent (we can control the attribute name). parent = models.OneToOneField(Place, primary_key=True, parent_link=True) capacity = models.IntegerField() - def __unicode__(self): + def __str__(self): return "%s the parking lot" % self.name class ParkingLot2(Place): @@ -64,13 +69,14 @@ class SelfRefParent(models.Model): class SelfRefChild(SelfRefParent): child_data = models.IntegerField() +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() class Meta: ordering = ('-pub_date', 'headline') - def __unicode__(self): + def __str__(self): return self.headline class ArticleWithAuthor(Article): @@ -91,17 +97,19 @@ class Evaluation(Article): class QualityControl(Evaluation): assignee = models.CharField(max_length=50) +@python_2_unicode_compatible class BaseM(models.Model): base_name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.base_name +@python_2_unicode_compatible class DerivedM(BaseM): customPK = models.IntegerField(primary_key=True) derived_name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return "PK = %d, base_name = %s, derived_name = %s" \ % (self.customPK, self.base_name, self.derived_name) @@ -120,15 +128,17 @@ class InternalCertificationAudit(CertificationAudit): auditing_dept = models.CharField(max_length=20) # Check that abstract classes don't get m2m tables autocreated. +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=100) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class AbstractEvent(models.Model): name = models.CharField(max_length=100) attendees = models.ManyToManyField(Person, related_name="%(class)s_set") @@ -137,7 +147,7 @@ class AbstractEvent(models.Model): abstract = True ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name class BirthdayParty(AbstractEvent): diff --git a/tests/regressiontests/model_inheritance_select_related/models.py b/tests/regressiontests/model_inheritance_select_related/models.py index f810531bff..6b28772620 100644 --- a/tests/regressiontests/model_inheritance_select_related/models.py +++ b/tests/regressiontests/model_inheritance_select_related/models.py @@ -5,27 +5,31 @@ select_related(). from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=50) class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return "%s the place" % self.name +@python_2_unicode_compatible class Restaurant(Place): serves_sushi = models.BooleanField() serves_steak = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the restaurant" % self.name +@python_2_unicode_compatible class Person(models.Model): name = models.CharField(max_length=50) favorite_restaurant = models.ForeignKey(Restaurant) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/regressiontests/model_regress/models.py b/tests/regressiontests/model_regress/models.py index aca6d44837..82efb9c783 100644 --- a/tests/regressiontests/model_regress/models.py +++ b/tests/regressiontests/model_regress/models.py @@ -1,5 +1,6 @@ # coding: utf-8 from django.db import models +from django.utils.encoding import python_2_unicode_compatible CHOICES = ( @@ -8,6 +9,7 @@ CHOICES = ( ) +@python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() @@ -20,7 +22,7 @@ class Article(models.Model): # A utf-8 verbose name (Ångström's Articles) to test they are valid. verbose_name = "\xc3\x85ngstr\xc3\xb6m's Articles" - def __unicode__(self): + def __str__(self): return self.headline @@ -38,29 +40,31 @@ class Event(models.Model): when = models.DateTimeField() +@python_2_unicode_compatible class Department(models.Model): id = models.PositiveIntegerField(primary_key=True) name = models.CharField(max_length=200) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Worker(models.Model): department = models.ForeignKey(Department) name = models.CharField(max_length=200) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class BrokenUnicodeMethod(models.Model): name = models.CharField(max_length=7) - def __unicode__(self): - # Intentionally broken (trying to insert a unicode value into a str - # object). - return 'Názov: %s' % self.name + def __str__(self): + # Intentionally broken (invalid start byte in byte string). + return b'Name\xff: %s'.decode() % self.name class NonAutoPK(models.Model): diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py index 33202fad8f..fdbcabd187 100644 --- a/tests/regressiontests/modeladmin/models.py +++ b/tests/regressiontests/modeladmin/models.py @@ -1,8 +1,10 @@ # coding: utf-8 from django.contrib.auth.models import User from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Band(models.Model): name = models.CharField(max_length=100) bio = models.TextField() @@ -11,7 +13,7 @@ class Band(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name class Concert(models.Model): diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py index c2a1ca05ba..d55d50d0a5 100644 --- a/tests/regressiontests/modeladmin/tests.py +++ b/tests/regressiontests/modeladmin/tests.py @@ -5,7 +5,7 @@ from datetime import date from django import forms from django.conf import settings from django.contrib.admin.options import (ModelAdmin, TabularInline, - InlineModelAdmin, HORIZONTAL, VERTICAL) + HORIZONTAL, VERTICAL) from django.contrib.admin.sites import AdminSite from django.contrib.admin.validation import validate from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect @@ -15,7 +15,7 @@ from django.core.exceptions import ImproperlyConfigured from django.forms.models import BaseModelFormSet from django.forms.widgets import Select from django.test import TestCase -from django.test.utils import override_settings, str_prefix +from django.test.utils import str_prefix from django.utils import unittest from .models import Band, Concert, ValidationTestModel, ValidationTestInlineModel @@ -24,6 +24,7 @@ from .models import Band, Concert, ValidationTestModel, ValidationTestInlineMode class MockRequest(object): pass + class MockSuperUser(object): def has_perm(self, perm): return True @@ -47,7 +48,7 @@ class ModelAdminTests(TestCase): def test_default_fields(self): ma = ModelAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date']) def test_default_fieldsets(self): @@ -72,7 +73,7 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) - self.assertEqual( ma.get_fieldsets(request), + self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ['name']})]) self.assertEqual(ma.get_fieldsets(request, self.band), @@ -90,8 +91,8 @@ class ModelAdminTests(TestCase): fields = ['name'] ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), ['name']) - self.assertEqual(ma.get_form(request, self.band).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name']) + self.assertEqual(list(ma.get_form(request, self.band).base_fields), ['name']) # Using `fieldsets`. @@ -99,8 +100,8 @@ class ModelAdminTests(TestCase): fieldsets = [(None, {'fields': ['name']})] ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), ['name']) - self.assertEqual(ma.get_form(request, self.band).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name']) + self.assertEqual(list(ma.get_form(request, self.band).base_fields), ['name']) # Using `exclude`. @@ -108,7 +109,7 @@ class ModelAdminTests(TestCase): exclude = ['bio'] ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'sign_date']) # You can also pass a tuple to `exclude`. @@ -116,7 +117,7 @@ class ModelAdminTests(TestCase): exclude = ('bio',) ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'sign_date']) # Using `fields` and `exclude`. @@ -125,7 +126,7 @@ class ModelAdminTests(TestCase): exclude = ['bio'] ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name']) def test_custom_form_meta_exclude_with_readonly(self): @@ -148,8 +149,8 @@ class ModelAdminTests(TestCase): form = AdminBandForm ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), - ['sign_date',]) + self.assertEqual(list(ma.get_form(request).base_fields), + ['sign_date']) # Then, with `InlineModelAdmin` ----------------- @@ -172,8 +173,8 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), - ['main_band', 'opening_band', 'id', 'DELETE',]) + list(list(ma.get_formsets(request))[0]().forms[0].fields), + ['main_band', 'opening_band', 'id', 'DELETE']) def test_custom_form_meta_exclude(self): """ @@ -194,8 +195,8 @@ class ModelAdminTests(TestCase): form = AdminBandForm ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), - ['bio', 'sign_date',]) + self.assertEqual(list(ma.get_form(request).base_fields), + ['bio', 'sign_date']) # Then, with `InlineModelAdmin` ----------------- @@ -218,8 +219,8 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), - ['main_band', 'opening_band', 'day', 'id', 'DELETE',]) + list(list(ma.get_formsets(request))[0]().forms[0].fields), + ['main_band', 'opening_band', 'day', 'id', 'DELETE']) def test_custom_form_validation(self): # If we specify a form, it should use it allowing custom validation to work @@ -235,7 +236,7 @@ class ModelAdminTests(TestCase): form = AdminBandForm ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date', 'delete']) self.assertEqual( @@ -255,7 +256,7 @@ class ModelAdminTests(TestCase): exclude = ['name'] class BandAdmin(ModelAdmin): - exclude = ['sign_date',] + exclude = ['sign_date'] form = AdminBandForm def get_form(self, request, obj=None, **kwargs): @@ -263,9 +264,8 @@ class ModelAdminTests(TestCase): return super(BandAdmin, self).get_form(request, obj, **kwargs) ma = BandAdmin(Band, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), - ['name', 'sign_date',]) - + self.assertEqual(list(ma.get_form(request).base_fields), + ['name', 'sign_date']) def test_formset_exclude_kwarg_override(self): """ @@ -296,8 +296,8 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), - ['main_band', 'day', 'transport', 'id', 'DELETE',]) + list(list(ma.get_formsets(request))[0]().forms[0].fields), + ['main_band', 'day', 'transport', 'id', 'DELETE']) def test_queryset_override(self): # If we need to override the queryset of a ModelChoiceField in our custom form @@ -461,7 +461,7 @@ class ModelAdminTests(TestCase): form = AdminConcertForm ma = ConcertAdmin(Concert, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['main_band', 'opening_band', 'day']) class AdminConcertForm(forms.ModelForm): @@ -475,7 +475,7 @@ class ModelAdminTests(TestCase): form = AdminConcertForm ma = ConcertAdmin(Concert, self.site) - self.assertEqual(ma.get_form(request).base_fields.keys(), + self.assertEqual(list(ma.get_form(request).base_fields), ['extra', 'transport']) class ConcertInline(TabularInline): @@ -491,7 +491,7 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) self.assertEqual( - list(ma.get_formsets(request))[0]().forms[0].fields.keys(), + list(list(ma.get_formsets(request))[0]().forms[0].fields), ['extra', 'transport', 'id', 'DELETE', 'main_band']) diff --git a/tests/regressiontests/multiple_database/models.py b/tests/regressiontests/multiple_database/models.py index 7d655fe3d6..e46438649b 100644 --- a/tests/regressiontests/multiple_database/models.py +++ b/tests/regressiontests/multiple_database/models.py @@ -4,15 +4,17 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Review(models.Model): source = models.CharField(max_length=100) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = generic.GenericForeignKey() - def __unicode__(self): + def __str__(self): return self.source class Meta: @@ -22,11 +24,12 @@ class PersonManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) +@python_2_unicode_compatible class Person(models.Model): objects = PersonManager() name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name class Meta: @@ -45,6 +48,7 @@ class BookManager(models.Manager): kwargs.pop('extra_arg', None) return super(BookManager, self).get_or_create(*args, **kwargs) +@python_2_unicode_compatible class Book(models.Model): objects = BookManager() title = models.CharField(max_length=100) @@ -54,17 +58,18 @@ class Book(models.Model): reviews = generic.GenericRelation(Review) pages = models.IntegerField(default=100) - def __unicode__(self): + def __str__(self): return self.title class Meta: ordering = ('title',) +@python_2_unicode_compatible class Pet(models.Model): name = models.CharField(max_length=100) owner = models.ForeignKey(Person) - def __unicode__(self): + def __str__(self): return self.name class Meta: diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py index 74a5f2f550..782fe2bfc6 100644 --- a/tests/regressiontests/multiple_database/tests.py +++ b/tests/regressiontests/multiple_database/tests.py @@ -1546,6 +1546,21 @@ class RouterTestCase(TestCase): # If you evaluate the query, it should work, running on 'other' self.assertEqual(list(qs.values_list('title', flat=True)), ['Dive into Python']) + def test_deferred_models(self): + mark_def = Person.objects.using('default').create(name="Mark Pilgrim") + mark_other = Person.objects.using('other').create(name="Mark Pilgrim") + orig_b = Book.objects.using('other').create(title="Dive into Python", + published=datetime.date(2009, 5, 4), + editor=mark_other) + b = Book.objects.using('other').only('title').get(pk=orig_b.pk) + self.assertEqual(b.published, datetime.date(2009, 5, 4)) + b = Book.objects.using('other').only('title').get(pk=orig_b.pk) + b.editor = mark_def + b.save(using='default') + self.assertEqual(Book.objects.using('default').get(pk=b.pk).published, + datetime.date(2009, 5, 4)) + + class AuthTestCase(TestCase): multi_db = True diff --git a/tests/regressiontests/nested_foreign_keys/__init__.py b/tests/regressiontests/nested_foreign_keys/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/nested_foreign_keys/__init__.py diff --git a/tests/regressiontests/nested_foreign_keys/models.py b/tests/regressiontests/nested_foreign_keys/models.py new file mode 100644 index 0000000000..50d447951b --- /dev/null +++ b/tests/regressiontests/nested_foreign_keys/models.py @@ -0,0 +1,28 @@ +from django.db import models + + +class Person(models.Model): + name = models.CharField(max_length=200) + + +class Movie(models.Model): + title = models.CharField(max_length=200) + director = models.ForeignKey(Person) + + +class Event(models.Model): + pass + + +class Screening(Event): + movie = models.ForeignKey(Movie) + +class ScreeningNullFK(Event): + movie = models.ForeignKey(Movie, null=True) + + +class Package(models.Model): + screening = models.ForeignKey(Screening, null=True) + +class PackageNullFK(models.Model): + screening = models.ForeignKey(ScreeningNullFK, null=True) diff --git a/tests/regressiontests/nested_foreign_keys/tests.py b/tests/regressiontests/nested_foreign_keys/tests.py new file mode 100644 index 0000000000..a976d12453 --- /dev/null +++ b/tests/regressiontests/nested_foreign_keys/tests.py @@ -0,0 +1,166 @@ +from __future__ import absolute_import +from django.test import TestCase + +from .models import Person, Movie, Event, Screening, ScreeningNullFK, Package, PackageNullFK + + +# These are tests for #16715. The basic scheme is always the same: 3 models with +# 2 relations. The first relation may be null, while the second is non-nullable. +# In some cases, Django would pick the wrong join type for the second relation, +# resulting in missing objects in the queryset. +# +# Model A +# | (Relation A/B : nullable) +# Model B +# | (Relation B/C : non-nullable) +# Model C +# +# Because of the possibility of NULL rows resulting from the LEFT OUTER JOIN +# between Model A and Model B (i.e. instances of A without reference to B), +# the second join must also be LEFT OUTER JOIN, so that we do not ignore +# instances of A that do not reference B. +# +# Relation A/B can either be an explicit foreign key or an implicit reverse +# relation such as introduced by one-to-one relations (through multi-table +# inheritance). +class NestedForeignKeysTests(TestCase): + def setUp(self): + self.director = Person.objects.create(name='Terry Gilliam / Terry Jones') + self.movie = Movie.objects.create(title='Monty Python and the Holy Grail', director=self.director) + + + # This test failed in #16715 because in some cases INNER JOIN was selected + # for the second foreign key relation instead of LEFT OUTER JOIN. + def testInheritance(self): + some_event = Event.objects.create() + screening = Screening.objects.create(movie=self.movie) + + self.assertEqual(len(Event.objects.all()), 2) + self.assertEqual(len(Event.objects.select_related('screening')), 2) + # This failed. + self.assertEqual(len(Event.objects.select_related('screening__movie')), 2) + + self.assertEqual(len(Event.objects.values()), 2) + self.assertEqual(len(Event.objects.values('screening__pk')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__pk')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__title')), 2) + # This failed. + self.assertEqual(len(Event.objects.values('screening__movie__pk', 'screening__movie__title')), 2) + + # Simple filter/exclude queries for good measure. + self.assertEqual(Event.objects.filter(screening__movie=self.movie).count(), 1) + self.assertEqual(Event.objects.exclude(screening__movie=self.movie).count(), 1) + + + # These all work because the second foreign key in the chain has null=True. + def testInheritanceNullFK(self): + some_event = Event.objects.create() + screening = ScreeningNullFK.objects.create(movie=None) + screening_with_movie = ScreeningNullFK.objects.create(movie=self.movie) + + self.assertEqual(len(Event.objects.all()), 3) + self.assertEqual(len(Event.objects.select_related('screeningnullfk')), 3) + self.assertEqual(len(Event.objects.select_related('screeningnullfk__movie')), 3) + + self.assertEqual(len(Event.objects.values()), 3) + self.assertEqual(len(Event.objects.values('screeningnullfk__pk')), 3) + self.assertEqual(len(Event.objects.values('screeningnullfk__movie__pk')), 3) + self.assertEqual(len(Event.objects.values('screeningnullfk__movie__title')), 3) + self.assertEqual(len(Event.objects.values('screeningnullfk__movie__pk', 'screeningnullfk__movie__title')), 3) + + self.assertEqual(Event.objects.filter(screeningnullfk__movie=self.movie).count(), 1) + self.assertEqual(Event.objects.exclude(screeningnullfk__movie=self.movie).count(), 2) + + + # This test failed in #16715 because in some cases INNER JOIN was selected + # for the second foreign key relation instead of LEFT OUTER JOIN. + def testExplicitForeignKey(self): + package = Package.objects.create() + screening = Screening.objects.create(movie=self.movie) + package_with_screening = Package.objects.create(screening=screening) + + self.assertEqual(len(Package.objects.all()), 2) + self.assertEqual(len(Package.objects.select_related('screening')), 2) + self.assertEqual(len(Package.objects.select_related('screening__movie')), 2) + + self.assertEqual(len(Package.objects.values()), 2) + self.assertEqual(len(Package.objects.values('screening__pk')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__pk')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__title')), 2) + # This failed. + self.assertEqual(len(Package.objects.values('screening__movie__pk', 'screening__movie__title')), 2) + + self.assertEqual(Package.objects.filter(screening__movie=self.movie).count(), 1) + self.assertEqual(Package.objects.exclude(screening__movie=self.movie).count(), 1) + + + # These all work because the second foreign key in the chain has null=True. + def testExplicitForeignKeyNullFK(self): + package = PackageNullFK.objects.create() + screening = ScreeningNullFK.objects.create(movie=None) + screening_with_movie = ScreeningNullFK.objects.create(movie=self.movie) + package_with_screening = PackageNullFK.objects.create(screening=screening) + package_with_screening_with_movie = PackageNullFK.objects.create(screening=screening_with_movie) + + self.assertEqual(len(PackageNullFK.objects.all()), 3) + self.assertEqual(len(PackageNullFK.objects.select_related('screening')), 3) + self.assertEqual(len(PackageNullFK.objects.select_related('screening__movie')), 3) + + self.assertEqual(len(PackageNullFK.objects.values()), 3) + self.assertEqual(len(PackageNullFK.objects.values('screening__pk')), 3) + self.assertEqual(len(PackageNullFK.objects.values('screening__movie__pk')), 3) + self.assertEqual(len(PackageNullFK.objects.values('screening__movie__title')), 3) + self.assertEqual(len(PackageNullFK.objects.values('screening__movie__pk', 'screening__movie__title')), 3) + + self.assertEqual(PackageNullFK.objects.filter(screening__movie=self.movie).count(), 1) + self.assertEqual(PackageNullFK.objects.exclude(screening__movie=self.movie).count(), 2) + + +# Some additional tests for #16715. The only difference is the depth of the +# nesting as we now use 4 models instead of 3 (and thus 3 relations). This +# checks if promotion of join types works for deeper nesting too. +class DeeplyNestedForeignKeysTests(TestCase): + def setUp(self): + self.director = Person.objects.create(name='Terry Gilliam / Terry Jones') + self.movie = Movie.objects.create(title='Monty Python and the Holy Grail', director=self.director) + + + def testInheritance(self): + some_event = Event.objects.create() + screening = Screening.objects.create(movie=self.movie) + + self.assertEqual(len(Event.objects.all()), 2) + self.assertEqual(len(Event.objects.select_related('screening__movie__director')), 2) + + self.assertEqual(len(Event.objects.values()), 2) + self.assertEqual(len(Event.objects.values('screening__movie__director__pk')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__director__name')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__director__pk', 'screening__movie__director__name')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__pk', 'screening__movie__director__pk')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__pk', 'screening__movie__director__name')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__title', 'screening__movie__director__pk')), 2) + self.assertEqual(len(Event.objects.values('screening__movie__title', 'screening__movie__director__name')), 2) + + self.assertEqual(Event.objects.filter(screening__movie__director=self.director).count(), 1) + self.assertEqual(Event.objects.exclude(screening__movie__director=self.director).count(), 1) + + + def testExplicitForeignKey(self): + package = Package.objects.create() + screening = Screening.objects.create(movie=self.movie) + package_with_screening = Package.objects.create(screening=screening) + + self.assertEqual(len(Package.objects.all()), 2) + self.assertEqual(len(Package.objects.select_related('screening__movie__director')), 2) + + self.assertEqual(len(Package.objects.values()), 2) + self.assertEqual(len(Package.objects.values('screening__movie__director__pk')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__director__name')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__director__pk', 'screening__movie__director__name')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__pk', 'screening__movie__director__pk')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__pk', 'screening__movie__director__name')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__title', 'screening__movie__director__pk')), 2) + self.assertEqual(len(Package.objects.values('screening__movie__title', 'screening__movie__director__name')), 2) + + self.assertEqual(Package.objects.filter(screening__movie__director=self.director).count(), 1) + self.assertEqual(Package.objects.exclude(screening__movie__director=self.director).count(), 1) diff --git a/tests/regressiontests/null_fk/models.py b/tests/regressiontests/null_fk/models.py index e32ff542a0..c86ee8a5a9 100644 --- a/tests/regressiontests/null_fk/models.py +++ b/tests/regressiontests/null_fk/models.py @@ -3,6 +3,7 @@ Regression tests for proper working of ForeignKey(null=True). """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible class SystemDetails(models.Model): @@ -16,13 +17,15 @@ class Forum(models.Model): system_info = models.ForeignKey(SystemInfo) forum_name = models.CharField(max_length=32) +@python_2_unicode_compatible class Post(models.Model): forum = models.ForeignKey(Forum, null=True) title = models.CharField(max_length=32) - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Comment(models.Model): post = models.ForeignKey(Post, null=True) comment_text = models.CharField(max_length=250) @@ -30,7 +33,7 @@ class Comment(models.Model): class Meta: ordering = ('comment_text',) - def __unicode__(self): + def __str__(self): return self.comment_text # Ticket 15823 diff --git a/tests/regressiontests/null_fk_ordering/models.py b/tests/regressiontests/null_fk_ordering/models.py index e4a19f1512..3caff0d594 100644 --- a/tests/regressiontests/null_fk_ordering/models.py +++ b/tests/regressiontests/null_fk_ordering/models.py @@ -8,17 +8,19 @@ xpected results from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible # The first two models represent a very simple null FK ordering case. class Author(models.Model): name = models.CharField(max_length=150) +@python_2_unicode_compatible class Article(models.Model): title = models.CharField(max_length=150) author = models.ForeignKey(Author, null=True) - def __unicode__(self): + def __str__(self): return 'Article titled: %s' % (self.title, ) class Meta: @@ -33,13 +35,15 @@ class Forum(models.Model): system_info = models.ForeignKey(SystemInfo) forum_name = models.CharField(max_length=32) +@python_2_unicode_compatible class Post(models.Model): forum = models.ForeignKey(Forum, null=True) title = models.CharField(max_length=32) - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Comment(models.Model): post = models.ForeignKey(Post, null=True) comment_text = models.CharField(max_length=250) @@ -47,5 +51,5 @@ class Comment(models.Model): class Meta: ordering = ['post__forum__system_info__system_name', 'comment_text'] - def __unicode__(self): + def __str__(self): return self.comment_text diff --git a/tests/regressiontests/null_queries/models.py b/tests/regressiontests/null_queries/models.py index 886bd75276..25560fbab7 100644 --- a/tests/regressiontests/null_queries/models.py +++ b/tests/regressiontests/null_queries/models.py @@ -1,19 +1,22 @@ from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Poll(models.Model): question = models.CharField(max_length=200) - def __unicode__(self): + def __str__(self): return "Q: %s " % self.question +@python_2_unicode_compatible class Choice(models.Model): poll = models.ForeignKey(Poll) choice = models.CharField(max_length=200) - def __unicode__(self): + def __str__(self): return "Choice: %s in poll %s" % (self.choice, self.poll) # A set of models with an inner one pointing to two outer ones. diff --git a/tests/regressiontests/one_to_one_regress/models.py b/tests/regressiontests/one_to_one_regress/models.py index 5d32bf03f8..38b801f40e 100644 --- a/tests/regressiontests/one_to_one_regress/models.py +++ b/tests/regressiontests/one_to_one_regress/models.py @@ -1,39 +1,44 @@ from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) - def __unicode__(self): + def __str__(self): return "%s the place" % self.name +@python_2_unicode_compatible class Restaurant(models.Model): place = models.OneToOneField(Place) serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the restaurant" % self.place.name +@python_2_unicode_compatible class Bar(models.Model): place = models.OneToOneField(Place) serves_cocktails = models.BooleanField() - def __unicode__(self): + def __str__(self): return "%s the bar" % self.place.name class UndergroundBar(models.Model): place = models.OneToOneField(Place, null=True) serves_cocktails = models.BooleanField() +@python_2_unicode_compatible class Favorites(models.Model): name = models.CharField(max_length = 50) restaurants = models.ManyToManyField(Restaurant) - def __unicode__(self): + def __str__(self): return "Favorites for %s" % self.name class Target(models.Model): diff --git a/tests/regressiontests/pagination_regress/tests.py b/tests/regressiontests/pagination_regress/tests.py index 59ac41e062..e98352e006 100644 --- a/tests/regressiontests/pagination_regress/tests.py +++ b/tests/regressiontests/pagination_regress/tests.py @@ -17,14 +17,16 @@ class PaginatorTests(TestCase): paginator = Paginator(*params) self.check_attribute('count', paginator, count, params) self.check_attribute('num_pages', paginator, num_pages, params) - self.check_attribute('page_range', paginator, page_range, params) + self.check_attribute('page_range', paginator, page_range, params, coerce=list) - def check_attribute(self, name, paginator, expected, params): + def check_attribute(self, name, paginator, expected, params, coerce=None): """ Helper method that checks a single attribute and gives a nice error message upon test failure. """ got = getattr(paginator, name) + if coerce is not None: + got = coerce(got) self.assertEqual(expected, got, "For '%s', expected %s but got %s. Paginator parameters were: %s" % (name, expected, got, params)) diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 6328776e91..f0178a0256 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -7,6 +7,7 @@ import threading from django.db import models from django.utils import six +from django.utils.encoding import python_2_unicode_compatible class DumbCategory(models.Model): @@ -19,6 +20,7 @@ class ProxyCategory(DumbCategory): class NamedCategory(DumbCategory): name = models.CharField(max_length=10) +@python_2_unicode_compatible class Tag(models.Model): name = models.CharField(max_length=10) parent = models.ForeignKey('self', blank=True, null=True, @@ -28,9 +30,10 @@ class Tag(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Note(models.Model): note = models.CharField(max_length=100) misc = models.CharField(max_length=10) @@ -38,7 +41,7 @@ class Note(models.Model): class Meta: ordering = ['note'] - def __unicode__(self): + def __str__(self): return self.note def __init__(self, *args, **kwargs): @@ -48,14 +51,16 @@ class Note(models.Model): # that use objects of that type as an argument. self.lock = threading.Lock() +@python_2_unicode_compatible class Annotation(models.Model): name = models.CharField(max_length=10) tag = models.ForeignKey(Tag) notes = models.ManyToManyField(Note) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class ExtraInfo(models.Model): info = models.CharField(max_length=100) note = models.ForeignKey(Note) @@ -63,9 +68,10 @@ class ExtraInfo(models.Model): class Meta: ordering = ['info'] - def __unicode__(self): + def __str__(self): return self.info +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=10) num = models.IntegerField(unique=True) @@ -74,9 +80,10 @@ class Author(models.Model): class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Item(models.Model): name = models.CharField(max_length=10) created = models.DateTimeField() @@ -88,16 +95,18 @@ class Item(models.Model): class Meta: ordering = ['-note', 'name'] - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Report(models.Model): name = models.CharField(max_length=10) creator = models.ForeignKey(Author, to_field='num', null=True) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Ranking(models.Model): rank = models.IntegerField() author = models.ForeignKey(Author) @@ -106,9 +115,10 @@ class Ranking(models.Model): # A complex ordering specification. Should stress the system a bit. ordering = ('author__extra__note', 'author__name', 'rank') - def __unicode__(self): + def __str__(self): return '%d: %s' % (self.rank, self.author.name) +@python_2_unicode_compatible class Cover(models.Model): title = models.CharField(max_length=50) item = models.ForeignKey(Item) @@ -116,13 +126,14 @@ class Cover(models.Model): class Meta: ordering = ['item'] - def __unicode__(self): + def __str__(self): return self.title +@python_2_unicode_compatible class Number(models.Model): num = models.IntegerField() - def __unicode__(self): + def __str__(self): return six.text_type(self.num) # Symmetrical m2m field with a normal field using the reverse accesor name @@ -168,6 +179,7 @@ class CustomManager(models.Manager): qs = super(CustomManager, self).get_query_set() return qs.filter(public=True, tag__name='t1') +@python_2_unicode_compatible class ManagedModel(models.Model): data = models.CharField(max_length=10) tag = models.ForeignKey(Tag) @@ -176,7 +188,7 @@ class ManagedModel(models.Model): objects = CustomManager() normal_manager = models.Manager() - def __unicode__(self): + def __str__(self): return self.data # An inter-related setup with multiple paths from Child to Detail. @@ -211,11 +223,12 @@ class Related(models.Model): # An inter-related setup with a model subclass that has a nullable # path to another model, and a return path from that model. +@python_2_unicode_compatible class Celebrity(models.Model): name = models.CharField("Name", max_length=20) greatest_fan = models.ForeignKey("Fan", null=True, unique=True) - def __unicode__(self): + def __str__(self): return self.name class TvChef(Celebrity): @@ -225,10 +238,11 @@ class Fan(models.Model): fan_of = models.ForeignKey(Celebrity) # Multiple foreign keys +@python_2_unicode_compatible class LeafA(models.Model): data = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return self.data class LeafB(models.Model): @@ -238,11 +252,12 @@ class Join(models.Model): a = models.ForeignKey(LeafA) b = models.ForeignKey(LeafB) +@python_2_unicode_compatible class ReservedName(models.Model): name = models.CharField(max_length=20) order = models.IntegerField() - def __unicode__(self): + def __str__(self): return self.name # A simpler shared-foreign-key setup that can expose some problems. @@ -256,13 +271,14 @@ class PointerB(models.Model): connection = models.ForeignKey(SharedConnection) # Multi-layer ordering +@python_2_unicode_compatible class SingleObject(models.Model): name = models.CharField(max_length=10) class Meta: ordering = ['name'] - def __unicode__(self): + def __str__(self): return self.name class RelatedObject(models.Model): @@ -271,6 +287,7 @@ class RelatedObject(models.Model): class Meta: ordering = ['single'] +@python_2_unicode_compatible class Plaything(models.Model): name = models.CharField(max_length=10) others = models.ForeignKey(RelatedObject, null=True) @@ -278,79 +295,89 @@ class Plaything(models.Model): class Meta: ordering = ['others'] - def __unicode__(self): + def __str__(self): return self.name class Article(models.Model): name = models.CharField(max_length=20) created = models.DateTimeField() +@python_2_unicode_compatible class Food(models.Model): name = models.CharField(max_length=20, unique=True) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Eaten(models.Model): food = models.ForeignKey(Food, to_field="name") meal = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return "%s at %s" % (self.food, self.meal) +@python_2_unicode_compatible class Node(models.Model): num = models.IntegerField(unique=True) parent = models.ForeignKey("self", to_field="num", null=True) - def __unicode__(self): + def __str__(self): return "%s" % self.num # Bug #12252 +@python_2_unicode_compatible class ObjectA(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class ObjectB(models.Model): name = models.CharField(max_length=50) objecta = models.ForeignKey(ObjectA) num = models.PositiveSmallIntegerField() - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class ObjectC(models.Model): name = models.CharField(max_length=50) objecta = models.ForeignKey(ObjectA) objectb = models.ForeignKey(ObjectB) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class SimpleCategory(models.Model): name = models.CharField(max_length=15) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class SpecialCategory(SimpleCategory): special_name = models.CharField(max_length=15) - def __unicode__(self): + def __str__(self): return self.name + " " + self.special_name +@python_2_unicode_compatible class CategoryItem(models.Model): category = models.ForeignKey(SimpleCategory) - def __unicode__(self): + def __str__(self): return "category item: " + str(self.category) +@python_2_unicode_compatible class OneToOneCategory(models.Model): new_name = models.CharField(max_length=15) category = models.OneToOneField(SimpleCategory) - def __unicode__(self): + def __str__(self): return "one2one " + self.new_name class NullableName(models.Model): @@ -358,3 +385,18 @@ class NullableName(models.Model): class Meta: ordering = ['id'] + +class ModelD(models.Model): + name = models.TextField() + +class ModelC(models.Model): + name = models.TextField() + +class ModelB(models.Model): + name = models.TextField() + c = models.ForeignKey(ModelC) + +class ModelA(models.Model): + name = models.TextField() + b = models.ForeignKey(ModelB, null=True) + d = models.ForeignKey(ModelD) diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 1582993dfc..005aa9650b 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -23,7 +23,7 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, SpecialCategory, OneToOneCategory, NullableName, ProxyCategory, - SingleObject, RelatedObject) + SingleObject, RelatedObject, ModelA, ModelD) class BaseQuerysetTest(TestCase): @@ -806,7 +806,7 @@ class Queries1Tests(BaseQuerysetTest): qs = Tag.objects.values_list('id', flat=True).order_by('id') qs.query.bump_prefix() first = qs[0] - self.assertEqual(list(qs), range(first, first+5)) + self.assertEqual(list(qs), list(range(first, first+5))) def test_ticket8439(self): # Complex combinations of conjunctions, disjunctions and nullable @@ -1272,8 +1272,8 @@ class Queries5Tests(TestCase): # them in a values() query. dicts = qs.values('id', 'rank').order_by('id') self.assertEqual( - [d.items()[1] for d in dicts], - [('rank', 2), ('rank', 1), ('rank', 3)] + [d['rank'] for d in dicts], + [2, 1, 3] ) def test_ticket7256(self): @@ -2043,65 +2043,88 @@ class WhereNodeTest(TestCase): def test_empty_full_handling_conjunction(self): qn = connection.ops.quote_name w = WhereNode(children=[EverythingNode()]) - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w.negate() self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w = WhereNode(children=[NothingNode()]) self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w.negate() - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w = WhereNode(children=[EverythingNode(), EverythingNode()]) - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w.negate() self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w = WhereNode(children=[EverythingNode(), self.DummyNode()]) - self.assertEquals(w.as_sql(qn, connection), ('dummy', [])) + self.assertEqual(w.as_sql(qn, connection), ('dummy', [])) w = WhereNode(children=[self.DummyNode(), self.DummyNode()]) - self.assertEquals(w.as_sql(qn, connection), ('(dummy AND dummy)', [])) + self.assertEqual(w.as_sql(qn, connection), ('(dummy AND dummy)', [])) w.negate() - self.assertEquals(w.as_sql(qn, connection), ('NOT (dummy AND dummy)', [])) + self.assertEqual(w.as_sql(qn, connection), ('NOT (dummy AND dummy)', [])) w = WhereNode(children=[NothingNode(), self.DummyNode()]) self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w.negate() - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) def test_empty_full_handling_disjunction(self): qn = connection.ops.quote_name w = WhereNode(children=[EverythingNode()], connector='OR') - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w.negate() self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w = WhereNode(children=[NothingNode()], connector='OR') self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w.negate() - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w = WhereNode(children=[EverythingNode(), EverythingNode()], connector='OR') - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w.negate() self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w = WhereNode(children=[EverythingNode(), self.DummyNode()], connector='OR') - self.assertEquals(w.as_sql(qn, connection), ('', [])) + self.assertEqual(w.as_sql(qn, connection), ('', [])) w.negate() self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) w = WhereNode(children=[self.DummyNode(), self.DummyNode()], connector='OR') - self.assertEquals(w.as_sql(qn, connection), ('(dummy OR dummy)', [])) + self.assertEqual(w.as_sql(qn, connection), ('(dummy OR dummy)', [])) w.negate() - self.assertEquals(w.as_sql(qn, connection), ('NOT (dummy OR dummy)', [])) + self.assertEqual(w.as_sql(qn, connection), ('NOT (dummy OR dummy)', [])) w = WhereNode(children=[NothingNode(), self.DummyNode()], connector='OR') - self.assertEquals(w.as_sql(qn, connection), ('dummy', [])) + self.assertEqual(w.as_sql(qn, connection), ('dummy', [])) w.negate() - self.assertEquals(w.as_sql(qn, connection), ('NOT (dummy)', [])) + self.assertEqual(w.as_sql(qn, connection), ('NOT (dummy)', [])) def test_empty_nodes(self): qn = connection.ops.quote_name empty_w = WhereNode() w = WhereNode(children=[empty_w, empty_w]) - self.assertEquals(w.as_sql(qn, connection), (None, [])) + self.assertEqual(w.as_sql(qn, connection), (None, [])) w.negate() - self.assertEquals(w.as_sql(qn, connection), (None, [])) + self.assertEqual(w.as_sql(qn, connection), (None, [])) w.connector = 'OR' - self.assertEquals(w.as_sql(qn, connection), (None, [])) + self.assertEqual(w.as_sql(qn, connection), (None, [])) w.negate() - self.assertEquals(w.as_sql(qn, connection), (None, [])) + self.assertEqual(w.as_sql(qn, connection), (None, [])) w = WhereNode(children=[empty_w, NothingNode()], connector='OR') self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) + +class NullJoinPromotionOrTest(TestCase): + def setUp(self): + d = ModelD.objects.create(name='foo') + ModelA.objects.create(name='bar', d=d) + + def test_ticket_17886(self): + # The first Q-object is generating the match, the rest of the filters + # should not remove the match even if they do not match anything. The + # problem here was that b__name generates a LOUTER JOIN, then + # b__c__name generates join to c, which the ORM tried to promote but + # failed as that join isn't nullable. + q_obj = ( + Q(d__name='foo')| + Q(b__name='foo')| + Q(b__c__name='foo') + ) + qset = ModelA.objects.filter(q_obj) + self.assertEqual(len(qset), 1) + # We generate one INNER JOIN to D. The join is direct and not nullable + # so we can use INNER JOIN for it. However, we can NOT use INNER JOIN + # for the b->c join, as a->b is nullable. + self.assertEqual(str(qset.query).count('INNER JOIN'), 1) diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index f192459246..f9e1112b2e 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import time import warnings from datetime import datetime, timedelta +from io import BytesIO from django.conf import settings from django.core.handlers.wsgi import WSGIRequest, LimitedStream @@ -10,17 +11,16 @@ from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_r from django.test.utils import str_prefix from django.utils import unittest from django.utils.http import cookie_date -from django.utils.six import StringIO from django.utils.timezone import utc class RequestsTests(unittest.TestCase): def test_httprequest(self): request = HttpRequest() - self.assertEqual(request.GET.keys(), []) - self.assertEqual(request.POST.keys(), []) - self.assertEqual(request.COOKIES.keys(), []) - self.assertEqual(request.META.keys(), []) + self.assertEqual(list(request.GET.keys()), []) + self.assertEqual(list(request.POST.keys()), []) + self.assertEqual(list(request.COOKIES.keys()), []) + self.assertEqual(list(request.META.keys()), []) def test_httprequest_repr(self): request = HttpRequest() @@ -35,17 +35,17 @@ class RequestsTests(unittest.TestCase): str_prefix("<HttpRequest\npath:/otherpath/,\nGET:{%(_)s'a': %(_)s'b'},\nPOST:{%(_)s'c': %(_)s'd'},\nCOOKIES:{%(_)s'e': %(_)s'f'},\nMETA:{%(_)s'g': %(_)s'h'}>")) def test_wsgirequest(self): - request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'wsgi.input': StringIO('')}) - self.assertEqual(request.GET.keys(), []) - self.assertEqual(request.POST.keys(), []) - self.assertEqual(request.COOKIES.keys(), []) + request = WSGIRequest({'PATH_INFO': 'bogus', 'REQUEST_METHOD': 'bogus', 'wsgi.input': BytesIO(b'')}) + self.assertEqual(list(request.GET.keys()), []) + self.assertEqual(list(request.POST.keys()), []) + self.assertEqual(list(request.COOKIES.keys()), []) self.assertEqual(set(request.META.keys()), set(['PATH_INFO', 'REQUEST_METHOD', 'SCRIPT_NAME', 'wsgi.input'])) self.assertEqual(request.META['PATH_INFO'], 'bogus') self.assertEqual(request.META['REQUEST_METHOD'], 'bogus') self.assertEqual(request.META['SCRIPT_NAME'], '') def test_wsgirequest_repr(self): - request = WSGIRequest({'PATH_INFO': '/somepath/', 'REQUEST_METHOD': 'get', 'wsgi.input': StringIO('')}) + request = WSGIRequest({'PATH_INFO': '/somepath/', 'REQUEST_METHOD': 'get', 'wsgi.input': BytesIO(b'')}) request.GET = {'get-key': 'get-value'} request.POST = {'post-key': 'post-value'} request.COOKIES = {'post-key': 'post-value'} @@ -207,26 +207,26 @@ class RequestsTests(unittest.TestCase): def test_limited_stream(self): # Read all of a limited stream - stream = LimitedStream(StringIO(b'test'), 2) + stream = LimitedStream(BytesIO(b'test'), 2) self.assertEqual(stream.read(), b'te') # Reading again returns nothing. self.assertEqual(stream.read(), b'') # Read a number of characters greater than the stream has to offer - stream = LimitedStream(StringIO(b'test'), 2) + stream = LimitedStream(BytesIO(b'test'), 2) self.assertEqual(stream.read(5), b'te') # Reading again returns nothing. self.assertEqual(stream.readline(5), b'') # Read sequentially from a stream - stream = LimitedStream(StringIO(b'12345678'), 8) + stream = LimitedStream(BytesIO(b'12345678'), 8) self.assertEqual(stream.read(5), b'12345') self.assertEqual(stream.read(5), b'678') # Reading again returns nothing. self.assertEqual(stream.readline(5), b'') # Read lines from a stream - stream = LimitedStream(StringIO(b'1234\n5678\nabcd\nefgh\nijkl'), 24) + stream = LimitedStream(BytesIO(b'1234\n5678\nabcd\nefgh\nijkl'), 24) # Read a full line, unconditionally self.assertEqual(stream.readline(), b'1234\n') # Read a number of characters less than a line @@ -246,7 +246,7 @@ class RequestsTests(unittest.TestCase): # If a stream contains a newline, but the provided length # is less than the number of provided characters, the newline # doesn't reset the available character count - stream = LimitedStream(StringIO(b'1234\nabcdef'), 9) + stream = LimitedStream(BytesIO(b'1234\nabcdef'), 9) self.assertEqual(stream.readline(10), b'1234\n') self.assertEqual(stream.readline(3), b'abc') # Now expire the available characters @@ -255,7 +255,7 @@ class RequestsTests(unittest.TestCase): self.assertEqual(stream.readline(2), b'') # Same test, but with read, not readline. - stream = LimitedStream(StringIO(b'1234\nabcdef'), 9) + stream = LimitedStream(BytesIO(b'1234\nabcdef'), 9) self.assertEqual(stream.read(6), b'1234\na') self.assertEqual(stream.read(2), b'bc') self.assertEqual(stream.read(2), b'd') @@ -266,7 +266,7 @@ class RequestsTests(unittest.TestCase): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.read(), b'name=value') def test_read_after_value(self): @@ -277,7 +277,7 @@ class RequestsTests(unittest.TestCase): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.POST, {'name': ['value']}) self.assertEqual(request.body, b'name=value') self.assertEqual(request.read(), b'name=value') @@ -290,7 +290,7 @@ class RequestsTests(unittest.TestCase): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.read(2), b'na') self.assertRaises(Exception, lambda: request.body) self.assertEqual(request.POST, {}) @@ -312,7 +312,7 @@ class RequestsTests(unittest.TestCase): request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; boundary=boundary', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.POST, {'name': ['value']}) self.assertRaises(Exception, lambda: request.body) @@ -334,14 +334,14 @@ class RequestsTests(unittest.TestCase): request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; boundary=boundary', 'CONTENT_LENGTH': 0, - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) self.assertEqual(request.POST, {}) def test_read_by_lines(self): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) self.assertEqual(list(request), [b'name=value']) def test_POST_after_body_read(self): @@ -351,7 +351,7 @@ class RequestsTests(unittest.TestCase): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) raw_data = request.body self.assertEqual(request.POST, {'name': ['value']}) @@ -363,7 +363,7 @@ class RequestsTests(unittest.TestCase): payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) raw_data = request.body self.assertEqual(request.read(1), b'n') self.assertEqual(request.POST, {'name': ['value']}) @@ -383,7 +383,7 @@ class RequestsTests(unittest.TestCase): request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; boundary=boundary', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload)}) + 'wsgi.input': BytesIO(payload)}) raw_data = request.body # Consume enough data to mess up the parsing: self.assertEqual(request.read(13), b'--boundary\r\nC') @@ -397,7 +397,7 @@ class RequestsTests(unittest.TestCase): request = WSGIRequest({ 'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': StringIO(payload) + 'wsgi.input': BytesIO(payload) }) with warnings.catch_warnings(record=True): @@ -408,14 +408,14 @@ class RequestsTests(unittest.TestCase): If wsgi.input.read() raises an exception while trying to read() the POST, the exception should be identifiable (not a generic IOError). """ - class ExplodingStringIO(StringIO): + class ExplodingBytesIO(BytesIO): def read(self, len=0): raise IOError("kaboom!") payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), - 'wsgi.input': ExplodingStringIO(payload)}) + 'wsgi.input': ExplodingBytesIO(payload)}) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") diff --git a/tests/regressiontests/select_related_onetoone/models.py b/tests/regressiontests/select_related_onetoone/models.py index 3d6da9b4c5..3284defb11 100644 --- a/tests/regressiontests/select_related_onetoone/models.py +++ b/tests/regressiontests/select_related_onetoone/models.py @@ -1,44 +1,50 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class User(models.Model): username = models.CharField(max_length=100) email = models.EmailField() - def __unicode__(self): + def __str__(self): return self.username +@python_2_unicode_compatible class UserProfile(models.Model): user = models.OneToOneField(User) city = models.CharField(max_length=100) state = models.CharField(max_length=2) - def __unicode__(self): + def __str__(self): return "%s, %s" % (self.city, self.state) +@python_2_unicode_compatible class UserStatResult(models.Model): results = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return 'UserStatResults, results = %s' % (self.results,) +@python_2_unicode_compatible class UserStat(models.Model): user = models.OneToOneField(User, primary_key=True) posts = models.IntegerField() results = models.ForeignKey(UserStatResult) - def __unicode__(self): + def __str__(self): return 'UserStat, posts = %s' % (self.posts,) +@python_2_unicode_compatible class StatDetails(models.Model): base_stats = models.OneToOneField(UserStat) comments = models.IntegerField() - def __unicode__(self): + def __str__(self): return 'StatDetails, comments = %s' % (self.comments,) diff --git a/tests/regressiontests/select_related_regress/models.py b/tests/regressiontests/select_related_regress/models.py index 1af9ff4bca..a291a547f7 100644 --- a/tests/regressiontests/select_related_regress/models.py +++ b/tests/regressiontests/select_related_regress/models.py @@ -1,34 +1,39 @@ from __future__ import unicode_literals from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Building(models.Model): name = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return "Building: %s" % self.name +@python_2_unicode_compatible class Device(models.Model): building = models.ForeignKey('Building') name = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return "device '%s' in building %s" % (self.name, self.building) +@python_2_unicode_compatible class Port(models.Model): device = models.ForeignKey('Device') port_number = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return "%s/%s" % (self.device.name, self.port_number) +@python_2_unicode_compatible class Connection(models.Model): start = models.ForeignKey(Port, related_name='connection_start', unique=True) end = models.ForeignKey(Port, related_name='connection_end', unique=True) - def __unicode__(self): + def __str__(self): return "%s to %s" % (self.start, self.end) # Another non-tree hierarchy that exercises code paths similar to the above @@ -72,18 +77,20 @@ class SpecialClient(Client): value = models.IntegerField() # Some model inheritance exercises +@python_2_unicode_compatible class Parent(models.Model): name = models.CharField(max_length=10) - def __unicode__(self): + def __str__(self): return self.name class Child(Parent): value = models.IntegerField() +@python_2_unicode_compatible class Item(models.Model): name = models.CharField(max_length=10) child = models.ForeignKey(Child, null=True) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py index 4e73be015c..f4b3adaa5e 100644 --- a/tests/regressiontests/serializers_regress/tests.py +++ b/tests/regressiontests/serializers_regress/tests.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, unicode_literals import datetime import decimal -from io import BytesIO try: import yaml @@ -23,6 +22,7 @@ from django.core.serializers.base import DeserializationError from django.db import connection, models from django.http import HttpResponse from django.test import TestCase +from django.utils import six from django.utils.functional import curry from django.utils.unittest import skipUnless @@ -502,17 +502,17 @@ def streamTest(format, self): obj.save_base(raw=True) # Serialize the test database to a stream - for stream in (BytesIO(), HttpResponse()): + for stream in (six.StringIO(), HttpResponse()): serializers.serialize(format, [obj], indent=2, stream=stream) # Serialize normally for a comparison string_data = serializers.serialize(format, [obj], indent=2) # Check that the two are the same - if isinstance(stream, BytesIO): + if isinstance(stream, six.StringIO): self.assertEqual(string_data, stream.getvalue()) else: - self.assertEqual(string_data, stream.content) + self.assertEqual(string_data, stream.content.decode('utf-8')) stream.close() for format in serializers.get_serializer_formats(): diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py index b98b4b73c2..c90c785a6e 100644 --- a/tests/regressiontests/servers/tests.py +++ b/tests/regressiontests/servers/tests.py @@ -144,7 +144,7 @@ class LiveServerDatabase(LiveServerBase): Refs #2879. """ f = self.urlopen('/model_view/') - self.assertEqual(f.read().splitlines(), ['jane', 'robert']) + self.assertEqual(f.read().splitlines(), [b'jane', b'robert']) def test_database_writes(self): """ diff --git a/tests/regressiontests/settings_tests/tests.py b/tests/regressiontests/settings_tests/tests.py index ffcb79eb09..7225fb03ef 100644 --- a/tests/regressiontests/settings_tests/tests.py +++ b/tests/regressiontests/settings_tests/tests.py @@ -148,6 +148,16 @@ class SettingsTests(TestCase): def test_settings_delete_wrapped(self): self.assertRaises(TypeError, delattr, settings, '_wrapped') + def test_override_settings_delete(self): + """ + Allow deletion of a setting in an overriden settings set (#18824) + """ + previous_i18n = settings.USE_I18N + with self.settings(USE_I18N=False): + del settings.USE_I18N + self.assertRaises(AttributeError, getattr, settings, 'USE_I18N') + self.assertEqual(settings.USE_I18N, previous_i18n) + def test_allowed_include_roots_string(self): """ ALLOWED_INCLUDE_ROOTS is not allowed to be incorrectly set to a string diff --git a/tests/regressiontests/signals_regress/models.py b/tests/regressiontests/signals_regress/models.py index bf64f69e8c..829314c06c 100644 --- a/tests/regressiontests/signals_regress/models.py +++ b/tests/regressiontests/signals_regress/models.py @@ -1,15 +1,18 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=20) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Book(models.Model): name = models.CharField(max_length=20) authors = models.ManyToManyField(Author) - def __unicode__(self): + def __str__(self): return self.name diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py index 2368405060..05c8b3dc74 100644 --- a/tests/regressiontests/signing/tests.py +++ b/tests/regressiontests/signing/tests.py @@ -4,7 +4,8 @@ import time from django.core import signing from django.test import TestCase -from django.utils.encoding import force_text +from django.utils.encoding import force_str +from django.utils import six class TestSigner(TestCase): @@ -21,7 +22,7 @@ class TestSigner(TestCase): self.assertEqual( signer.signature(s), signing.base64_hmac(signer.salt + 'signer', s, - 'predictable-secret') + 'predictable-secret').decode() ) self.assertNotEqual(signer.signature(s), signer2.signature(s)) @@ -31,7 +32,8 @@ class TestSigner(TestCase): self.assertEqual( signer.signature('hello'), signing.base64_hmac('extra-salt' + 'signer', - 'hello', 'predictable-secret')) + 'hello', 'predictable-secret').decode() + ) self.assertNotEqual( signing.Signer('predictable-secret', salt='one').signature('hello'), signing.Signer('predictable-secret', salt='two').signature('hello')) @@ -39,17 +41,20 @@ class TestSigner(TestCase): def test_sign_unsign(self): "sign/unsign should be reversible" signer = signing.Signer('predictable-secret') - examples = ( + examples = [ 'q;wjmbk;wkmb', '3098247529087', '3098247:529:087:', 'jkw osanteuh ,rcuh nthu aou oauh ,ud du', '\u2019', - ) + ] + if not six.PY3: + examples.append(b'a byte string') for example in examples: - self.assertNotEqual( - force_text(example), force_text(signer.sign(example))) - self.assertEqual(example, signer.unsign(signer.sign(example))) + signed = signer.sign(example) + self.assertIsInstance(signed, str) + self.assertNotEqual(force_str(example), signed) + self.assertEqual(example, signer.unsign(signed)) def unsign_detects_tampering(self): "unsign should raise an exception if the value has been tampered with" @@ -69,15 +74,18 @@ class TestSigner(TestCase): def test_dumps_loads(self): "dumps and loads be reversible for any JSON serializable object" - objects = ( + objects = [ ['a', 'list'], - b'a string', 'a unicode string \u2019', {'a': 'dictionary'}, - ) + ] + if not six.PY3: + objects.append(b'a byte string') for o in objects: self.assertNotEqual(o, signing.dumps(o)) self.assertEqual(o, signing.loads(signing.dumps(o))) + self.assertNotEqual(o, signing.dumps(o, compress=True)) + self.assertEqual(o, signing.loads(signing.dumps(o, compress=True))) def test_decode_detects_tampering(self): "loads should raise exception for tampered objects" diff --git a/tests/regressiontests/sites_framework/models.py b/tests/regressiontests/sites_framework/models.py index 9ecc3e6660..55c4f4992e 100644 --- a/tests/regressiontests/sites_framework/models.py +++ b/tests/regressiontests/sites_framework/models.py @@ -1,7 +1,9 @@ from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class AbstractArticle(models.Model): title = models.CharField(max_length=50) @@ -11,7 +13,7 @@ class AbstractArticle(models.Model): class Meta: abstract = True - def __unicode__(self): + def __str__(self): return self.title class SyndicatedArticle(AbstractArticle): diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py index 19951f100b..078788a6a9 100644 --- a/tests/regressiontests/staticfiles_tests/tests.py +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -7,7 +7,6 @@ import posixpath import shutil import sys import tempfile -from io import BytesIO from django.template import loader, Context from django.conf import settings @@ -194,19 +193,18 @@ class TestFindStatic(CollectionTestCase, TestDefaults): Test ``findstatic`` management command. """ def _get_file(self, filepath): - out = BytesIO() + out = six.StringIO() call_command('findstatic', filepath, all=False, verbosity=0, stdout=out) out.seek(0) lines = [l.strip() for l in out.readlines()] - contents = codecs.open( - smart_text(lines[1].strip()), "r", "utf-8").read() - return contents + with codecs.open(smart_text(lines[1].strip()), "r", "utf-8") as f: + return f.read() def test_all_files(self): """ Test that findstatic returns all candidate files if run without --first. """ - out = BytesIO() + out = six.StringIO() call_command('findstatic', 'test/file.txt', verbosity=0, stdout=out) out.seek(0) lines = [l.strip() for l in out.readlines()] @@ -528,11 +526,11 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, """ Handle cache key creation correctly, see #17861. """ - name = b"/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/" + chr(22) + chr(180) + name = "/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/" + "\x16" + "\xb4" cache_key = storage.staticfiles_storage.cache_key(name) cache_validator = BaseCache({}) cache_validator.validate_key(cache_key) - self.assertEqual(cache_key, 'staticfiles:e95bbc36387084582df2a70750d7b351') + self.assertEqual(cache_key, 'staticfiles:821ea71ef36f95b3922a77f7364670e7') # we set DEBUG to False here since the template tag wouldn't work otherwise @@ -570,8 +568,8 @@ class TestCollectionSimpleCachedStorage(BaseCollectionTestCase, self.assertEqual(relpath, "cached/styles.deploy12345.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertNotIn("cached/other.css", content) - self.assertIn("other.deploy12345.css", content) + self.assertNotIn(b"cached/other.css", content) + self.assertIn(b"other.deploy12345.css", content) if sys.platform != 'win32': diff --git a/tests/regressiontests/string_lookup/models.py b/tests/regressiontests/string_lookup/models.py index 53687a22cb..a2d64cd0b2 100644 --- a/tests/regressiontests/string_lookup/models.py +++ b/tests/regressiontests/string_lookup/models.py @@ -1,42 +1,51 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals + from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Foo(models.Model): name = models.CharField(max_length=50) friend = models.CharField(max_length=50, blank=True) - def __unicode__(self): + def __str__(self): return "Foo %s" % self.name +@python_2_unicode_compatible class Bar(models.Model): name = models.CharField(max_length=50) normal = models.ForeignKey(Foo, related_name='normal_foo') fwd = models.ForeignKey("Whiz") back = models.ForeignKey("Foo") - def __unicode__(self): + def __str__(self): return "Bar %s" % self.place.name +@python_2_unicode_compatible class Whiz(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return "Whiz %s" % self.name +@python_2_unicode_compatible class Child(models.Model): parent = models.OneToOneField('Base') name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return "Child %s" % self.name +@python_2_unicode_compatible class Base(models.Model): name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return "Base %s" % self.name +@python_2_unicode_compatible class Article(models.Model): name = models.CharField(max_length=50) text = models.TextField() diff --git a/tests/regressiontests/syndication/models.py b/tests/regressiontests/syndication/models.py index a2c504e57f..10b3fe3a0c 100644 --- a/tests/regressiontests/syndication/models.py +++ b/tests/regressiontests/syndication/models.py @@ -1,6 +1,8 @@ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Entry(models.Model): title = models.CharField(max_length=200) date = models.DateTimeField() @@ -8,17 +10,18 @@ class Entry(models.Model): class Meta: ordering = ('date',) - def __unicode__(self): + def __str__(self): return self.title def get_absolute_url(self): return "/blog/%s/" % self.pk +@python_2_unicode_compatible class Article(models.Model): title = models.CharField(max_length=200) entry = models.ForeignKey(Entry) - def __unicode__(self): + def __str__(self): return self.title diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py index fd570700af..8b4aebbf22 100644 --- a/tests/regressiontests/templates/filters.py +++ b/tests/regressiontests/templates/filters.py @@ -13,14 +13,17 @@ from datetime import date, datetime, timedelta from django.test.utils import str_prefix from django.utils.tzinfo import LocalTimezone, FixedOffset from django.utils.safestring import mark_safe +from django.utils.encoding import python_2_unicode_compatible # These two classes are used to test auto-escaping of __unicode__ output. +@python_2_unicode_compatible class UnsafeClass: - def __unicode__(self): + def __str__(self): return 'you & me' +@python_2_unicode_compatible class SafeClass: - def __unicode__(self): + def __str__(self): return mark_safe('you > me') # RESULT SYNTAX -- @@ -111,7 +114,7 @@ def get_filter_tests(): # The make_list filter can destroy existing escaping, so the results are # escaped. 'filter-make_list01': ("{% autoescape off %}{{ a|make_list }}{% endautoescape %}", {"a": mark_safe("&")}, str_prefix("[%(_)s'&']")), - 'filter-make_list02': ("{{ a|make_list }}", {"a": mark_safe("&")}, "[u'&']"), + 'filter-make_list02': ("{{ a|make_list }}", {"a": mark_safe("&")}, str_prefix("[%(_)s'&']")), 'filter-make_list03': ('{% autoescape off %}{{ a|make_list|stringformat:"s"|safe }}{% endautoescape %}', {"a": mark_safe("&")}, str_prefix("[%(_)s'&']")), 'filter-make_list04': ('{{ a|make_list|stringformat:"s"|safe }}', {"a": mark_safe("&")}, str_prefix("[%(_)s'&']")), @@ -338,11 +341,11 @@ def get_filter_tests(): 'join04': (r'{% autoescape off %}{{ a|join:" & " }}{% endautoescape %}', {'a': ['alpha', 'beta & me']}, 'alpha & beta & me'), # Test that joining with unsafe joiners don't result in unsafe strings (#11377) - 'join05': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': ' & '}, 'alpha & beta & me'), - 'join06': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta & me'), - 'join07': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': ' & ' }, 'alpha & beta & me'), - 'join08': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta & me'), - + 'join05': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': ' & '}, 'alpha & beta & me'), + 'join06': (r'{{ a|join:var }}', {'a': ['alpha', 'beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta & me'), + 'join07': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': ' & ' }, 'alpha & beta & me'), + 'join08': (r'{{ a|join:var|lower }}', {'a': ['Alpha', 'Beta & me'], 'var': mark_safe(' & ')}, 'alpha & beta & me'), + 'date01': (r'{{ d|date:"m" }}', {'d': datetime(2008, 1, 1)}, '01'), 'date02': (r'{{ d|date }}', {'d': datetime(2008, 1, 1)}, 'Jan. 1, 2008'), #Ticket 9520: Make sure |date doesn't blow up on non-dates diff --git a/tests/regressiontests/templates/response.py b/tests/regressiontests/templates/response.py index 3c45b7a9d4..93919b95cd 100644 --- a/tests/regressiontests/templates/response.py +++ b/tests/regressiontests/templates/response.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import os import pickle import time @@ -29,16 +31,16 @@ class SimpleTemplateResponseTest(TestCase): def test_template_resolving(self): response = SimpleTemplateResponse('first/test.html') response.render() - self.assertEqual('First template\n', response.content) + self.assertEqual(response.content, b'First template\n') templates = ['foo.html', 'second/test.html', 'first/test.html'] response = SimpleTemplateResponse(templates) response.render() - self.assertEqual('Second template\n', response.content) + self.assertEqual(response.content, b'Second template\n') response = self._response() response.render() - self.assertEqual(response.content, 'foo') + self.assertEqual(response.content, b'foo') def test_explicit_baking(self): # explicit baking @@ -50,17 +52,17 @@ class SimpleTemplateResponseTest(TestCase): def test_render(self): # response is not re-rendered without the render call response = self._response().render() - self.assertEqual(response.content, 'foo') + self.assertEqual(response.content, b'foo') # rebaking doesn't change the rendered content response.template_name = Template('bar{{ baz }}') response.render() - self.assertEqual(response.content, 'foo') + self.assertEqual(response.content, b'foo') # but rendered content can be overridden by manually # setting content response.content = 'bar' - self.assertEqual(response.content, 'bar') + self.assertEqual(response.content, b'bar') def test_iteration_unrendered(self): # unrendered response raises an exception on iteration @@ -77,7 +79,7 @@ class SimpleTemplateResponseTest(TestCase): # iteration works for rendered responses response = self._response().render() res = [x for x in response] - self.assertEqual(res, ['foo']) + self.assertEqual(res, [b'foo']) def test_content_access_unrendered(self): # unrendered response raises an exception when content is accessed @@ -89,7 +91,7 @@ class SimpleTemplateResponseTest(TestCase): def test_content_access_rendered(self): # rendered response content can be accessed response = self._response().render() - self.assertEqual(response.content, 'foo') + self.assertEqual(response.content, b'foo') def test_set_content(self): # content can be overriden @@ -97,23 +99,23 @@ class SimpleTemplateResponseTest(TestCase): self.assertFalse(response.is_rendered) response.content = 'spam' self.assertTrue(response.is_rendered) - self.assertEqual(response.content, 'spam') + self.assertEqual(response.content, b'spam') response.content = 'baz' - self.assertEqual(response.content, 'baz') + self.assertEqual(response.content, b'baz') def test_dict_context(self): response = self._response('{{ foo }}{{ processors }}', {'foo': 'bar'}) self.assertEqual(response.context_data, {'foo': 'bar'}) response.render() - self.assertEqual(response.content, 'bar') + self.assertEqual(response.content, b'bar') def test_context_instance(self): response = self._response('{{ foo }}{{ processors }}', Context({'foo': 'bar'})) self.assertEqual(response.context_data.__class__, Context) response.render() - self.assertEqual(response.content, 'bar') + self.assertEqual(response.content, b'bar') def test_kwargs(self): response = self._response(content_type = 'application/json', status=504) @@ -140,7 +142,7 @@ class SimpleTemplateResponseTest(TestCase): # When the content is rendered, all the callbacks are invoked, too. response.render() - self.assertEqual('First template\n', response.content) + self.assertEqual(response.content, b'First template\n') self.assertEqual(post, ['post1','post2']) @@ -202,17 +204,17 @@ class TemplateResponseTest(TestCase): def test_render(self): response = self._response('{{ foo }}{{ processors }}').render() - self.assertEqual(response.content, 'yes') + self.assertEqual(response.content, b'yes') def test_render_with_requestcontext(self): response = self._response('{{ foo }}{{ processors }}', {'foo': 'bar'}).render() - self.assertEqual(response.content, 'baryes') + self.assertEqual(response.content, b'baryes') def test_render_with_context(self): response = self._response('{{ foo }}{{ processors }}', Context({'foo': 'bar'})).render() - self.assertEqual(response.content, 'bar') + self.assertEqual(response.content, b'bar') def test_kwargs(self): response = self._response(content_type = 'application/json', diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index edbb21b6bd..41f40e7467 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -27,6 +27,7 @@ from django.test import RequestFactory from django.test.utils import (setup_test_template_loader, restore_template_loaders, override_settings) from django.utils import unittest +from django.utils.encoding import python_2_unicode_compatible from django.utils.formats import date_format from django.utils.translation import activate, deactivate, ugettext as _ from django.utils.safestring import mark_safe @@ -46,7 +47,7 @@ from .response import (TemplateResponseTest, CacheMiddlewareTest, try: from .loaders import RenderToStringTest, EggLoaderTest except ImportError as e: - if "pkg_resources" in e.message: + if "pkg_resources" in e.args[0]: pass # If setuptools isn't installed, that's fine. Just move on. else: raise @@ -148,10 +149,11 @@ class SilentAttrClass(object): raise SomeException b = property(b) +@python_2_unicode_compatible class UTF8Class: - "Class whose __str__ returns non-ASCII data" + "Class whose __str__ returns non-ASCII data on Python 2" def __str__(self): - return 'ŠĐĆŽćžšđ'.encode('utf-8') + return 'ŠĐĆŽćžšđ' class Templates(unittest.TestCase): def setUp(self): @@ -408,8 +410,7 @@ class Templates(unittest.TestCase): ) failures = [] - tests = template_tests.items() - tests.sort() + tests = sorted(template_tests.items()) # Turn TEMPLATE_DEBUG off, because tests assume that. old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False @@ -418,7 +419,7 @@ class Templates(unittest.TestCase): old_invalid = settings.TEMPLATE_STRING_IF_INVALID expected_invalid_str = 'INVALID' - #Set ALLOWED_INCLUDE_ROOTS so that ssi works. + # Set ALLOWED_INCLUDE_ROOTS so that ssi works. old_allowed_include_roots = settings.ALLOWED_INCLUDE_ROOTS settings.ALLOWED_INCLUDE_ROOTS = ( os.path.dirname(os.path.abspath(__file__)), @@ -1452,8 +1453,9 @@ class Templates(unittest.TestCase): 'widthratio04': ('{% widthratio a b 100 %}', {'a':50,'b':100}, '50'), 'widthratio05': ('{% widthratio a b 100 %}', {'a':100,'b':100}, '100'), - # 62.5 should round to 63 - 'widthratio06': ('{% widthratio a b 100 %}', {'a':50,'b':80}, '63'), + # 62.5 should round to 63 on Python 2 and 62 on Python 3 + # See http://docs.python.org/py3k/whatsnew/3.0.html + 'widthratio06': ('{% widthratio a b 100 %}', {'a':50,'b':80}, '62' if six.PY3 else '63'), # 71.4 should round to 71 'widthratio07': ('{% widthratio a b 100 %}', {'a':50,'b':70}, '71'), @@ -1466,6 +1468,14 @@ class Templates(unittest.TestCase): # #10043: widthratio should allow max_width to be a variable 'widthratio11': ('{% widthratio a b c %}', {'a':50,'b':100, 'c': 100}, '50'), + # #18739: widthratio should handle None args consistently with non-numerics + 'widthratio12a': ('{% widthratio a b c %}', {'a':'a','b':100,'c':100}, ''), + 'widthratio12b': ('{% widthratio a b c %}', {'a':None,'b':100,'c':100}, ''), + 'widthratio13a': ('{% widthratio a b c %}', {'a':0,'b':'b','c':100}, ''), + 'widthratio13b': ('{% widthratio a b c %}', {'a':0,'b':None,'c':100}, ''), + 'widthratio14a': ('{% widthratio a b c %}', {'a':0,'b':100,'c':'c'}, template.TemplateSyntaxError), + 'widthratio14b': ('{% widthratio a b c %}', {'a':0,'b':100,'c':None}, template.TemplateSyntaxError), + ### WITH TAG ######################################################## 'with01': ('{% with key=dict.key %}{{ key }}{% endwith %}', {'dict': {'key': 50}}, '50'), 'legacywith01': ('{% with dict.key as key %}{{ key }}{% endwith %}', {'dict': {'key': 50}}, '50'), diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py index 59f8f423dd..d80293b358 100644 --- a/tests/regressiontests/test_client_regress/tests.py +++ b/tests/regressiontests/test_client_regress/tests.py @@ -858,7 +858,7 @@ class UnicodePayloadTests(TestCase): json = '{"english": "mountain pass"}' response = self.client.post("/test_client_regress/parse_unicode_json/", json, content_type="application/json") - self.assertEqual(response.content, json) + self.assertEqual(response.content, json.encode()) def test_unicode_payload_utf8(self): "A non-ASCII unicode data encoded as UTF-8 can be POSTed" @@ -888,7 +888,7 @@ class DummyFile(object): def __init__(self, filename): self.name = filename def read(self): - return 'TEST_FILE_CONTENT' + return b'TEST_FILE_CONTENT' class UploadedFileEncodingTest(TestCase): def test_file_encoding(self): diff --git a/tests/regressiontests/test_client_regress/views.py b/tests/regressiontests/test_client_regress/views.py index 8792a97dc0..9b0654806b 100644 --- a/tests/regressiontests/test_client_regress/views.py +++ b/tests/regressiontests/test_client_regress/views.py @@ -80,9 +80,7 @@ def return_json_file(request): # This just checks that the uploaded data is JSON obj_dict = json.loads(request.body.decode(charset)) - obj_json = json.dumps(obj_dict, encoding=charset, - cls=DjangoJSONEncoder, - ensure_ascii=False) + obj_json = json.dumps(obj_dict, cls=DjangoJSONEncoder, ensure_ascii=False) response = HttpResponse(obj_json.encode(charset), status=200, content_type='application/json; charset=%s' % charset) response['Content-Disposition'] = 'attachment; filename=testfile.json' diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index f43a855a59..3749014372 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -5,6 +5,7 @@ from django.forms import EmailField, IntegerField from django.http import HttpResponse from django.template.loader import render_to_string from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature +from django.utils import six from django.utils.unittest import skip from .models import Person @@ -495,12 +496,6 @@ __test__ = {"API_TEST": r""" >>> from django.utils.xmlutils import SimplerXMLGenerator >>> from django.utils.six import StringIO ->>> def produce_long(): -... return 42L - ->>> def produce_int(): -... return 42 - >>> def produce_json(): ... return json.dumps(['foo', {'bar': ('baz', None, 1.0, 2), 'whiz': 42}]) @@ -529,14 +524,6 @@ __test__ = {"API_TEST": r""" ... xml.endElement("bar") ... return stream.getvalue() -# Long values are normalized and are comparable to normal integers ... ->>> produce_long() -42 - -# ... and vice versa ->>> produce_int() -42L - # JSON output is normalized for field order, so it doesn't matter # which order json dictionary attributes are listed in output >>> produce_json() @@ -560,3 +547,21 @@ __test__ = {"API_TEST": r""" '<foo bbb="2.0" aaa="1.0">Hello</foo><bar ddd="4.0" ccc="3.0"></bar>' """} + +if not six.PY3: + __test__["API_TEST"] += """ +>>> def produce_long(): +... return 42L + +>>> def produce_int(): +... return 42 + +# Long values are normalized and are comparable to normal integers ... +>>> produce_long() +42 + +# ... and vice versa +>>> produce_int() +42L + +""" diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 500a0e0327..168892719a 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -1,12 +1,13 @@ """ Unit tests for reverse URL lookups. """ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist -from django.core.urlresolvers import (reverse, resolve, NoReverseMatch, - Resolver404, ResolverMatch, RegexURLResolver, RegexURLPattern) +from django.core.urlresolvers import (reverse, resolve, get_callable, + get_resolver, NoReverseMatch, Resolver404, ResolverMatch, RegexURLResolver, + RegexURLPattern) from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.shortcuts import redirect from django.test import TestCase @@ -171,6 +172,16 @@ class URLPatternReverse(TestCase): self.assertRaises(NoReverseMatch, reverse, None) class ResolverTests(unittest.TestCase): + def test_resolver_repr(self): + """ + Test repr of RegexURLResolver, especially when urlconf_name is a list + (#17892). + """ + # Pick a resolver from a namespaced urlconf + resolver = get_resolver('regressiontests.urlpatterns_reverse.namespace_urls') + sub_resolver = resolver.namespace_dict['test-ns1'][1] + self.assertIn('<RegexURLPattern list>', repr(sub_resolver)) + def test_non_regex(self): """ Verifies that we raise a Resolver404 if what we are resolving doesn't @@ -405,8 +416,8 @@ class RequestURLconfTests(TestCase): def test_urlconf(self): response = self.client.get('/test/me/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'outer:/test/me/,' - 'inner:/inner_urlconf/second_test/') + self.assertEqual(response.content, b'outer:/test/me/,' + b'inner:/inner_urlconf/second_test/') response = self.client.get('/inner_urlconf/second_test/') self.assertEqual(response.status_code, 200) response = self.client.get('/second_test/') @@ -422,7 +433,7 @@ class RequestURLconfTests(TestCase): self.assertEqual(response.status_code, 404) response = self.client.get('/second_test/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'outer:,inner:/second_test/') + self.assertEqual(response.content, b'outer:,inner:/second_test/') def test_urlconf_overridden_with_null(self): settings.MIDDLEWARE_CLASSES += ( @@ -519,3 +530,16 @@ class ErroneousViewTests(TestCase): """ # The regex error will be hit before NoReverseMatch can be raised self.assertRaises(ImproperlyConfigured, reverse, 'whatever blah blah') + +class ViewLoadingTests(TestCase): + def test_view_loading(self): + # A missing view (identified by an AttributeError) should raise + # ViewDoesNotExist, ... + self.assertRaisesRegexp(ViewDoesNotExist, ".*View does not exist in.*", + get_callable, + 'regressiontests.urlpatterns_reverse.views.i_should_not_exist') + # ... but if the AttributeError is caused by something else don't + # swallow it. + self.assertRaises(AttributeError, get_callable, + 'regressiontests.urlpatterns_reverse.views_broken.i_am_broken') + diff --git a/tests/regressiontests/urlpatterns_reverse/views_broken.py b/tests/regressiontests/urlpatterns_reverse/views_broken.py new file mode 100644 index 0000000000..4953aab239 --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/views_broken.py @@ -0,0 +1,2 @@ +# I just raise an AttributeError to confuse the view loading mechanism +raise AttributeError('I am here to confuse django.core.urlresolvers.get_callable') diff --git a/tests/regressiontests/utils/archive.py b/tests/regressiontests/utils/archive.py index 0927c624f3..5575f340f6 100644 --- a/tests/regressiontests/utils/archive.py +++ b/tests/regressiontests/utils/archive.py @@ -27,12 +27,14 @@ class ArchiveTester(object): os.chdir(self.old_cwd) def test_extract_method(self): - Archive(self.archive).extract(self.tmpdir) + with Archive(self.archive) as archive: + archive.extract(self.tmpdir) self.check_files(self.tmpdir) def test_extract_method_no_to_path(self): os.chdir(self.tmpdir) - Archive(self.archive_path).extract() + with Archive(self.archive_path) as archive: + archive.extract() self.check_files(self.tmpdir) def test_extract_function(self): diff --git a/tests/regressiontests/utils/crypto.py b/tests/regressiontests/utils/crypto.py index 52a286cb27..4c6b722ca9 100644 --- a/tests/regressiontests/utils/crypto.py +++ b/tests/regressiontests/utils/crypto.py @@ -6,7 +6,17 @@ import timeit import hashlib from django.utils import unittest -from django.utils.crypto import pbkdf2 +from django.utils.crypto import constant_time_compare, pbkdf2 + + +class TestUtilsCryptoMisc(unittest.TestCase): + + def test_constant_time_compare(self): + # It's hard to test for constant time, just test the result. + self.assertTrue(constant_time_compare(b'spam', b'spam')) + self.assertFalse(constant_time_compare(b'spam', b'eggs')) + self.assertTrue(constant_time_compare('spam', 'spam')) + self.assertFalse(constant_time_compare('spam', 'eggs')) class TestUtilsCryptoPBKDF2(unittest.TestCase): diff --git a/tests/regressiontests/utils/dateparse.py b/tests/regressiontests/utils/dateparse.py new file mode 100644 index 0000000000..1a6ca646b8 --- /dev/null +++ b/tests/regressiontests/utils/dateparse.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals + +from datetime import date, time, datetime + +from django.utils.dateparse import parse_date, parse_time, parse_datetime +from django.utils import unittest +from django.utils.tzinfo import FixedOffset + + +class DateParseTests(unittest.TestCase): + + def test_parse_date(self): + # Valid inputs + self.assertEqual(parse_date('2012-04-23'), date(2012, 4, 23)) + self.assertEqual(parse_date('2012-4-9'), date(2012, 4, 9)) + # Invalid inputs + self.assertEqual(parse_date('20120423'), None) + self.assertRaises(ValueError, parse_date, '2012-04-56') + + def test_parse_time(self): + # Valid inputs + self.assertEqual(parse_time('09:15:00'), time(9, 15)) + self.assertEqual(parse_time('10:10'), time(10, 10)) + self.assertEqual(parse_time('10:20:30.400'), time(10, 20, 30, 400000)) + self.assertEqual(parse_time('4:8:16'), time(4, 8, 16)) + # Invalid inputs + self.assertEqual(parse_time('091500'), None) + self.assertRaises(ValueError, parse_time, '09:15:90') + + def test_parse_datetime(self): + # Valid inputs + self.assertEqual(parse_datetime('2012-04-23T09:15:00'), + datetime(2012, 4, 23, 9, 15)) + self.assertEqual(parse_datetime('2012-4-9 4:8:16'), + datetime(2012, 4, 9, 4, 8, 16)) + self.assertEqual(parse_datetime('2012-04-23T09:15:00Z'), + datetime(2012, 4, 23, 9, 15, 0, 0, FixedOffset(0))) + self.assertEqual(parse_datetime('2012-4-9 4:8:16-0320'), + datetime(2012, 4, 9, 4, 8, 16, 0, FixedOffset(-200))) + self.assertEqual(parse_datetime('2012-04-23T10:20:30.400+02:30'), + datetime(2012, 4, 23, 10, 20, 30, 400000, FixedOffset(150))) + # Invalid inputs + self.assertEqual(parse_datetime('20120423091500'), None) + self.assertRaises(ValueError, parse_datetime, '2012-04-56T09:15:90') diff --git a/tests/regressiontests/utils/encoding.py b/tests/regressiontests/utils/encoding.py new file mode 100644 index 0000000000..d191845518 --- /dev/null +++ b/tests/regressiontests/utils/encoding.py @@ -0,0 +1,17 @@ +# -*- encoding: utf-8 -*- +from __future__ import unicode_literals + +from django.utils import unittest +from django.utils.encoding import force_bytes + + +class TestEncodingUtils(unittest.TestCase): + def test_force_bytes_exception(self): + """ + Test that force_bytes knows how to convert to bytes an exception + containing non-ASCII characters in its args. + """ + error_msg = "This is an exception, voilà" + exc = ValueError(error_msg) + result = force_bytes(exc) + self.assertEqual(result, error_msg.encode('utf-8')) diff --git a/tests/regressiontests/utils/html.py b/tests/regressiontests/utils/html.py index fe40d4eaae..98df80a5e2 100644 --- a/tests/regressiontests/utils/html.py +++ b/tests/regressiontests/utils/html.py @@ -146,3 +146,12 @@ class TestUtilsHtml(unittest.TestCase): ) for value, output in items: self.check_output(f, value, output) + + def test_remove_tags(self): + f = html.remove_tags + items = ( + ("<b><i>Yes</i></b>", "b i", "Yes"), + ("<a>x</a> <p><b>y</b></p>", "a b", "x <p>y</p>"), + ) + for value, tags, output in items: + self.assertEquals(f(value, tags), output) diff --git a/tests/regressiontests/utils/module_loading.py b/tests/regressiontests/utils/module_loading.py index cbd81c7294..dffb51966c 100644 --- a/tests/regressiontests/utils/module_loading.py +++ b/tests/regressiontests/utils/module_loading.py @@ -109,7 +109,12 @@ class ProxyFinder(object): def find_module(self, fullname, path=None): tail = fullname.rsplit('.', 1)[-1] try: - self._cache[fullname] = imp.find_module(tail, path) + fd, fn, info = imp.find_module(tail, path) + if fullname in self._cache: + old_fd = self._cache[fullname][0] + if old_fd: + old_fd.close() + self._cache[fullname] = (fd, fn, info) except ImportError: return None else: @@ -119,7 +124,11 @@ class ProxyFinder(object): if fullname in sys.modules: return sys.modules[fullname] fd, fn, info = self._cache[fullname] - return imp.load_module(fullname, fd, fn, info) + try: + return imp.load_module(fullname, fd, fn, info) + finally: + if fd: + fd.close() class TestFinder(object): def __init__(self, *args, **kwargs): diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index a7deeeefae..f4fa75b177 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -3,26 +3,28 @@ Tests for django.utils. """ from __future__ import absolute_import +from .archive import TestBzip2Tar, TestGzipTar, TestTar, TestZip +from .baseconv import TestBaseConv +from .checksums import TestUtilsChecksums +from .crypto import TestUtilsCryptoMisc, TestUtilsCryptoPBKDF2 +from .datastructures import (DictWrapperTests, ImmutableListTests, + MergeDictTests, MultiValueDictTests, SortedDictTests) from .dateformat import DateFormatTests +from .dateparse import DateParseTests +from .datetime_safe import DatetimeTests +from .decorators import DecoratorFromMiddlewareTests +from .encoding import TestEncodingUtils from .feedgenerator import FeedgeneratorTest -from .module_loading import DefaultLoader, EggLoader, CustomLoader -from .termcolors import TermColorTests +from .functional import FunctionalTestCase from .html import TestUtilsHtml from .http import TestUtilsHttp -from .checksums import TestUtilsChecksums -from .text import TestUtilsText +from .ipv6 import TestUtilsIPv6 +from .jslex import JsToCForGettextTest, JsTokensTest +from .module_loading import CustomLoader, DefaultLoader, EggLoader +from .regex_helper import NormalizeTests from .simplelazyobject import TestUtilsSimpleLazyObject -from .decorators import DecoratorFromMiddlewareTests -from .functional import FunctionalTestCase +from .termcolors import TermColorTests +from .text import TestUtilsText from .timesince import TimesinceTests -from .datastructures import (MultiValueDictTests, SortedDictTests, - DictWrapperTests, ImmutableListTests, MergeDictTests) -from .tzinfo import TzinfoTests -from .datetime_safe import DatetimeTests -from .baseconv import TestBaseConv -from .jslex import JsTokensTest, JsToCForGettextTest -from .ipv6 import TestUtilsIPv6 from .timezone import TimezoneTests -from .crypto import TestUtilsCryptoPBKDF2 -from .archive import TestZip, TestTar, TestGzipTar, TestBzip2Tar -from .regex_helper import NormalizeTests +from .tzinfo import TzinfoTests diff --git a/tests/regressiontests/utils/text.py b/tests/regressiontests/utils/text.py index dd6de63841..ebf67952f9 100644 --- a/tests/regressiontests/utils/text.py +++ b/tests/regressiontests/utils/text.py @@ -113,3 +113,11 @@ class TestUtilsText(SimpleTestCase): self.assertEqual(text.wrap(long_word, 20), long_word) self.assertEqual(text.wrap('a %s word' % long_word, 10), 'a\n%s\nword' % long_word) + + def test_slugify(self): + items = ( + ('Hello, World!', 'hello-world'), + ('spam & eggs', 'spam-eggs'), + ) + for value, output in items: + self.assertEqual(text.slugify(value), output) diff --git a/tests/regressiontests/views/models.py b/tests/regressiontests/views/models.py index 54f5c1c1f0..461f98c028 100644 --- a/tests/regressiontests/views/models.py +++ b/tests/regressiontests/views/models.py @@ -3,16 +3,19 @@ Regression tests for Django built-in views. """ from django.db import models +from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible class Author(models.Model): name = models.CharField(max_length=100) - def __unicode__(self): + def __str__(self): return self.name def get_absolute_url(self): return '/views/authors/%s/' % self.id +@python_2_unicode_compatible class BaseArticle(models.Model): """ An abstract article Model so that we can create article models with and @@ -25,7 +28,7 @@ class BaseArticle(models.Model): class Meta: abstract = True - def __unicode__(self): + def __str__(self): return self.title class Article(BaseArticle): diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index e8a7d49e79..56383ac196 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -36,7 +36,7 @@ class DebugViewTests(TestCase): self.assertEqual(response.status_code, 500) data = { - 'file_data.txt': SimpleUploadedFile('file_data.txt', 'haha'), + 'file_data.txt': SimpleUploadedFile('file_data.txt', b'haha'), } response = self.client.post('/raises/', data) self.assertContains(response, 'file_data.txt', status_code=500) diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py index cb580267d2..601df6d512 100644 --- a/tests/regressiontests/views/tests/i18n.py +++ b/tests/regressiontests/views/tests/i18n.py @@ -7,7 +7,8 @@ from os import path from django.conf import settings from django.core.urlresolvers import reverse from django.test import TestCase -from django.utils.translation import override, activate, get_language +from django.utils import six +from django.utils.translation import override, get_language from django.utils.text import javascript_quote from ..urls import locale_dir @@ -29,20 +30,21 @@ class I18NTests(TestCase): def test_jsi18n(self): """The javascript_catalog can be deployed with language settings""" - saved_lang = get_language() for lang_code in ['es', 'fr', 'ru']: - activate(lang_code) - catalog = gettext.translation('djangojs', locale_dir, [lang_code]) - trans_txt = catalog.ugettext('this is to be translated') - response = self.client.get('/views/jsi18n/') - # in response content must to be a line like that: - # catalog['this is to be translated'] = 'same_that_trans_txt' - # javascript_quote is used to be able to check unicode strings - self.assertContains(response, javascript_quote(trans_txt), 1) - if lang_code == 'fr': - # Message with context (msgctxt) - self.assertContains(response, "['month name\x04May'] = 'mai';", 1) - activate(saved_lang) + with override(lang_code): + catalog = gettext.translation('djangojs', locale_dir, [lang_code]) + if six.PY3: + trans_txt = catalog.gettext('this is to be translated') + else: + trans_txt = catalog.ugettext('this is to be translated') + response = self.client.get('/views/jsi18n/') + # in response content must to be a line like that: + # catalog['this is to be translated'] = 'same_that_trans_txt' + # javascript_quote is used to be able to check unicode strings + self.assertContains(response, javascript_quote(trans_txt), 1) + if lang_code == 'fr': + # Message with context (msgctxt) + self.assertContains(response, "['month name\x04May'] = 'mai';", 1) class JsI18NTests(TestCase): diff --git a/tests/regressiontests/views/tests/shortcuts.py b/tests/regressiontests/views/tests/shortcuts.py index a48bb25d0b..62bd82f4a7 100644 --- a/tests/regressiontests/views/tests/shortcuts.py +++ b/tests/regressiontests/views/tests/shortcuts.py @@ -12,44 +12,44 @@ class ShortcutTests(TestCase): def test_render_to_response(self): response = self.client.get('/shortcuts/render_to_response/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'FOO.BAR..\n') + self.assertEqual(response.content, b'FOO.BAR..\n') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_render_to_response_with_request_context(self): response = self.client.get('/shortcuts/render_to_response/request_context/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'FOO.BAR../path/to/static/media/\n') + self.assertEqual(response.content, b'FOO.BAR../path/to/static/media/\n') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_render_to_response_with_mimetype(self): response = self.client.get('/shortcuts/render_to_response/mimetype/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'FOO.BAR..\n') + self.assertEqual(response.content, b'FOO.BAR..\n') self.assertEqual(response['Content-Type'], 'application/x-rendertest') def test_render(self): response = self.client.get('/shortcuts/render/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'FOO.BAR../path/to/static/media/\n') + self.assertEqual(response.content, b'FOO.BAR../path/to/static/media/\n') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') self.assertEqual(response.context.current_app, None) def test_render_with_base_context(self): response = self.client.get('/shortcuts/render/base_context/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'FOO.BAR..\n') + self.assertEqual(response.content, b'FOO.BAR..\n') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_render_with_content_type(self): response = self.client.get('/shortcuts/render/content_type/') self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'FOO.BAR../path/to/static/media/\n') + self.assertEqual(response.content, b'FOO.BAR../path/to/static/media/\n') self.assertEqual(response['Content-Type'], 'application/x-rendertest') def test_render_with_status(self): response = self.client.get('/shortcuts/render/status/') self.assertEqual(response.status_code, 403) - self.assertEqual(response.content, 'FOO.BAR../path/to/static/media/\n') + self.assertEqual(response.content, b'FOO.BAR../path/to/static/media/\n') def test_render_with_current_app(self): response = self.client.get('/shortcuts/render/current_app/') diff --git a/tests/regressiontests/wsgi/tests.py b/tests/regressiontests/wsgi/tests.py index a482a5c1eb..5a40bd88e2 100644 --- a/tests/regressiontests/wsgi/tests.py +++ b/tests/regressiontests/wsgi/tests.py @@ -40,8 +40,8 @@ class WSGITest(TestCase): response_data["headers"], [('Content-Type', 'text/html; charset=utf-8')]) self.assertEqual( - six.text_type(response), - "Content-Type: text/html; charset=utf-8\n\nHello World!") + bytes(response), + b"Content-Type: text/html; charset=utf-8\r\n\r\nHello World!") class GetInternalWSGIApplicationTest(unittest.TestCase): |
