summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Rosner <brosner@gmail.com>2008-06-26 15:42:33 +0000
committerBrian Rosner <brosner@gmail.com>2008-06-26 15:42:33 +0000
commitc8da0874c78ed4c6e1ad08cc78228799a333f76c (patch)
treebef645a8eb2c1a17f013a1031ed34494547dced0
parentf15845c573f019fc7f6d7404add122f9b7c52dc4 (diff)
newforms-admin: Merged from trunk up to [7766].
git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7770 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--AUTHORS1
-rw-r--r--django/conf/locale/pl/LC_MESSAGES/django.mobin66404 -> 66481 bytes
-rw-r--r--django/conf/locale/pl/LC_MESSAGES/django.po226
-rw-r--r--django/contrib/auth/views.py14
-rw-r--r--django/contrib/sites/tests.py4
-rw-r--r--django/core/servers/basehttp.py2
-rw-r--r--django/db/backends/__init__.py1
-rw-r--r--django/db/backends/oracle/base.py1
-rw-r--r--django/db/backends/oracle/creation.py12
-rw-r--r--django/db/models/fields/__init__.py4
-rw-r--r--django/db/models/fields/related.py9
-rw-r--r--django/db/models/options.py11
-rw-r--r--django/db/models/query.py36
-rw-r--r--django/db/models/sql/query.py72
-rw-r--r--django/db/models/sql/subqueries.py8
-rw-r--r--django/template/defaulttags.py24
-rw-r--r--django/templatetags/cache.py25
-rw-r--r--django/utils/datastructures.py31
-rw-r--r--django/utils/html.py52
-rw-r--r--docs/cache.txt11
-rw-r--r--docs/contributing.txt77
-rw-r--r--docs/db-api.txt8
-rw-r--r--docs/model-api.txt4
-rw-r--r--tests/modeltests/model_inheritance/models.py45
-rw-r--r--tests/regressiontests/datastructures/tests.py8
-rw-r--r--tests/regressiontests/defaultfilters/tests.py6
-rw-r--r--tests/regressiontests/model_inheritance_regress/models.py22
-rw-r--r--tests/regressiontests/queries/models.py95
-rw-r--r--tests/regressiontests/templates/loaders.py26
-rw-r--r--tests/regressiontests/templates/tests.py79
30 files changed, 588 insertions, 326 deletions
diff --git a/AUTHORS b/AUTHORS
index 4966384776..bfaf6db6ca 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -385,6 +385,7 @@ answer newbie questions, and generally made Django that much better:
Wang Chun <wangchun@exoweb.net>
Filip Wasilewski <filip.wasilewski@gmail.com>
Dan Watson <http://theidioteque.net/>
+ Joel Watts <joel@joelwatts.com>
Chris Wesseling <Chris.Wesseling@cwi.nl>
James Wheare <django@sparemint.com>
charly.wilhelm@gmail.com
diff --git a/django/conf/locale/pl/LC_MESSAGES/django.mo b/django/conf/locale/pl/LC_MESSAGES/django.mo
index b82290dd04..5cdabb1d11 100644
--- a/django/conf/locale/pl/LC_MESSAGES/django.mo
+++ b/django/conf/locale/pl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/django/conf/locale/pl/LC_MESSAGES/django.po b/django/conf/locale/pl/LC_MESSAGES/django.po
index d084d90688..6f73bb45d0 100644
--- a/django/conf/locale/pl/LC_MESSAGES/django.po
+++ b/django/conf/locale/pl/LC_MESSAGES/django.po
@@ -5,9 +5,9 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2008-04-14 18:02+0200\n"
+"POT-Creation-Date: 2008-06-24 07:36+0200\n"
"PO-Revision-Date: 2008-02-25 15:53+0100\n"
-"Last-Translator: Piotr Lewandowski <django@icomputing.pl>\n"
+"Last-Translator: Jarek Zgoda <jarek.zgoda@gmail.com>\n"
"Language-Team: Polish <pl@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -16,191 +16,199 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%"
"100<10 || n%100>=20) ? 1 : 2);\n"
-#: conf/global_settings.py:39
+#: conf/global_settings.py:44
msgid "Arabic"
msgstr "Arabski"
-#: conf/global_settings.py:40
+#: conf/global_settings.py:45
msgid "Bengali"
msgstr "Bengalski"
-#: conf/global_settings.py:41
+#: conf/global_settings.py:46
msgid "Bulgarian"
msgstr "Bułgarski"
-#: conf/global_settings.py:42
+#: conf/global_settings.py:47
msgid "Catalan"
msgstr "Kataloński"
-#: conf/global_settings.py:43
+#: conf/global_settings.py:48
msgid "Czech"
msgstr "Czeski"
-#: conf/global_settings.py:44
+#: conf/global_settings.py:49
msgid "Welsh"
msgstr "Walijski"
-#: conf/global_settings.py:45
+#: conf/global_settings.py:50
msgid "Danish"
msgstr "Duński"
-#: conf/global_settings.py:46
+#: conf/global_settings.py:51
msgid "German"
msgstr "Niemiecki"
-#: conf/global_settings.py:47
+#: conf/global_settings.py:52
msgid "Greek"
msgstr "Grecki"
-#: conf/global_settings.py:48
+#: conf/global_settings.py:53
msgid "English"
msgstr "Angielski"
-#: conf/global_settings.py:49
+#: conf/global_settings.py:54
msgid "Spanish"
msgstr "Hiszpański"
-#: conf/global_settings.py:50
+#: conf/global_settings.py:55
+msgid "Estonian"
+msgstr "Estoński"
+
+#: conf/global_settings.py:56
msgid "Argentinean Spanish"
msgstr "Hiszpański argentyński"
-#: conf/global_settings.py:51
+#: conf/global_settings.py:57
msgid "Basque"
msgstr "Baskijski"
-#: conf/global_settings.py:52
+#: conf/global_settings.py:58
msgid "Persian"
msgstr "Perski"
-#: conf/global_settings.py:53
+#: conf/global_settings.py:59
msgid "Finnish"
msgstr "Fiński"
-#: conf/global_settings.py:54
+#: conf/global_settings.py:60
msgid "French"
msgstr "Francuski"
-#: conf/global_settings.py:55
+#: conf/global_settings.py:61
msgid "Irish"
msgstr "Irlandzki"
-#: conf/global_settings.py:56
+#: conf/global_settings.py:62
msgid "Galician"
msgstr "Galicyjski"
-#: conf/global_settings.py:57
+#: conf/global_settings.py:63
msgid "Hungarian"
msgstr "Węgierski"
-#: conf/global_settings.py:58
+#: conf/global_settings.py:64
msgid "Hebrew"
msgstr "Hebrajski"
-#: conf/global_settings.py:59
+#: conf/global_settings.py:65
msgid "Croatian"
msgstr "Chorwacki"
-#: conf/global_settings.py:60
+#: conf/global_settings.py:66
msgid "Icelandic"
msgstr "Islandzki"
-#: conf/global_settings.py:61
+#: conf/global_settings.py:67
msgid "Italian"
msgstr "Włoski"
-#: conf/global_settings.py:62
+#: conf/global_settings.py:68
msgid "Japanese"
msgstr "Japoński"
-#: conf/global_settings.py:63
+#: conf/global_settings.py:69
msgid "Georgian"
msgstr "Gruziński"
-#: conf/global_settings.py:64
+#: conf/global_settings.py:70
msgid "Korean"
msgstr "Koreański"
-#: conf/global_settings.py:65
+#: conf/global_settings.py:71
msgid "Khmer"
msgstr "Khmerski"
-#: conf/global_settings.py:66
+#: conf/global_settings.py:72
msgid "Kannada"
msgstr "Kannada"
-#: conf/global_settings.py:67
+#: conf/global_settings.py:73
msgid "Latvian"
msgstr "Łotewski"
-#: conf/global_settings.py:68
+#: conf/global_settings.py:74
+msgid "Lithuanian"
+msgstr "Litewski"
+
+#: conf/global_settings.py:75
msgid "Macedonian"
msgstr "Macedoński"
-#: conf/global_settings.py:69
+#: conf/global_settings.py:76
msgid "Dutch"
msgstr "Holenderski"
-#: conf/global_settings.py:70
+#: conf/global_settings.py:77
msgid "Norwegian"
msgstr "Norweski"
-#: conf/global_settings.py:71
+#: conf/global_settings.py:78
msgid "Polish"
msgstr "Polski"
-#: conf/global_settings.py:72
+#: conf/global_settings.py:79
msgid "Portugese"
msgstr "Portugalski"
-#: conf/global_settings.py:73
+#: conf/global_settings.py:80
msgid "Brazilian Portuguese"
msgstr "Brazylijski portugalski"
-#: conf/global_settings.py:74
+#: conf/global_settings.py:81
msgid "Romanian"
msgstr "Rumuński"
-#: conf/global_settings.py:75
+#: conf/global_settings.py:82
msgid "Russian"
msgstr "Rosyjski"
-#: conf/global_settings.py:76
+#: conf/global_settings.py:83
msgid "Slovak"
msgstr "Słowacki"
-#: conf/global_settings.py:77
+#: conf/global_settings.py:84
msgid "Slovenian"
msgstr "Słoweński"
-#: conf/global_settings.py:78
+#: conf/global_settings.py:85
msgid "Serbian"
msgstr "Serbski"
-#: conf/global_settings.py:79
+#: conf/global_settings.py:86
msgid "Swedish"
msgstr "Szwedzki"
-#: conf/global_settings.py:80
+#: conf/global_settings.py:87
msgid "Tamil"
msgstr "Tamilski"
-#: conf/global_settings.py:81
+#: conf/global_settings.py:88
msgid "Telugu"
msgstr "Telugu"
-#: conf/global_settings.py:82
+#: conf/global_settings.py:89
msgid "Turkish"
msgstr "Turecki"
-#: conf/global_settings.py:83
+#: conf/global_settings.py:90
msgid "Ukrainian"
msgstr "Ukraiński"
-#: conf/global_settings.py:84
+#: conf/global_settings.py:91
msgid "Simplified Chinese"
msgstr "Uproszczony chiński"
-#: conf/global_settings.py:85
+#: conf/global_settings.py:92
msgid "Traditional Chinese"
msgstr "Chiński tradycyjny"
@@ -417,7 +425,7 @@ msgstr ""
#: contrib/admin/templates/admin/delete_confirmation.html:25
msgid "Yes, I'm sure"
-msgstr "Tak, usuń"
+msgstr "Tak, na pewno"
#: contrib/admin/templates/admin/filter.html:2
#, python-format
@@ -824,15 +832,15 @@ msgstr ""
"Twoja przeglądarka nie chce akceptować ciasteczek. Zmień jej ustawienia i "
"spróbuj ponownie."
-#: contrib/admin/views/decorators.py:90
-msgid "Usernames cannot contain the '@' character."
-msgstr "Nazwy użytkowników nie mogą zawierać znaków '@'."
-
-#: contrib/admin/views/decorators.py:92
+#: contrib/admin/views/decorators.py:89
#, python-format
msgid "Your e-mail address is not your username. Try '%s' instead."
msgstr "Twój adres e-mail to nie jest twój login. Spróbuj '%s'."
+#: contrib/admin/views/decorators.py:93
+msgid "Usernames cannot contain the '@' character."
+msgstr "Nazwy użytkowników nie mogą zawierać znaków '@'."
+
#: contrib/admin/views/doc.py:48 contrib/admin/views/doc.py:50
#: contrib/admin/views/doc.py:52
msgid "tag:"
@@ -1063,7 +1071,7 @@ msgstr "Zaznacz %s"
msgid "Select %s to change"
msgstr "Zaznacz %s aby zmienić"
-#: contrib/admin/views/main.py:784
+#: contrib/admin/views/main.py:765
msgid "Database error"
msgstr "Błąd bazy danych"
@@ -1128,15 +1136,15 @@ msgstr "uprawnienia"
msgid "group"
msgstr "grupa"
-#: contrib/auth/models.py:98 contrib/auth/models.py:141
+#: contrib/auth/models.py:98 contrib/auth/models.py:148
msgid "groups"
msgstr "grupy"
-#: contrib/auth/models.py:131
+#: contrib/auth/models.py:138
msgid "username"
msgstr "użytkownik"
-#: contrib/auth/models.py:131
+#: contrib/auth/models.py:138
msgid ""
"Required. 30 characters or fewer. Alphanumeric characters only (letters, "
"digits and underscores)."
@@ -1144,23 +1152,23 @@ msgstr ""
"Wymagane. 30 znaków lub mniej. Tylko znaki alfanumeryczne (litery, cyfry i "
"podkreślenia)."
-#: contrib/auth/models.py:132
+#: contrib/auth/models.py:139
msgid "first name"
msgstr "Imię"
-#: contrib/auth/models.py:133
+#: contrib/auth/models.py:140
msgid "last name"
msgstr "Nazwisko"
-#: contrib/auth/models.py:134
+#: contrib/auth/models.py:141
msgid "e-mail address"
msgstr "adres e-mail"
-#: contrib/auth/models.py:135
+#: contrib/auth/models.py:142
msgid "password"
msgstr "hasło"
-#: contrib/auth/models.py:135
+#: contrib/auth/models.py:142
msgid ""
"Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change "
"password form</a>."
@@ -1168,19 +1176,19 @@ msgstr ""
"Użyj '[algo]$[salt]$[hexdigest]' lub <a href=\"password/\">formularza zmiany "
"hasła</a>."
-#: contrib/auth/models.py:136
+#: contrib/auth/models.py:143
msgid "staff status"
msgstr "w zespole"
-#: contrib/auth/models.py:136
+#: contrib/auth/models.py:143
msgid "Designates whether the user can log into this admin site."
msgstr "Oznacza czy użytkownik może zalogować się do panelu admina."
-#: contrib/auth/models.py:137
+#: contrib/auth/models.py:144
msgid "active"
msgstr "aktywny"
-#: contrib/auth/models.py:137
+#: contrib/auth/models.py:144
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
@@ -1188,11 +1196,11 @@ msgstr ""
"Oznacza czy użytkownika należy uważać za aktywnego. Odznacz tozamiast usuwać "
"konta."
-#: contrib/auth/models.py:138
+#: contrib/auth/models.py:145
msgid "superuser status"
msgstr "Główny Administrator"
-#: contrib/auth/models.py:138
+#: contrib/auth/models.py:145
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
@@ -1200,15 +1208,15 @@ msgstr ""
"Oznacza, że ten użytkownik ma wszystkie uprawnienia bez jawnego "
"przypisywania ich."
-#: contrib/auth/models.py:139
+#: contrib/auth/models.py:146
msgid "last login"
msgstr "ostatnio zalogowany"
-#: contrib/auth/models.py:140
+#: contrib/auth/models.py:147
msgid "date joined"
msgstr "data przyłączenia"
-#: contrib/auth/models.py:142
+#: contrib/auth/models.py:149
msgid ""
"In addition to the permissions manually assigned, this user will also get "
"all permissions granted to each group he/she is in."
@@ -1216,39 +1224,39 @@ msgstr ""
"Oprócz uprawnień przypisanych bezpośrednio użytkownikowi otrzyma on "
"uprawnienia grup, do których należy."
-#: contrib/auth/models.py:143
+#: contrib/auth/models.py:150
msgid "user permissions"
msgstr "uprawnienia użytkownika"
-#: contrib/auth/models.py:147
+#: contrib/auth/models.py:154
msgid "user"
msgstr "użytkownik"
-#: contrib/auth/models.py:148
+#: contrib/auth/models.py:155
msgid "users"
msgstr "użytkownicy"
-#: contrib/auth/models.py:154
+#: contrib/auth/models.py:161
msgid "Personal info"
msgstr "Dane osobowe"
-#: contrib/auth/models.py:155
+#: contrib/auth/models.py:162
msgid "Permissions"
msgstr "Uprawnienia"
-#: contrib/auth/models.py:156
+#: contrib/auth/models.py:163
msgid "Important dates"
msgstr "Ważne daty"
-#: contrib/auth/models.py:157
+#: contrib/auth/models.py:164
msgid "Groups"
msgstr "Grupy"
-#: contrib/auth/models.py:316
+#: contrib/auth/models.py:323
msgid "message"
msgstr "wiadomość"
-#: contrib/auth/views.py:47
+#: contrib/auth/views.py:48
msgid "Logged out"
msgstr "Wylogowany"
@@ -3516,23 +3524,23 @@ msgstr "przekieruj"
msgid "redirects"
msgstr "przekierowania"
-#: contrib/sessions/models.py:41
+#: contrib/sessions/models.py:45
msgid "session key"
msgstr "klucz sesji"
-#: contrib/sessions/models.py:42
+#: contrib/sessions/models.py:47
msgid "session data"
msgstr "data sesji"
-#: contrib/sessions/models.py:43
+#: contrib/sessions/models.py:48
msgid "expire date"
msgstr "data wygaśnięcia sesji"
-#: contrib/sessions/models.py:48
+#: contrib/sessions/models.py:53
msgid "session"
msgstr "sesja"
-#: contrib/sessions/models.py:49
+#: contrib/sessions/models.py:54
msgid "sessions"
msgstr "sesje"
@@ -3617,7 +3625,7 @@ msgstr "Rok nie może być wcześniejszy niż 1900."
msgid "Invalid date: %s"
msgstr "Niepoprawna data: %s"
-#: core/validators.py:156 db/models/fields/__init__.py:527
+#: core/validators.py:156 db/models/fields/__init__.py:548
msgid "Enter a valid date in YYYY-MM-DD format."
msgstr "Proszę wpisać poprawną datę w formacie RRRR-MM-DD."
@@ -3625,7 +3633,7 @@ msgstr "Proszę wpisać poprawną datę w formacie RRRR-MM-DD."
msgid "Enter a valid time in HH:MM format."
msgstr "Proszę wpisać poprawną godzinę w formacie HH:MM."
-#: core/validators.py:165 db/models/fields/__init__.py:604
+#: core/validators.py:165 db/models/fields/__init__.py:625
msgid "Enter a valid date/time in YYYY-MM-DD HH:MM format."
msgstr "Wprowadź poprawną datę i godzinę w formacie RRRR-MM-DD GG:MM."
@@ -3888,60 +3896,60 @@ msgstr ""
msgid "%(object)s with this %(type)s already exists for the given %(field)s."
msgstr "%(object)s z %(type)s już istnieje dla %(field)s."
-#: db/models/fields/__init__.py:52
+#: db/models/fields/__init__.py:51
#, python-format
msgid "%(optname)s with this %(fieldname)s already exists."
msgstr "Już istnieje %(optname)s z %(fieldname)s."
-#: db/models/fields/__init__.py:161 db/models/fields/__init__.py:327
-#: db/models/fields/__init__.py:759 db/models/fields/__init__.py:770
+#: db/models/fields/__init__.py:176 db/models/fields/__init__.py:348
+#: db/models/fields/__init__.py:780 db/models/fields/__init__.py:791
#: newforms/fields.py:46 oldforms/__init__.py:374
msgid "This field is required."
msgstr "To pole jest wymagane."
-#: db/models/fields/__init__.py:427
+#: db/models/fields/__init__.py:448
msgid "This value must be an integer."
msgstr "Ta wartość musi być liczbą całkowitą."
-#: db/models/fields/__init__.py:466
+#: db/models/fields/__init__.py:487
msgid "This value must be either True or False."
msgstr ""
"Ta wartość musi być wartością logiczną (True, False - prawda lub fałsz)."
-#: db/models/fields/__init__.py:490
+#: db/models/fields/__init__.py:511
msgid "This field cannot be null."
msgstr "To pole nie może być puste."
-#: db/models/fields/__init__.py:668
+#: db/models/fields/__init__.py:689
msgid "This value must be a decimal number."
msgstr "Ta wartość musi być liczbą dziesiętną."
-#: db/models/fields/__init__.py:779
+#: db/models/fields/__init__.py:800
msgid "Enter a valid filename."
msgstr "Wpisz poprawną nazwę pliku."
-#: db/models/fields/__init__.py:960
+#: db/models/fields/__init__.py:981
msgid "This value must be either None, True or False."
msgstr ""
"Ta wartość musi być jedną z None (nic), True (prawda) lub False (fałsz)."
-#: db/models/fields/related.py:93
+#: db/models/fields/related.py:94
#, python-format
msgid "Please enter a valid %s."
msgstr "Proszę wpisać poprawne %s."
-#: db/models/fields/related.py:701
+#: db/models/fields/related.py:746
msgid "Separate multiple IDs with commas."
msgstr "Oddziel identyfikatory przecinkami."
-#: db/models/fields/related.py:703
+#: db/models/fields/related.py:748
msgid ""
"Hold down \"Control\", or \"Command\" on a Mac, to select more than one."
msgstr ""
"Przytrzymaj wciśnięty klawisz \"Ctrl\" lub \"Command\" na Mac'u aby "
"zaznaczyć więcej niż jeden wybór."
-#: db/models/fields/related.py:750
+#: db/models/fields/related.py:795
#, python-format
msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid."
msgid_plural ""
@@ -4003,11 +4011,11 @@ msgstr "Upewnij się, że jest nie więcej niż %s miejsc po przecinku."
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Upewnij się, że jest nie więcej niż %s miejsc przed przecinkiem."
-#: newforms/fields.py:263 newforms/fields.py:751
+#: newforms/fields.py:263 newforms/fields.py:750
msgid "Enter a valid date."
msgstr "Wpisz poprawną datę."
-#: newforms/fields.py:296 newforms/fields.py:752
+#: newforms/fields.py:296 newforms/fields.py:751
msgid "Enter a valid time."
msgstr "Wpisz poprawną godzinę."
@@ -4031,25 +4039,25 @@ msgstr "Wpisz poprawny URL."
msgid "This URL appears to be a broken link."
msgstr "Ten odnośnik jest nieprawidłowy."
-#: newforms/fields.py:560 newforms/models.py:299
+#: newforms/fields.py:559 newforms/models.py:305
msgid "Select a valid choice. That choice is not one of the available choices."
msgstr "Wybierz poprawną wartość. Podana nie jest jednym z dostępnych wyborów."
-#: newforms/fields.py:599
+#: newforms/fields.py:598
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Wybierz poprawną wartość. %(value)s nie jest jednym z dostępnych wyborów."
-#: newforms/fields.py:600 newforms/fields.py:662 newforms/models.py:371
+#: newforms/fields.py:599 newforms/fields.py:661 newforms/models.py:372
msgid "Enter a list of values."
msgstr "Podaj listę wartości."
-#: newforms/fields.py:780
+#: newforms/fields.py:779
msgid "Enter a valid IPv4 address."
msgstr "Wprowadź poprawny adres IPv4."
-#: newforms/models.py:372
+#: newforms/models.py:373
#, python-format
msgid "Select a valid choice. %s is not one of the available choices."
msgstr "Wybierz poprawną wartość. %s nie jest jednym z dostępnych wyborów."
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index 0886d12d02..0a52240631 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -1,12 +1,13 @@
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm, AdminPasswordChangeForm
from django.core.exceptions import PermissionDenied
from django.shortcuts import render_to_response, get_object_or_404
-from django.template import RequestContext
from django.contrib.sites.models import Site, RequestSite
from django.http import HttpResponseRedirect
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.template import RequestContext
+from django.utils.http import urlquote
from django.utils.html import escape
from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
@@ -62,7 +63,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
if not login_url:
from django.conf import settings
login_url = settings.LOGIN_URL
- return HttpResponseRedirect('%s?%s=%s' % (login_url, redirect_field_name, next))
+ return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next)))
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html',
@@ -73,7 +74,10 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas
if is_admin_site:
form.save(domain_override=request.META['HTTP_HOST'])
else:
- form.save(email_template_name=email_template_name)
+ if Site._meta.installed:
+ form.save(email_template_name=email_template_name)
+ else:
+ form.save(domain_override=RequestSite(request).domain, email_template_name=email_template_name)
return HttpResponseRedirect('%sdone/' % request.path)
else:
form = password_reset_form()
diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py
index d2ec331eca..5c38ee1b6d 100644
--- a/django/contrib/sites/tests.py
+++ b/django/contrib/sites/tests.py
@@ -2,8 +2,8 @@
>>> # Make sure that get_current() does not return a deleted Site object.
>>> from django.contrib.sites.models import Site
>>> s = Site.objects.get_current()
->>> s
-<Site: example.com>
+>>> isinstance(s, Site)
+True
>>> s.delete()
>>> Site.objects.get_current()
diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
index 05f8756655..5bdf08b8ca 100644
--- a/django/core/servers/basehttp.py
+++ b/django/core/servers/basehttp.py
@@ -17,7 +17,7 @@ import urllib
from django.utils.http import http_date
__version__ = "0.1"
-__all__ = ['WSGIServer','WSGIRequestHandler','demo_app']
+__all__ = ['WSGIServer','WSGIRequestHandler']
server_version = "WSGIServer/" + __version__
sys_version = "Python/" + sys.version.split()[0]
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 63a6435bdb..7a4e46a7d7 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -45,7 +45,6 @@ class BaseDatabaseFeatures(object):
autoindexes_primary_keys = True
inline_fk_references = True
needs_datetime_string_cast = True
- needs_upper_for_iops = False
supports_constraints = True
supports_tablespaces = False
uses_case_insensitive_names = False
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index a1a9fb1c73..1df6bac069 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -27,7 +27,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
allows_unique_and_pk = False # Suppress UNIQUE/PK for Oracle (ORA-02259)
empty_fetchmany_value = ()
needs_datetime_string_cast = False
- needs_upper_for_iops = True
supports_tablespaces = True
uses_case_insensitive_names = True
uses_custom_query_class = True
diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py
index f4ada55ac6..219686789f 100644
--- a/django/db/backends/oracle/creation.py
+++ b/django/db/backends/oracle/creation.py
@@ -5,9 +5,13 @@ from django.core import management
# types, as strings. Column-type strings can contain format strings; they'll
# be interpolated against the values of Field.__dict__ before being output.
# If a column type is set to None, it won't be included in the output.
+#
+# Any format strings starting with "qn_" are quoted before being used in the
+# output (the "qn_" prefix is stripped before the lookup is performed.
+
DATA_TYPES = {
'AutoField': 'NUMBER(11)',
- 'BooleanField': 'NUMBER(1) CHECK (%(column)s IN (0,1))',
+ 'BooleanField': 'NUMBER(1) CHECK (%(qn_column)s IN (0,1))',
'CharField': 'NVARCHAR2(%(max_length)s)',
'CommaSeparatedIntegerField': 'VARCHAR2(%(max_length)s)',
'DateField': 'DATE',
@@ -19,11 +23,11 @@ DATA_TYPES = {
'ImageField': 'NVARCHAR2(%(max_length)s)',
'IntegerField': 'NUMBER(11)',
'IPAddressField': 'VARCHAR2(15)',
- 'NullBooleanField': 'NUMBER(1) CHECK ((%(column)s IN (0,1)) OR (%(column)s IS NULL))',
+ 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(column)s IS NULL))',
'OneToOneField': 'NUMBER(11)',
'PhoneNumberField': 'VARCHAR2(20)',
- 'PositiveIntegerField': 'NUMBER(11) CHECK (%(column)s >= 0)',
- 'PositiveSmallIntegerField': 'NUMBER(11) CHECK (%(column)s >= 0)',
+ 'PositiveIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)',
+ 'PositiveSmallIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)',
'SlugField': 'NVARCHAR2(50)',
'SmallIntegerField': 'NUMBER(11)',
'TextField': 'NCLOB',
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 8a51193a01..d906d5bdda 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -16,6 +16,7 @@ from django.core import validators
from django import oldforms
from django import newforms as forms
from django.core.exceptions import ObjectDoesNotExist
+from django.utils.datastructures import DictWrapper
from django.utils.functional import curry
from django.utils.itercompat import tee
from django.utils.text import capfirst
@@ -153,8 +154,9 @@ class Field(object):
# mapped to one of the built-in Django field types. In this case, you
# can implement db_type() instead of get_internal_type() to specify
# exactly which wacky database column type you want to use.
+ data = DictWrapper(self.__dict__, connection.ops.quote_name, "qn_")
try:
- return get_creation_module().DATA_TYPES[self.get_internal_type()] % self.__dict__
+ return get_creation_module().DATA_TYPES[self.get_internal_type()] % data
except KeyError:
return None
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 1c0466f97f..5ae94e02de 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -103,13 +103,15 @@ class RelatedField(object):
if hasattr(sup, 'contribute_to_class'):
sup.contribute_to_class(cls, name)
+
+ if not cls._meta.abstract and self.rel.related_name:
+ self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()}
+
other = self.rel.to
if isinstance(other, basestring):
add_lazy_relation(cls, self, other)
else:
self.do_related_class(other, cls)
- if not cls._meta.abstract and self.rel.related_name:
- self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()}
def set_attributes_from_rel(self):
self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name)
@@ -119,7 +121,8 @@ class RelatedField(object):
def do_related_class(self, other, cls):
self.set_attributes_from_rel()
related = RelatedObject(other, cls, self)
- self.contribute_to_related_class(other, related)
+ if not cls._meta.abstract:
+ self.contribute_to_related_class(other, related)
def get_db_prep_lookup(self, lookup_type, value):
# If we are doing a lookup on a Related Field, we must be
diff --git a/django/db/models/options.py b/django/db/models/options.py
index c78ab1cd48..1ac24fb311 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -273,14 +273,17 @@ class Options(object):
"""
Initialises the field name -> field object mapping.
"""
- cache = dict([(f.name, (f, m, True, False)) for f, m in
- self.get_fields_with_model()])
- for f, model in self.get_m2m_with_model():
- cache[f.name] = (f, model, True, True)
+ cache = {}
+ # We intentionally handle related m2m objects first so that symmetrical
+ # m2m accessor names can be overridden, if necessary.
for f, model in self.get_all_related_m2m_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, True)
for f, model in self.get_all_related_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, False)
+ for f, model in self.get_m2m_with_model():
+ cache[f.name] = (f, model, True, True)
+ for f, model in self.get_fields_with_model():
+ cache[f.name] = (f, model, True, False)
if self.order_with_respect_to:
cache['_order'] = OrderWrt(), None, True, False
if app_cache_ready():
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 8714cffb7f..98caaf004c 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -36,7 +36,7 @@ class CollectedObjects(object):
"""
Adds an item.
model is the class of the object being added,
- pk is the primary key, obj is the object itself,
+ pk is the primary key, obj is the object itself,
parent_model is the model of the parent object
that this object was reached through, nullable should
be True if this relation is nullable.
@@ -77,7 +77,7 @@ class CollectedObjects(object):
def ordered_keys(self):
"""
- Returns the models in the order that they should be
+ Returns the models in the order that they should be
dealth with i.e. models with no dependencies first.
"""
dealt_with = SortedDict()
@@ -92,7 +92,7 @@ class CollectedObjects(object):
found = True
if not found:
raise CyclicDependency("There is a cyclic dependency of items to be processed.")
-
+
return dealt_with.keys()
def unordered_keys(self):
@@ -218,6 +218,8 @@ class QuerySet(object):
def __and__(self, other):
self._merge_sanity_check(other)
+ if isinstance(other, EmptyQuerySet):
+ return other._clone()
combined = self._clone()
combined.query.combine(other.query, sql.AND)
return combined
@@ -225,6 +227,8 @@ class QuerySet(object):
def __or__(self, other):
self._merge_sanity_check(other)
combined = self._clone()
+ if isinstance(other, EmptyQuerySet):
+ return combined
combined.query.combine(other.query, sql.OR)
return combined
@@ -488,7 +492,9 @@ class QuerySet(object):
and usually it will be more natural to use other methods.
"""
if isinstance(filter_obj, Q) or hasattr(filter_obj, 'add_to_query'):
- return self._filter_or_exclude(None, filter_obj)
+ clone = self._clone()
+ clone.query.add_q(filter_obj)
+ return clone
else:
return self._filter_or_exclude(None, **filter_obj)
@@ -583,11 +589,11 @@ class QuerySet(object):
def _merge_sanity_check(self, other):
"""
- Checks that we are merging two comparable queryset classes.
+ Checks that we are merging two comparable queryset classes. By default
+ this does nothing, but see the ValuesQuerySet for an example of where
+ it's useful.
"""
- if self.__class__ is not other.__class__:
- raise TypeError("Cannot merge querysets of different types ('%s' and '%s'."
- % (self.__class__.__name__, other.__class__.__name__))
+ pass
class ValuesQuerySet(QuerySet):
def __init__(self, *args, **kwargs):
@@ -599,7 +605,7 @@ class ValuesQuerySet(QuerySet):
# names of the model fields to select.
def iterator(self):
- if (not self.extra_names and
+ if (not self.extra_names and
len(self.field_names) != len(self.model._meta.fields)):
self.query.trim_extra_select(self.extra_names)
names = self.query.extra_select.keys() + self.field_names
@@ -688,9 +694,9 @@ class DateQuerySet(QuerySet):
"""
self.query = self.query.clone(klass=sql.DateQuery, setup=True)
self.query.select = []
- self.query.add_date_select(self._field.column, self._kind, self._order)
+ self.query.add_date_select(self._field, self._kind, self._order)
if self._field.null:
- self.query.add_filter(('%s__isnull' % self._field.name, True))
+ self.query.add_filter(('%s__isnull' % self._field.name, False))
def _clone(self, klass=None, setup=False, **kwargs):
c = super(DateQuerySet, self)._clone(klass, False, **kwargs)
@@ -705,6 +711,12 @@ class EmptyQuerySet(QuerySet):
super(EmptyQuerySet, self).__init__(model, query)
self._result_cache = []
+ def __and__(self, other):
+ return self._clone()
+
+ def __or__(self, other):
+ return other._clone()
+
def count(self):
return 0
@@ -773,7 +785,7 @@ def delete_objects(seen_objs):
except CyclicDependency:
# if there is a cyclic dependency, we cannot in general delete
# the objects. However, if an appropriate transaction is set
- # up, or if the database is lax enough, it will succeed.
+ # up, or if the database is lax enough, it will succeed.
# So for now, we go ahead and try anway.
ordered_classes = seen_objs.unordered_keys()
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 3044882a86..e8d10bc55b 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -610,6 +610,10 @@ class Query(object):
alias = joins[-1]
col = target.column
+ # Must use left outer joins for nullable fields.
+ for join in joins:
+ self.promote_alias(join)
+
# If we get to this point and the field is a relation to another model,
# append the default ordering for that model.
if field.rel and len(joins) > 1 and opts.ordering:
@@ -631,8 +635,10 @@ class Query(object):
# We have to do the same "final join" optimisation as in
# add_filter, since the final column might not otherwise be part of
# the select set (so we can't order on it).
- join = self.alias_map[alias]
- if col == join[RHS_JOIN_COL]:
+ while 1:
+ join = self.alias_map[alias]
+ if col != join[RHS_JOIN_COL]:
+ break
self.unref_alias(alias)
alias = join[LHS_ALIAS]
col = join[LHS_JOIN_COL]
@@ -679,12 +685,16 @@ class Query(object):
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.
+
+ Returns True if the join was promoted.
"""
if ((unconditional or self.alias_map[alias][NULLABLE]) and
self.alias_map[alias] != self.LOUTER):
data = list(self.alias_map[alias])
data[JOIN_TYPE] = self.LOUTER
self.alias_map[alias] = tuple(data)
+ return True
+ return False
def change_aliases(self, change_map):
"""
@@ -826,6 +836,10 @@ class Query(object):
if not always_create:
for alias in self.join_map.get(t_ident, ()):
if alias not in exclusions:
+ if lhs_table and not self.alias_refcount[self.alias_map[alias][LHS_ALIAS]]:
+ # The LHS of this join tuple is no longer part of the
+ # query, so skip this possibility.
+ continue
self.ref_alias(alias)
if promote:
self.promote_alias(alias)
@@ -985,20 +999,22 @@ class Query(object):
col = target.column
alias = join_list[-1]
- if final > 1:
+ while final > 1:
# An optimization: if the final join is against the same column as
# we are comparing against, we can go back one step in the join
- # chain and compare against the lhs of the join instead. The result
- # (potentially) involves one less table join.
+ # chain and compare against the lhs of the join instead (and then
+ # repeat the optimization). The result, potentially, involves less
+ # table joins.
join = self.alias_map[alias]
- if col == join[RHS_JOIN_COL]:
- self.unref_alias(alias)
- alias = join[LHS_ALIAS]
- col = join[LHS_JOIN_COL]
- join_list = join_list[:-1]
- final -= 1
- if final == penultimate:
- penultimate = last.pop()
+ if col != join[RHS_JOIN_COL]:
+ break
+ self.unref_alias(alias)
+ alias = join[LHS_ALIAS]
+ col = join[LHS_JOIN_COL]
+ join_list = join_list[:-1]
+ final -= 1
+ if final == penultimate:
+ penultimate = last.pop()
if (lookup_type == 'isnull' and value is True and not negate and
final > 1):
@@ -1033,17 +1049,27 @@ class Query(object):
self.promote_alias(table)
self.where.add((alias, col, field, lookup_type, value), connector)
+
if negate:
for alias in join_list:
self.promote_alias(alias)
- if final > 1 and lookup_type != 'isnull':
- for alias in join_list:
- if self.alias_map[alias] == self.LOUTER:
- j_col = self.alias_map[alias][RHS_JOIN_COL]
- entry = Node([(alias, j_col, None, 'isnull', True)])
- entry.negate()
- self.where.add(entry, AND)
- break
+ if lookup_type != 'isnull':
+ if final > 1:
+ for alias in join_list:
+ if self.alias_map[alias][JOIN_TYPE] == self.LOUTER:
+ j_col = self.alias_map[alias][RHS_JOIN_COL]
+ entry = Node([(alias, j_col, None, 'isnull', True)])
+ entry.negate()
+ self.where.add(entry, AND)
+ break
+ elif not (lookup_type == 'in' and not value):
+ # Leaky abstraction artifact: We have to specifically
+ # exclude the "foo__in=[]" case from this handling, because
+ # it's short-circuited in the Where class.
+ entry = Node([(alias, col, field, 'isnull', True)])
+ entry.negate()
+ self.where.add(entry, AND)
+
if can_reuse is not None:
can_reuse.update(join_list)
@@ -1294,10 +1320,12 @@ class Query(object):
final_alias = join[LHS_ALIAS]
col = join[LHS_JOIN_COL]
joins = joins[:-1]
+ promote = False
for join in joins[1:]:
# Only nullable aliases are promoted, so we don't end up
# doing unnecessary left outer joins here.
- self.promote_alias(join)
+ if self.promote_alias(join, promote):
+ promote = True
self.select.append((final_alias, col))
self.select_fields.append(field)
except MultiJoin:
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
index 28436abede..0bb741d706 100644
--- a/django/db/models/sql/subqueries.py
+++ b/django/db/models/sql/subqueries.py
@@ -357,12 +357,14 @@ class DateQuery(Query):
date = typecast_timestamp(str(date))
yield date
- def add_date_select(self, column, lookup_type, order='ASC'):
+ def add_date_select(self, field, lookup_type, order='ASC'):
"""
Converts the query into a date extraction query.
"""
- alias = self.join((None, self.model._meta.db_table, None, None))
- select = Date((alias, column), lookup_type,
+ result = self.setup_joins([field.name], self.get_meta(),
+ self.get_initial_alias(), False)
+ alias = result[3][-1]
+ select = Date((alias, field.column), lookup_type,
self.connection.ops.date_trunc_sql)
self.select = [select]
self.select_fields = [None]
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index cf3b35b4cc..01c43ee86f 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -39,12 +39,11 @@ class CommentNode(Node):
class CycleNode(Node):
def __init__(self, cyclevars, variable_name=None):
- self.cycle_iter = itertools_cycle(cyclevars)
+ self.cycle_iter = itertools_cycle([Variable(v) for v in cyclevars])
self.variable_name = variable_name
def render(self, context):
- value = self.cycle_iter.next()
- value = Variable(value).resolve(context)
+ value = self.cycle_iter.next().resolve(context)
if self.variable_name:
context[self.variable_name] = value
return value
@@ -162,10 +161,12 @@ class IfChangedNode(Node):
self.nodelist = nodelist
self._last_seen = None
self._varlist = map(Variable, varlist)
+ self._id = str(id(self))
def render(self, context):
- if 'forloop' in context and context['forloop']['first']:
+ if 'forloop' in context and self._id not in context['forloop']:
self._last_seen = None
+ context['forloop'][self._id] = 1
try:
if self._varlist:
# Consider multiple parameters. This automatically behaves
@@ -452,17 +453,17 @@ def cycle(parser, token):
<tr class="{% cycle rowcolors %}">...</tr>
<tr class="{% cycle rowcolors %}">...</tr>
- You can use any number of values, seperated by spaces. Commas can also
+ You can use any number of values, separated by spaces. Commas can also
be used to separate values; if a comma is used, the cycle values are
interpreted as literal strings.
"""
# Note: This returns the exact same node on each {% cycle name %} call;
# that is, the node object returned from {% cycle a b c as name %} and the
- # one returned from {% cycle name %} are the exact same object. This
+ # one returned from {% cycle name %} are the exact same object. This
# shouldn't cause problems (heh), but if it does, now you know.
#
- # Ugly hack warning: this stuffs the named template dict into parser so
+ # Ugly hack warning: This stuffs the named template dict into parser so
# that names are only unique within each template (as opposed to using
# a global variable, which would make cycle names have to be unique across
# *all* templates.
@@ -481,8 +482,7 @@ def cycle(parser, token):
# {% cycle foo %} case.
name = args[1]
if not hasattr(parser, '_namedCycleNodes'):
- raise TemplateSyntaxError("No named cycles in template."
- " '%s' is not defined" % name)
+ raise TemplateSyntaxError("No named cycles in template. '%s' is not defined" % name)
if not name in parser._namedCycleNodes:
raise TemplateSyntaxError("Named cycle '%s' does not exist" % name)
return parser._namedCycleNodes[name]
@@ -682,8 +682,10 @@ ifnotequal = register.tag(ifnotequal)
def do_if(parser, token):
"""
The ``{% if %}`` tag evaluates a variable, and if that variable is "true"
- (i.e. exists, is not empty, and is not a false boolean value) the contents
- of the block are output::
+ (i.e., exists, is not empty, and is not a false boolean value), the
+ contents of the block are output:
+
+ ::
{% if athlete_list %}
Number of athletes: {{ athlete_list|count }}
diff --git a/django/templatetags/cache.py b/django/templatetags/cache.py
index 4436b15d6e..07bd590fba 100644
--- a/django/templatetags/cache.py
+++ b/django/templatetags/cache.py
@@ -1,4 +1,4 @@
-from django.template import Library, Node, TemplateSyntaxError
+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_unicode
@@ -6,20 +6,27 @@ from django.utils.encoding import force_unicode
register = Library()
class CacheNode(Node):
- def __init__(self, nodelist, expire_time, fragment_name, vary_on):
+ def __init__(self, nodelist, expire_time_var, fragment_name, vary_on):
self.nodelist = nodelist
- self.expire_time = expire_time
+ self.expire_time_var = Variable(expire_time_var)
self.fragment_name = fragment_name
self.vary_on = vary_on
def render(self, context):
+ try:
+ expire_time = self.expire_time_var.resolve(context)
+ except VariableDoesNotExist:
+ raise TemplateSyntaxError('"cache" tag got an unknkown variable: %r' % self.expire_time_var.var)
+ try:
+ 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.
- cache_key = u':'.join([self.fragment_name] + \
- [force_unicode(resolve_variable(var, context)) for var in self.vary_on])
+ cache_key = u':'.join([self.fragment_name] + [force_unicode(resolve_variable(var, context)) for var in self.vary_on])
value = cache.get(cache_key)
if value is None:
value = self.nodelist.render(context)
- cache.set(cache_key, value, self.expire_time)
+ cache.set(cache_key, value, expire_time)
return value
def do_cache(parser, token):
@@ -48,10 +55,6 @@ def do_cache(parser, token):
tokens = token.contents.split()
if len(tokens) < 3:
raise TemplateSyntaxError(u"'%r' tag requires at least 2 arguments." % tokens[0])
- try:
- expire_time = int(tokens[1])
- except ValueError:
- raise TemplateSyntaxError(u"First argument to '%r' must be an integer (got '%s')." % (tokens[0], tokens[1]))
- return CacheNode(nodelist, expire_time, tokens[2], tokens[3:])
+ return CacheNode(nodelist, tokens[1], tokens[2], tokens[3:])
register.tag('cache', do_cache)
diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py
index 4c278c0d8e..21a72f2d1e 100644
--- a/django/utils/datastructures.py
+++ b/django/utils/datastructures.py
@@ -343,3 +343,34 @@ class FileDict(dict):
d = dict(self, content='<omitted>')
return dict.__repr__(d)
return dict.__repr__(self)
+
+class DictWrapper(dict):
+ """
+ Wraps accesses to a dictionary so that certain values (those starting with
+ the specified prefix) are passed through a function before being returned.
+ The prefix is removed before looking up the real value.
+
+ Used by the SQL construction code to ensure that values are correctly
+ quoted before being used.
+ """
+ def __init__(self, data, func, prefix):
+ super(DictWrapper, self).__init__(data)
+ self.func = func
+ self.prefix = prefix
+
+ def __getitem__(self, key):
+ """
+ Retrieves the real value after stripping the prefix string (if
+ present). If the prefix is present, pass the value through self.func
+ before returning, otherwise return the raw value.
+ """
+ if key.startswith(self.prefix):
+ use_func = True
+ key = key[len(self.prefix):]
+ else:
+ use_func = False
+ value = super(DictWrapper, self).__getitem__(key)
+ if use_func:
+ return self.func(value)
+ return value
+
diff --git a/django/utils/html.py b/django/utils/html.py
index 07e4f0d3f4..747af52879 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -76,20 +76,20 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
"""
Converts any URLs in text into clickable links.
- Works on http://, https://, and www. links. Links can have trailing
- punctuation (periods, commas, close-parens) and leading punctuation
- (opening parens) and it'll still do the right thing.
+ Works on http://, https://, www. links and links ending in .org, .net or
+ .com. Links can have trailing punctuation (periods, commas, close-parens)
+ and leading punctuation (opening parens) and it'll still do the right
+ thing.
If trim_url_limit is not None, the URLs in link text longer than this limit
will truncated to trim_url_limit-3 characters and appended with an elipsis.
If nofollow is True, the URLs in link text will get a rel="nofollow"
attribute.
+
+ If autoescape is True, the link text and URLs will get autoescaped.
"""
- if autoescape:
- trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x)
- else:
- trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+ trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_unicode(text))
nofollow_attr = nofollow and ' rel="nofollow"' or ''
@@ -97,30 +97,30 @@ def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
match = punctuation_re.match(word)
if match:
lead, middle, trail = match.groups()
- if safe_input:
- middle = mark_safe(middle)
- if middle.startswith('www.') or ('@' not in middle and not (middle.startswith('http://') or middle.startswith('https://')) and \
- len(middle) > 0 and middle[0] in string.ascii_letters + string.digits and \
- (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
- middle = 'http://%s' % middle
+ # Make URL we want to point to.
+ url = None
if middle.startswith('http://') or middle.startswith('https://'):
url = urlquote(middle, safe='/&=:;#?+*')
- if autoescape and not safe_input:
- url = escape(url)
- trimmed_url = trim_url(middle)
- middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr,
- trimmed_url)
- elif '@' in middle and not middle.startswith('www.') and \
- not ':' in middle and simple_email_re.match(middle):
- if autoescape:
- middle = conditional_escape(middle)
- middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
- if lead + middle + trail != word:
+ elif middle.startswith('www.') or ('@' not in middle and \
+ len(middle) > 0 and middle[0] in string.ascii_letters + string.digits and \
+ (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
+ url = urlquote('http://%s' % middle, safe='/&=:;#?+*')
+ elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
+ url = 'mailto:%s' % middle
+ nofollow_attr = ''
+ # Make link.
+ if url:
+ trimmed = trim_url(middle)
if autoescape and not safe_input:
lead, trail = escape(lead), escape(trail)
+ url, trimmed = escape(url), escape(trimmed)
+ middle = '<a href="%s"%s>%s</a>' % (url, nofollow_attr, trimmed)
words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
- elif autoescape and not safe_input:
- words[i] = escape(word)
+ else:
+ if safe_input:
+ words[i] = mark_safe(word)
+ elif autoescape:
+ words[i] = escape(word)
elif safe_input:
words[i] = mark_safe(word)
elif autoescape:
diff --git a/docs/cache.txt b/docs/cache.txt
index e7e1cdd791..3318b2ad4a 100644
--- a/docs/cache.txt
+++ b/docs/cache.txt
@@ -336,6 +336,17 @@ template tag to uniquely identify the cache fragment::
It's perfectly fine to specify more than one argument to identify the fragment.
Simply pass as many arguments to ``{% cache %}`` as you need.
+The cache timeout can be a template variable, as long as the template variable
+resolves to an integer value. For example, if the template variable
+``my_timeout`` is set to the value ``600``, then the following two examples are
+equivalent::
+
+ {% cache 600 sidebar %} ... {% endcache %}
+ {% cache my_timeout sidebar %} ... {% endcache %}
+
+This feature is useful in avoiding repetition in templates. You can set the
+timeout in a variable, in one place, and just reuse that value.
+
The low-level cache API
=======================
diff --git a/docs/contributing.txt b/docs/contributing.txt
index b73e3601bc..686c440c96 100644
--- a/docs/contributing.txt
+++ b/docs/contributing.txt
@@ -238,14 +238,14 @@ Since a picture is worth a thousand words, let's start there:
We've got two official roles here:
- * Core developers: people with commit access who make the big decisions
+ * Core developers: people with commit access who make the big decisions
and write the bulk of the code.
* Ticket triagers: trusted community members with a proven history of
working with the Django community. As a result of this history, they
have been entrusted by the core developers to make some of the smaller
decisions about tickets.
-
+
Second, note the five triage stages:
1. A ticket starts as "Unreviewed", meaning that nobody has examined
@@ -254,7 +254,7 @@ Second, note the five triage stages:
2. "Design decision needed" means "this concept requires a design
decision," which should be discussed either in the ticket comments or on
`django-developers`_. The "Design decision needed" step will generally
- only be used for feature requests. It can also be used for issues
+ only be used for feature requests. It can also be used for issues
that *might* be bugs, depending on opinion or interpretation. Obvious
bugs (such as crashes, incorrect query results, or non-compliance with a
standard) skip this step and move straight to "Accepted".
@@ -317,7 +317,7 @@ A ticket can be resolved in a number of ways:
tickets, we keep all the discussion in one place, which helps everyone.
"worksforme"
- Used when the the ticket doesn't contain enough detail to replicate
+ Used when the the ticket doesn't contain enough detail to replicate
the original bug.
If you believe that the ticket was closed in error -- because you're
@@ -332,50 +332,49 @@ reopen tickets that have been marked as "wontfix" by core developers.
Triage by the general community
-------------------------------
-Although the Core Developers and Ticket Triagers make the big decisions in
-the ticket triage process, there is also a lot that general community
+Although the core developers and ticket triagers make the big decisions in
+the ticket triage process, there's also a lot that general community
members can do to help the triage process. In particular, you can help out by:
- * Closing "Unreviewed" tickets as "invalid", "worksforme", or "duplicate".
+ * Closing "Unreviewed" tickets as "invalid", "worksforme" or "duplicate."
- * Promoting "Unreviewed" tickets to "Design Decision Required" if there
- is a design decision that needs to be made, or "Accepted" if they are
- an obvious bug.
+ * Promoting "Unreviewed" tickets to "Design decision needed" if a design
+ decision needs to be made, or "Accepted" in case of obvious bugs.
- * Correcting the "Needs Tests", "Needs documentation", or "Has Patch" flags
+ * Correcting the "Needs tests", "Needs documentation", or "Has patch" flags
for tickets where they are incorrectly set.
-
+
* Checking that old tickets are still valid. If a ticket hasn't seen
any activity in a long time, it's possible that the problem has been
- fixed, but the ticket hasn't been closed.
+ fixed but the ticket hasn't yet been closed.
- * Contact the owners of tickets that have been claimed, but have not seen
- any recent activity. If the owner doesn't respond after a week or so,
+ * Contacting the owners of tickets that have been claimed but have not seen
+ any recent activity. If the owner doesn't respond after a week or so,
remove the owner's claim on the ticket.
* Identifying trends and themes in the tickets. If there a lot of bug reports
- about a particular part of Django, it possibly indicates that we need
- to consider refactoring that part of the code. If a trend is emerging,
- you should raise it for discussion (referencing the relevant tickets)
- on `django-developers`_.
+ about a particular part of Django, it may indicate we should consider
+ refactoring that part of the code. If a trend is emerging, you should
+ raise it for discussion (referencing the relevant tickets) on
+ `django-developers`_.
-However, we do ask that as a general community member working in the
-ticket database:
+However, we do ask the following of all general community members working in
+the ticket database:
- * Please **don't** close tickets as "wontfix". The core developers will
- make the final determination of the fate of a ticket, usually after
+ * Please **don't** close tickets as "wontfix." The core developers will
+ make the final determination of the fate of a ticket, usually after
consultation with the community.
-
+
* Please **don't** promote tickets to "Ready for checkin" unless they are
- *trivial* changes - for example, spelling mistakes or
- broken links in documentation.
+ *trivial* changes -- for example, spelling mistakes or broken links in
+ documentation.
- * Please **don't** reverse a decision that has been made by a core
- developer. If you disagree with a discussion that has been made,
+ * Please **don't** reverse a decision that has been made by a core
+ developer. If you disagree with a discussion that has been made,
please post a message to `django-developers`_.
* Please be conservative in your actions. If you're unsure if you should
- be making a change, don't make the change - leave a comment with your
+ be making a change, don't make the change -- leave a comment with your
concerns on the ticket, or post a message to `django-developers`_.
Submitting and maintaining translations
@@ -739,8 +738,8 @@ If you're using another backend:
deleted when the tests are finished. This means your user account needs
permission to execute ``CREATE DATABASE``.
-If you want to run the full suite of tests, there are a number of dependencies that
-you should install:
+If you want to run the full suite of tests, you'll need to install a number of
+dependencies:
* PyYAML_
* Markdown_
@@ -748,10 +747,8 @@ you should install:
* Docutils_
* setuptools_
-Of these dependencies, setuptools_ is the only dependency that is required - if
-setuptools_ is not installed, you will get import errors when running one of
-the template tests. The tests using the other libraries will be skipped if the
-dependency can't be found.
+Each of these dependencies is optional. If you're missing any of them, the
+associated tests will be skipped.
.. _PyYAML: http://pyyaml.org/wiki/PyYAML
.. _Markdown: http://pypi.python.org/pypi/Markdown/1.7
@@ -773,13 +770,13 @@ for generic relations and internationalization, type::
Contrib apps
------------
-Tests for apps in ``django/contrib/`` go in their respective directories,
-in a ``tests.py`` file. (You can split the tests over multiple modules
-by using a ``tests`` folder in the normal Python way).
+Tests for apps in ``django/contrib/`` go in their respective directories under
+``django/contrib/``, in a ``tests.py`` file. (You can split the tests over
+multiple modules by using a ``tests`` directory in the normal Python way.)
For the tests to be found, a ``models.py`` file must exist (it doesn't
-have to have anything in it). If you have URLs that need to be
-mapped, you must add them in ``tests/urls.py``.
+have to have anything in it). If you have URLs that need to be
+mapped, put them in ``tests/urls.py``.
To run tests for just one contrib app (e.g. ``markup``), use the same
method as above::
diff --git a/docs/db-api.txt b/docs/db-api.txt
index 3202bd4f9f..f80d63797a 100644
--- a/docs/db-api.txt
+++ b/docs/db-api.txt
@@ -382,7 +382,7 @@ Pickling QuerySets
If you pickle_ a ``QuerySet``, this will also force all the results to be
loaded into memory prior to pickling. This is because pickling is usually used
-as a precursor to caching and when the cached queryset is reloaded, you want
+as a precursor to caching and when the cached ``QuerySet`` is reloaded, you want
the results to already be present. This means that when you unpickle a
``QuerySet``, it contains the results at the moment it was pickled, rather
than the results that are currently in the database.
@@ -2040,7 +2040,7 @@ automatically saved to the database.
One-to-one relationships
------------------------
-One-to-one relationships are very similar to Many-to-one relationships.
+One-to-one relationships are very similar to many-to-one relationships.
If you define a OneToOneField on your model, instances of that model will have
access to the related object via a simple attribute of the model.
@@ -2053,8 +2053,8 @@ For example::
ed = EntryDetail.objects.get(id=2)
ed.entry # Returns the related Entry object.
-The difference comes in reverse queries. The related model in a One-to-one
-relationship also has access to a ``Manager`` object; however, that ``Manager``
+The difference comes in "reverse" queries. The related model in a one-to-one
+relationship also has access to a ``Manager`` object, but that ``Manager``
represents a single object, rather than a collection of objects::
e = Entry.objects.get(id=2)
diff --git a/docs/model-api.txt b/docs/model-api.txt
index 6dc2d0835a..02a9fd2d95 100644
--- a/docs/model-api.txt
+++ b/docs/model-api.txt
@@ -1544,7 +1544,7 @@ the ``url`` function)::
'django.views.generic.list_detail.object_detail',
name='people_view'),
-and then using that name to perform the reverse URL resolution instead
+...and then using that name to perform the reverse URL resolution instead
of the view name::
from django.db.models import permalink
@@ -1553,7 +1553,7 @@ of the view name::
return ('people_view', [str(self.id)])
get_absolute_url = permalink(get_absolute_url)
-More details on named URL patterns can be found in `URL dispatch documentation`_.
+More details on named URL patterns are in the `URL dispatch documentation`_.
.. _URL dispatch documentation: ../url_dispatch/#naming-url-patterns
diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py
index 7c737b6bd1..9842cb166b 100644
--- a/tests/modeltests/model_inheritance/models.py
+++ b/tests/modeltests/model_inheritance/models.py
@@ -39,6 +39,29 @@ class Student(CommonInfo):
pass
#
+# Abstract base classes with related models
+#
+
+class Post(models.Model):
+ title = models.CharField(max_length=50)
+
+class Attachment(models.Model):
+ post = models.ForeignKey(Post, related_name='attached_%(class)s_set')
+ content = models.TextField()
+
+ class Meta:
+ abstract = True
+
+ def __unicode__(self):
+ return self.content
+
+class Comment(Attachment):
+ is_spam = models.BooleanField()
+
+class Link(Attachment):
+ url = models.URLField()
+
+#
# Multi-table inheritance
#
@@ -128,9 +151,25 @@ Traceback (most recent call last):
...
AttributeError: type object 'CommonInfo' has no attribute 'objects'
-# The Place/Restaurant/ItalianRestaurant models, on the other hand, all exist
-# as independent models. However, the subclasses also have transparent access
-# to the fields of their ancestors.
+# Create a Post
+>>> post = Post(title='Lorem Ipsum')
+>>> post.save()
+
+# The Post model has distinct accessors for the Comment and Link models.
+>>> post.attached_comment_set.create(content='Save $ on V1agr@', is_spam=True)
+<Comment: Save $ on V1agr@>
+>>> post.attached_link_set.create(content='The Web framework for perfectionists with deadlines.', url='http://www.djangoproject.com/')
+<Link: The Web framework for perfectionists with deadlines.>
+
+# The Post model doesn't have an attribute called 'attached_%(class)s_set'.
+>>> getattr(post, 'attached_%(class)s_set')
+Traceback (most recent call last):
+ ...
+AttributeError: 'Post' object has no attribute 'attached_%(class)s_set'
+
+# The Place/Restaurant/ItalianRestaurant models all exist as independent
+# models. However, the subclasses also have transparent access to the fields of
+# their ancestors.
# Create a couple of Places.
>>> p1 = Place(name='Master Shakes', address='666 W. Jersey')
diff --git a/tests/regressiontests/datastructures/tests.py b/tests/regressiontests/datastructures/tests.py
index b51b4b1233..d6141b09ce 100644
--- a/tests/regressiontests/datastructures/tests.py
+++ b/tests/regressiontests/datastructures/tests.py
@@ -125,4 +125,12 @@ Init from sequence of tuples
>>> d = FileDict({'other-key': 'once upon a time...'})
>>> repr(d)
"{'other-key': 'once upon a time...'}"
+
+### DictWrapper #############################################################
+
+>>> f = lambda x: "*%s" % x
+>>> d = DictWrapper({'a': 'a'}, f, 'xx_')
+>>> "Normal: %(a)s. Modified: %(xx_a)s" % d
+'Normal: a. Modified: *a'
+
"""
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index 62403935d4..b56c33a652 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -150,7 +150,7 @@ u'fran%C3%A7ois%20%26%20jill'
u'<a href="http://short.com/" rel="nofollow">http://short.com/</a>'
>>> urlizetrunc(u'http://www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&meta=', 20)
-u'<a href="http://www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&meta=" rel="nofollow">http://www.google....</a>'
+u'<a href="http://www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&meta=" rel="nofollow">http://www.google...</a>'
>>> urlizetrunc('http://www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&meta=', 20)
u'<a href="http://www.google.co.uk/search?hl=en&q=some+long+url&btnG=Search&meta=" rel="nofollow">http://www.google...</a>'
@@ -174,10 +174,10 @@ u'<a href="http://google.com" rel="nofollow">http://google.com</a>'
u'<a href="http://google.com/" rel="nofollow">http://google.com/</a>'
>>> urlize('www.google.com')
-u'<a href="http://www.google.com" rel="nofollow">http://www.google.com</a>'
+u'<a href="http://www.google.com" rel="nofollow">www.google.com</a>'
>>> urlize('djangoproject.org')
-u'<a href="http://djangoproject.org" rel="nofollow">http://djangoproject.org</a>'
+u'<a href="http://djangoproject.org" rel="nofollow">djangoproject.org</a>'
>>> urlize('info@djangoproject.org')
u'<a href="mailto:info@djangoproject.org">info@djangoproject.org</a>'
diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py
index 8801715a0c..33e2e0e4f6 100644
--- a/tests/regressiontests/model_inheritance_regress/models.py
+++ b/tests/regressiontests/model_inheritance_regress/models.py
@@ -2,6 +2,8 @@
Regression tests for Model inheritance behaviour.
"""
+import datetime
+
from django.db import models
class Place(models.Model):
@@ -10,7 +12,7 @@ class Place(models.Model):
class Meta:
ordering = ('name',)
-
+
def __unicode__(self):
return u"%s the place" % self.name
@@ -35,11 +37,17 @@ class ParkingLot(Place):
def __unicode__(self):
return u"%s the parking lot" % self.name
+class Parent(models.Model):
+ created = models.DateTimeField(default=datetime.datetime.now)
+
+class Child(Parent):
+ name = models.CharField(max_length=10)
+
__test__ = {'API_TESTS':"""
# Regression for #7350, #7202
-# Check that when you create a Parent object with a specific reference to an existent
-# child instance, saving the Parent doesn't duplicate the child.
-# This behaviour is only activated during a raw save - it is mostly relevant to
+# Check that when you create a Parent object with a specific reference to an
+# existent child instance, saving the Parent doesn't duplicate the child. This
+# behaviour is only activated during a raw save - it is mostly relevant to
# deserialization, but any sort of CORBA style 'narrow()' API would require a
# similar approach.
@@ -117,4 +125,10 @@ __test__ = {'API_TESTS':"""
>>> [sorted(d.items()) for d in dicts]
[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
+# Regressions tests for #7105: dates() queries should be able to use fields
+# from the parent model as easily as the child.
+>>> obj = Child.objects.create(name='child', created=datetime.datetime(2008, 6, 26, 17, 0, 0))
+>>> Child.objects.dates('created', 'month')
+[datetime.datetime(2008, 6, 1, 0, 0)]
+
"""}
diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py
index aa78d6583a..c02ad73998 100644
--- a/tests/regressiontests/queries/models.py
+++ b/tests/regressiontests/queries/models.py
@@ -45,6 +45,7 @@ class Author(models.Model):
class Item(models.Model):
name = models.CharField(max_length=10)
created = models.DateTimeField()
+ modified = models.DateTimeField(blank=True, null=True)
tags = models.ManyToManyField(Tag, blank=True, null=True)
creator = models.ForeignKey(Author)
note = models.ForeignKey(Note)
@@ -57,7 +58,7 @@ class Item(models.Model):
class Report(models.Model):
name = models.CharField(max_length=10)
- creator = models.ForeignKey(Author, to_field='num')
+ creator = models.ForeignKey(Author, to_field='num', null=True)
def __unicode__(self):
return self.name
@@ -89,6 +90,15 @@ class Number(models.Model):
def __unicode__(self):
return unicode(self.num)
+# Symmetrical m2m field with a normal field using the reverse accesor name
+# ("valid").
+class Valid(models.Model):
+ valid = models.CharField(max_length=10)
+ parent = models.ManyToManyField('self')
+
+ class Meta:
+ ordering = ['valid']
+
# Some funky cross-linked models for testing a couple of infinite recursion
# cases.
class X(models.Model):
@@ -121,12 +131,12 @@ class LoopZ(models.Model):
class CustomManager(models.Manager):
def get_query_set(self):
qs = super(CustomManager, self).get_query_set()
- return qs.filter(is_public=True, tag__name='t1')
+ return qs.filter(public=True, tag__name='t1')
class ManagedModel(models.Model):
data = models.CharField(max_length=10)
tag = models.ForeignKey(Tag)
- is_public = models.BooleanField(default=True)
+ public = models.BooleanField(default=True)
objects = CustomManager()
normal_manager = models.Manager()
@@ -134,6 +144,24 @@ class ManagedModel(models.Model):
def __unicode__(self):
return self.data
+# An inter-related setup with multiple paths from Child to Detail.
+class Detail(models.Model):
+ data = models.CharField(max_length=10)
+
+class MemberManager(models.Manager):
+ def get_query_set(self):
+ return super(MemberManager, self).get_query_set().select_related("details")
+
+class Member(models.Model):
+ name = models.CharField(max_length=10)
+ details = models.OneToOneField(Detail, primary_key=True)
+
+ objects = MemberManager()
+
+class Child(models.Model):
+ person = models.OneToOneField(Member, primary_key=True)
+ parent = models.ForeignKey(Member, related_name="children")
+
__test__ = {'API_TESTS':"""
>>> t1 = Tag(name='t1')
@@ -174,7 +202,7 @@ by 'info'. Helps detect some problems later.
>>> time2 = datetime.datetime(2007, 12, 19, 21, 0, 0)
>>> time3 = datetime.datetime(2007, 12, 20, 22, 25, 0)
>>> time4 = datetime.datetime(2007, 12, 20, 21, 0, 0)
->>> i1 = Item(name='one', created=time1, creator=a1, note=n3)
+>>> i1 = Item(name='one', created=time1, modified=time1, creator=a1, note=n3)
>>> i1.save()
>>> i1.tags = [t1, t2]
>>> i2 = Item(name='two', created=time2, creator=a2, note=n2)
@@ -190,6 +218,8 @@ by 'info'. Helps detect some problems later.
>>> r1.save()
>>> r2 = Report(name='r2', creator=a3)
>>> r2.save()
+>>> r3 = Report(name='r3')
+>>> r3.save()
Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering
will be rank3, rank2, rank1.
@@ -478,7 +508,7 @@ FieldError: Infinite loop caused by ordering.
# Ordering by a many-valued attribute (e.g. a many-to-many or reverse
# ForeignKey) is legal, but the results might not make sense. That isn't
# Django's problem. Garbage in, garbage out.
->>> Item.objects.all().order_by('tags', 'id')
+>>> Item.objects.filter(tags__isnull=False).order_by('tags', 'id')
[<Item: one>, <Item: two>, <Item: one>, <Item: two>, <Item: four>]
# If we replace the default ordering, Django adjusts the required tables
@@ -627,6 +657,10 @@ Bug #7087 -- dates with extra select columns
>>> Item.objects.dates('created', 'day').extra(select={'a': 1})
[datetime.datetime(2007, 12, 19, 0, 0), datetime.datetime(2007, 12, 20, 0, 0)]
+Bug #7155 -- nullable dates
+>>> Item.objects.dates('modified', 'day')
+[datetime.datetime(2007, 12, 19, 0, 0)]
+
Test that parallel iterators work.
>>> qs = Tag.objects.all()
@@ -705,8 +739,57 @@ More twisted cases, involving nested negations.
Bug #7095
Updates that are filtered on the model being updated are somewhat tricky to get
in MySQL. This exercises that case.
->>> mm = ManagedModel.objects.create(data='mm1', tag=t1, is_public=True)
+>>> mm = ManagedModel.objects.create(data='mm1', tag=t1, public=True)
>>> ManagedModel.objects.update(data='mm')
+A values() or values_list() query across joined models must use outer joins
+appropriately.
+>>> Report.objects.values_list("creator__extra__info", flat=True).order_by("name")
+[u'e1', u'e2', None]
+
+Similarly for select_related(), joins beyond an initial nullable join must
+use outer joins so that all results are included.
+>>> Report.objects.select_related("creator", "creator__extra").order_by("name")
+[<Report: r1>, <Report: r2>, <Report: r3>]
+
+When there are multiple paths to a table from another table, we have to be
+careful not to accidentally reuse an inappropriate join when using
+select_related(). We used to return the parent's Detail record here by mistake.
+
+>>> d1 = Detail.objects.create(data="d1")
+>>> d2 = Detail.objects.create(data="d2")
+>>> m1 = Member.objects.create(name="m1", details=d1)
+>>> m2 = Member.objects.create(name="m2", details=d2)
+>>> c1 = Child.objects.create(person=m2, parent=m1)
+>>> obj = m1.children.select_related("person__details")[0]
+>>> obj.person.details.data
+u'd2'
+
+Bug #7076 -- excluding shouldn't eliminate NULL entries.
+>>> Item.objects.exclude(modified=time1).order_by('name')
+[<Item: four>, <Item: three>, <Item: two>]
+>>> Tag.objects.exclude(parent__name=t1.name)
+[<Tag: t1>, <Tag: t4>, <Tag: t5>]
+
+Bug #7181 -- ordering by related tables should accomodate nullable fields (this
+test is a little tricky, since NULL ordering is database dependent. Instead, we
+just count the number of results).
+>>> len(Tag.objects.order_by('parent__name'))
+5
+
+Bug #7107 -- this shouldn't create an infinite loop.
+>>> Valid.objects.all()
+[]
+
+Empty querysets can be merged with others.
+>>> Note.objects.none() | Note.objects.all()
+[<Note: n1>, <Note: n2>, <Note: n3>]
+>>> Note.objects.all() | Note.objects.none()
+[<Note: n1>, <Note: n2>, <Note: n3>]
+>>> Note.objects.none() & Note.objects.all()
+[]
+>>> Note.objects.all() & Note.objects.none()
+[]
+
"""}
diff --git a/tests/regressiontests/templates/loaders.py b/tests/regressiontests/templates/loaders.py
index 82e3c622d1..db37116b94 100644
--- a/tests/regressiontests/templates/loaders.py
+++ b/tests/regressiontests/templates/loaders.py
@@ -1,6 +1,7 @@
-# -*- coding: utf-8 -*-
"""
Test cases for the template loaders
+
+Note: This test requires setuptools!
"""
from django.conf import settings
@@ -17,7 +18,7 @@ import StringIO
from django.template import TemplateDoesNotExist
from django.template.loaders.eggs import load_template_source as lts_egg
-#Mock classes and objects for pkg_resources functions
+# Mock classes and objects for pkg_resources functions.
class MockProvider(pkg_resources.NullProvider):
def __init__(self, module):
pkg_resources.NullProvider.__init__(self, module)
@@ -35,25 +36,25 @@ class MockProvider(pkg_resources.NullProvider):
def _get(self, path):
return self.module._resources[path].read()
-class MockLoader(object): pass
+class MockLoader(object):
+ pass
def create_egg(name, resources):
"""
- Creates a mock egg with a list of resources
-
- name: The name of the module
- resources: A dictionary of resources. Keys are the names and values the the data.
+ Creates a mock egg with a list of resources.
+
+ name: The name of the module.
+ resources: A dictionary of resources. Keys are the names and values the the data.
"""
egg = imp.new_module(name)
egg.__loader__ = MockLoader()
egg._resources = resources
sys.modules[name] = egg
-
class EggLoader(unittest.TestCase):
def setUp(self):
pkg_resources._provider_factories[MockLoader] = MockProvider
-
+
self.empty_egg = create_egg("egg_empty", {})
self.egg_1 = create_egg("egg_1", {
'templates/y.html' : StringIO.StringIO("y"),
@@ -61,7 +62,7 @@ class EggLoader(unittest.TestCase):
})
self._old_installed_apps = settings.INSTALLED_APPS
settings.INSTALLED_APPS = []
-
+
def tearDown(self):
settings.INSTALLED_APPS = self._old_installed_apps
@@ -74,19 +75,18 @@ class EggLoader(unittest.TestCase):
"Template loading fails if the template is not in the egg"
settings.INSTALLED_APPS = ['egg_1']
self.assertRaises(TemplateDoesNotExist, lts_egg, "not-existing.html")
-
+
def test_existing(self):
"A template can be loaded from an egg"
settings.INSTALLED_APPS = ['egg_1']
contents, template_name = lts_egg("y.html")
self.assertEqual(contents, "y")
self.assertEqual(template_name, "egg:egg_1:templates/y.html")
-
+
def test_not_installed(self):
"Loading an existent template from an egg not included in INSTALLED_APPS should fail"
settings.INSTALLED_APPS = []
self.assertRaises(TemplateDoesNotExist, lts_egg, "y.html")
-
if __name__ == "__main__":
unittest.main()
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 089a6d312d..186b8aacb5 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -20,7 +20,10 @@ from django.utils.tzinfo import LocalTimezone
from unicode import unicode_tests
from context import context_tests
-from loaders import *
+try:
+ from loaders import *
+except ImportError:
+ pass # If setuptools isn't installed, that's fine. Just move on.
import filters
@@ -132,8 +135,7 @@ class Templates(unittest.TestCase):
# Quickly check that we aren't accidentally using a name in both
# template and filter tests.
- overlapping_names = [name for name in filter_tests if name in
- template_tests]
+ overlapping_names = [name for name in filter_tests if name in template_tests]
assert not overlapping_names, 'Duplicate test name(s): %s' % ', '.join(overlapping_names)
template_tests.update(filter_tests)
@@ -156,7 +158,7 @@ class Templates(unittest.TestCase):
# Turn TEMPLATE_DEBUG off, because tests assume that.
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
- # Set TEMPLATE_STRING_IF_INVALID to a known string
+ # Set TEMPLATE_STRING_IF_INVALID to a known string.
old_invalid = settings.TEMPLATE_STRING_IF_INVALID
expected_invalid_str = 'INVALID'
@@ -539,13 +541,14 @@ class Templates(unittest.TestCase):
'if-tag-error05': ("{% if not foo or %}yes{% else %}no{% endif %}", {'foo': True}, template.TemplateSyntaxError),
### IFCHANGED TAG #########################################################
- 'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,2,3) }, '123'),
- 'ifchanged02': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,3) }, '13'),
- 'ifchanged03': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,1) }, '1'),
- 'ifchanged04': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 2, 3), 'numx': (2, 2, 2)}, '122232'),
- 'ifchanged05': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (1, 2, 3)}, '1123123123'),
- 'ifchanged06': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2)}, '1222'),
- 'ifchanged07': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% for y in numy %}{% ifchanged %}{{ y }}{% endifchanged %}{% endfor %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2), 'numy': (3, 3, 3)}, '1233323332333'),
+ 'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,2,3)}, '123'),
+ 'ifchanged02': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,1,3)}, '13'),
+ 'ifchanged03': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', {'num': (1,1,1)}, '1'),
+ 'ifchanged04': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', {'num': (1, 2, 3), 'numx': (2, 2, 2)}, '122232'),
+ 'ifchanged05': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', {'num': (1, 1, 1), 'numx': (1, 2, 3)}, '1123123123'),
+ 'ifchanged06': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', {'num': (1, 1, 1), 'numx': (2, 2, 2)}, '1222'),
+ 'ifchanged07': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% for y in numy %}{% ifchanged %}{{ y }}{% endifchanged %}{% endfor %}{% endfor %}{% endfor %}', {'num': (1, 1, 1), 'numx': (2, 2, 2), 'numy': (3, 3, 3)}, '1233323332333'),
+ 'ifchanged08': ('{% for data in datalist %}{% for c,d in data %}{% if c %}{% ifchanged %}{{ d }}{% endifchanged %}{% endif %}{% endfor %}{% endfor %}', {'datalist': [[(1, 'a'), (1, 'a'), (0, 'b'), (1, 'c')], [(0, 'a'), (1, 'c'), (1, 'd'), (1, 'd'), (0, 'e')]]}, 'accd'),
# Test one parameter given to ifchanged.
'ifchanged-param01': ('{% for n in num %}{% ifchanged n %}..{% endifchanged %}{{ n }}{% endfor %}', { 'num': (1,2,3) }, '..1..2..3'),
@@ -860,40 +863,46 @@ class Templates(unittest.TestCase):
### NOW TAG ########################################################
# Simple case
- 'now01' : ('{% now "j n Y"%}', {}, str(datetime.now().day) + ' ' + str(datetime.now().month) + ' ' + str(datetime.now().year)),
+ 'now01': ('{% now "j n Y"%}', {}, str(datetime.now().day) + ' ' + str(datetime.now().month) + ' ' + str(datetime.now().year)),
# Check parsing of escaped and special characters
- 'now02' : ('{% now "j "n" Y"%}', {}, template.TemplateSyntaxError),
- # 'now03' : ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)),
- # 'now04' : ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year))
+ 'now02': ('{% now "j "n" Y"%}', {}, template.TemplateSyntaxError),
+ # 'now03': ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)),
+ # 'now04': ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year))
### URL TAG ########################################################
# Successes
- 'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
- 'url02' : ('{% url regressiontests.templates.views.client_action client.id, action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
- 'url03' : ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'),
- 'url04' : ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'),
- 'url05' : (u'{% url метка_оператора v %}', {'v': u'Ω'},
- '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
+ 'url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
+ 'url02': ('{% url regressiontests.templates.views.client_action client.id, action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
+ 'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'),
+ 'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'),
+ 'url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
# Failures
- 'url-fail01' : ('{% url %}', {}, template.TemplateSyntaxError),
- 'url-fail02' : ('{% url no_such_view %}', {}, ''),
- 'url-fail03' : ('{% url regressiontests.templates.views.client no_such_param="value" %}', {}, ''),
+ 'url-fail01': ('{% url %}', {}, template.TemplateSyntaxError),
+ 'url-fail02': ('{% url no_such_view %}', {}, ''),
+ 'url-fail03': ('{% url regressiontests.templates.views.client no_such_param="value" %}', {}, ''),
### CACHE TAG ######################################################
- 'cache01' : ('{% load cache %}{% cache -1 test %}cache01{% endcache %}', {}, 'cache01'),
- 'cache02' : ('{% load cache %}{% cache -1 test %}cache02{% endcache %}', {}, 'cache02'),
- 'cache03' : ('{% load cache %}{% cache 2 test %}cache03{% endcache %}', {}, 'cache03'),
- 'cache04' : ('{% load cache %}{% cache 2 test %}cache04{% endcache %}', {}, 'cache03'),
- 'cache05' : ('{% load cache %}{% cache 2 test foo %}cache05{% endcache %}', {'foo': 1}, 'cache05'),
- 'cache06' : ('{% load cache %}{% cache 2 test foo %}cache06{% endcache %}', {'foo': 2}, 'cache06'),
- 'cache07' : ('{% load cache %}{% cache 2 test foo %}cache06{% endcache %}', {'foo': 1}, 'cache05'),
+ 'cache01': ('{% load cache %}{% cache -1 test %}cache01{% endcache %}', {}, 'cache01'),
+ 'cache02': ('{% load cache %}{% cache -1 test %}cache02{% endcache %}', {}, 'cache02'),
+ 'cache03': ('{% load cache %}{% cache 2 test %}cache03{% endcache %}', {}, 'cache03'),
+ 'cache04': ('{% load cache %}{% cache 2 test %}cache04{% endcache %}', {}, 'cache03'),
+ 'cache05': ('{% load cache %}{% cache 2 test foo %}cache05{% endcache %}', {'foo': 1}, 'cache05'),
+ 'cache06': ('{% load cache %}{% cache 2 test foo %}cache06{% endcache %}', {'foo': 2}, 'cache06'),
+ 'cache07': ('{% load cache %}{% cache 2 test foo %}cache07{% endcache %}', {'foo': 1}, 'cache05'),
- # Raise exception if we dont have at least 2 args, first one integer.
- 'cache08' : ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError),
- 'cache09' : ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError),
- 'cache10' : ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError),
+ # Allow first argument to be a variable.
+ 'cache08': ('{% load cache %}{% cache time test foo %}cache08{% endcache %}', {'foo': 2, 'time': 2}, 'cache06'),
+ 'cache09': ('{% load cache %}{% cache time test foo %}cache09{% endcache %}', {'foo': 3, 'time': -1}, 'cache09'),
+ 'cache10': ('{% load cache %}{% cache time test foo %}cache10{% endcache %}', {'foo': 3, 'time': -1}, 'cache10'),
+
+ # Raise exception if we don't have at least 2 args, first one integer.
+ 'cache11': ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError),
+ 'cache12': ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError),
+ 'cache13': ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError),
+ 'cache14': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': 'fail'}, template.TemplateSyntaxError),
+ 'cache15': ('{% load cache %}{% cache foo bar %}{% endcache %}', {'foo': []}, template.TemplateSyntaxError),
### AUTOESCAPE TAG ##############################################
'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"),