diff options
| author | Justin Bronn <jbronn@gmail.com> | 2008-07-04 20:16:22 +0000 |
|---|---|---|
| committer | Justin Bronn <jbronn@gmail.com> | 2008-07-04 20:16:22 +0000 |
| commit | bc3d6b49089ef0ea7d174eb383b1019b6a4a061a (patch) | |
| tree | b95728880e8a968ae6a921a21cec501f6a278d08 | |
| parent | aef8a8305d2190b00386b8b1603deb03a2949f5b (diff) | |
gis: Merged revisions 7772-7808,7811-7814,7816-7823,7826-7829,7831-7833,7835 via svnmerge from trunk. Modified `GeoWhereNode` accordingly for changes in r7835.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7836 bcc190cf-cafb-0310-a4f2-bffc1f526a37
104 files changed, 6765 insertions, 2047 deletions
@@ -59,7 +59,7 @@ answer newbie questions, and generally made Django that much better: Arthur <avandorp@gmail.com> av0000@mail.ru David Avsajanishvili <avsd05@gmail.com> - axiak@mit.edu + Mike Axiak <axiak@mit.edu> Niran Babalola <niran@niran.org> Morten Bagai <m@bagai.com> Mikaël Barbero <mikael.barbero nospam at nospam free.fr> @@ -94,6 +94,7 @@ answer newbie questions, and generally made Django that much better: Sengtha Chay <sengtha@e-khmer.com> ivan.chelubeev@gmail.com Bryan Chow <bryan at verdjn dot com> + Antonis Christofides <anthony@itia.ntua.gr> Michal Chruszcz <troll@pld-linux.org> Can Burak Çilingir <canburak@cs.bilgi.edu.tr> Ian Clelland <clelland@gmail.com> @@ -140,7 +141,9 @@ answer newbie questions, and generally made Django that much better: Marc Fargas <telenieko@telenieko.com> Szilveszter Farkas <szilveszter.farkas@gmail.com> favo@exoweb.net + fdr <drfarina@gmail.com> Dmitri Fedortchenko <zeraien@gmail.com> + Jonathan Feignberg <jdf@pobox.com> Liang Feng <hutuworm@gmail.com> Bill Fenner <fenner@gmail.com> Stefane Fermgier <sf@fermigier.com> @@ -192,6 +195,7 @@ answer newbie questions, and generally made Django that much better: james_027@yahoo.com jcrasta@gmail.com Zak Johnson <zakj@nox.cx> + Nis Jørgensen <nis@superlativ.dk> Michael Josephson <http://www.sdjournal.com/> jpellerin@gmail.com junzhang.jn@gmail.com diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 006ab421dd..2c9720da10 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -231,6 +231,21 @@ MEDIA_ROOT = '' # Example: "http://media.lawrence.com" MEDIA_URL = '' +# List of upload handler classes to be applied in order. +FILE_UPLOAD_HANDLERS = ( + 'django.core.files.uploadhandler.MemoryFileUploadHandler', + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', +) + +# Maximum size, in bytes, of a request before it will be streamed to the +# file system instead of into memory. +FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB + +# Directory in which upload streamed files will be temporarily saved. A value of +# `None` will make Django use the operating system's default temporary directory +# (i.e. "/tmp" on *nix systems). +FILE_UPLOAD_TEMP_DIR = None + # Default formatting for date objects. See all available format strings here: # http://www.djangoproject.com/documentation/templates/#now DATE_FORMAT = 'N j, Y' diff --git a/django/conf/locale/mk/LC_MESSAGES/django.mo b/django/conf/locale/mk/LC_MESSAGES/django.mo Binary files differindex 21e3ff7c5d..6dfd651e76 100644 --- a/django/conf/locale/mk/LC_MESSAGES/django.mo +++ b/django/conf/locale/mk/LC_MESSAGES/django.mo diff --git a/django/conf/locale/mk/LC_MESSAGES/django.po b/django/conf/locale/mk/LC_MESSAGES/django.po index c28d098515..5e531a4d12 100644 --- a/django/conf/locale/mk/LC_MESSAGES/django.po +++ b/django/conf/locale/mk/LC_MESSAGES/django.po @@ -1,12 +1,12 @@ # translation of django.po to Macedonian # -# Georgi Stanojevski <glisha@gmail.com>, 2006, 2007. +# Georgi Stanojevski <glisha@gmail.com>, 2006, 2007, 2008. msgid "" msgstr "" -"Project-Id-Version: Django\n" +"Project-Id-Version: django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2007-04-07 19:51+0200\n" -"PO-Revision-Date: 2007-04-07 20:29+0200\n" +"POT-Creation-Date: 2008-06-18 12:26+0200\n" +"PO-Revision-Date: 2008-06-28 16:36+0200\n" "Last-Translator: Georgi Stanojevski <glisha@gmail.com>\n" "Language-Team: Macedonian <ossm-members@hedona.on.net.mk>\n" "MIME-Version: 1.0\n" @@ -15,750 +15,195 @@ msgstr "" "X-Generator: KBabel 1.11.4\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -#: db/models/manipulators.py:307 -#, python-format -msgid "%(object)s with this %(type)s already exists for the given %(field)s." -msgstr "%(object)s од овој тип %(type)s веќе постои за даденото %(field)s." - -#: db/models/manipulators.py:308 contrib/admin/views/main.py:335 -#: contrib/admin/views/main.py:337 contrib/admin/views/main.py:339 -msgid "and" -msgstr "и" - -#: db/models/fields/related.py:53 -#, python-format -msgid "Please enter a valid %s." -msgstr "Ве молам внесете правилно %s." - -#: db/models/fields/related.py:642 -msgid "Separate multiple IDs with commas." -msgstr "Одвојте ги идентификационите броеви со запирки." - -#: db/models/fields/related.py:644 -msgid "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "Држете го „Control“ или „Command“ на Мекинтош за да изберете повеќе од едно." - -#: db/models/fields/related.py:691 -#, python-format -msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." -msgid_plural "Please enter valid %(self)s IDs. The values %(value)r are invalid." -msgstr[0] "" -"Ве молам внесете правилен %(self)s идентификацион број. Оваа вредност %" -"(value)r е неправилна." -msgstr[1] "" -"Ве молам внесете правилен %(self)s идентификацион број. Вредностите %(value)" -"r се неправилни." - -#: db/models/fields/__init__.py:42 -#, python-format -msgid "%(optname)s with this %(fieldname)s already exists." -msgstr "%(optname)s со ова %(fieldname)s веќе постои." - -#: db/models/fields/__init__.py:117 db/models/fields/__init__.py:274 -#: db/models/fields/__init__.py:610 db/models/fields/__init__.py:621 -#: oldforms/__init__.py:357 newforms/fields.py:80 newforms/fields.py:376 -#: newforms/fields.py:452 newforms/fields.py:463 newforms/models.py:178 -msgid "This field is required." -msgstr "Ова поле е задолжително." - -#: db/models/fields/__init__.py:367 -msgid "This value must be an integer." -msgstr "Оваа вредност мора да биде цел број." - -#: db/models/fields/__init__.py:402 -msgid "This value must be either True or False." -msgstr "Оваа вредност мора да биде или точно или неточно." - -#: db/models/fields/__init__.py:423 -msgid "This field cannot be null." -msgstr "Оваа вредност неможе да биде null." - -#: db/models/fields/__init__.py:457 core/validators.py:148 -msgid "Enter a valid date in YYYY-MM-DD format." -msgstr "Внесете правилен датум во форматот ГГГГ-ММ-ДД." - -#: db/models/fields/__init__.py:526 core/validators.py:157 -msgid "Enter a valid date/time in YYYY-MM-DD HH:MM format." -msgstr "Внесете правилен датум/време во форматот ГГГГ-ММ-ДД ЧЧ:ММ." - -#: db/models/fields/__init__.py:630 -msgid "Enter a valid filename." -msgstr "Внесите правилно име на датотека." - -#: db/models/fields/__init__.py:751 -msgid "This value must be either None, True or False." -msgstr "Оваа вредност мора да биде празна, точно или неточно." - -#: conf/global_settings.py:39 +#: conf/global_settings.py:44 msgid "Arabic" msgstr "Арапски" -#: conf/global_settings.py:40 +#: conf/global_settings.py:45 msgid "Bengali" msgstr "Бенгалски" -#: conf/global_settings.py:41 +#: conf/global_settings.py:46 +msgid "Bulgarian" +msgstr "Бугарски" + +#: conf/global_settings.py:47 msgid "Catalan" msgstr "Каталански" -#: conf/global_settings.py:42 +#: conf/global_settings.py:48 msgid "Czech" msgstr "Чешки" -#: conf/global_settings.py:43 +#: conf/global_settings.py:49 msgid "Welsh" msgstr "Велшки" -#: conf/global_settings.py:44 +#: conf/global_settings.py:50 msgid "Danish" msgstr "Дански" -#: conf/global_settings.py:45 +#: conf/global_settings.py:51 msgid "German" msgstr "Германски" -#: conf/global_settings.py:46 +#: conf/global_settings.py:52 msgid "Greek" msgstr "Грчки" -#: conf/global_settings.py:47 +#: conf/global_settings.py:53 msgid "English" msgstr "Англиски" -#: conf/global_settings.py:48 +#: conf/global_settings.py:54 msgid "Spanish" msgstr "Шпански" -#: conf/global_settings.py:49 +#: conf/global_settings.py:55 msgid "Argentinean Spanish" msgstr "Аргентиско шпански" -#: conf/global_settings.py:50 +#: conf/global_settings.py:56 +msgid "Basque" +msgstr "Баскиски" + +#: conf/global_settings.py:57 +msgid "Persian" +msgstr "Персиски" + +#: conf/global_settings.py:58 msgid "Finnish" msgstr "Фински" -#: conf/global_settings.py:51 +#: conf/global_settings.py:59 msgid "French" msgstr "Француски" -#: conf/global_settings.py:52 +#: conf/global_settings.py:60 +msgid "Irish" +msgstr "Ирски" + +#: conf/global_settings.py:61 msgid "Galician" msgstr "Галски" -#: conf/global_settings.py:53 +#: conf/global_settings.py:62 msgid "Hungarian" msgstr "Унгарски" -#: conf/global_settings.py:54 +#: conf/global_settings.py:63 msgid "Hebrew" msgstr "Еврејски" -#: conf/global_settings.py:55 +#: conf/global_settings.py:64 +msgid "Croatian" +msgstr "Хрватски" + +#: conf/global_settings.py:65 msgid "Icelandic" msgstr "Исландски" -#: conf/global_settings.py:56 +#: conf/global_settings.py:66 msgid "Italian" msgstr "Италијански" -#: conf/global_settings.py:57 +#: conf/global_settings.py:67 msgid "Japanese" msgstr "Јапонски" -#: conf/global_settings.py:58 +#: conf/global_settings.py:68 +msgid "Georgian" +msgstr "Грузиски" + +#: conf/global_settings.py:69 +msgid "Korean" +msgstr "Корејски" + +#: conf/global_settings.py:70 +msgid "Khmer" +msgstr "Кмер" + +#: conf/global_settings.py:71 msgid "Kannada" msgstr "Канада" -#: conf/global_settings.py:59 +#: conf/global_settings.py:72 msgid "Latvian" msgstr "Латвиски" -#: conf/global_settings.py:60 +#: conf/global_settings.py:73 msgid "Macedonian" msgstr "Македонски" -#: conf/global_settings.py:61 +#: conf/global_settings.py:74 msgid "Dutch" msgstr "Холандски" -#: conf/global_settings.py:62 +#: conf/global_settings.py:75 msgid "Norwegian" msgstr "Норвешки" -#: conf/global_settings.py:63 +#: conf/global_settings.py:76 msgid "Polish" msgstr "Полски" -#: conf/global_settings.py:64 +#: conf/global_settings.py:77 msgid "Portugese" msgstr "Португалкски" -#: conf/global_settings.py:65 -msgid "Brazilian" -msgstr "Бразилски" +#: conf/global_settings.py:78 +msgid "Brazilian Portuguese" +msgstr "Бразилско португалски" -#: conf/global_settings.py:66 +#: conf/global_settings.py:79 msgid "Romanian" msgstr "Романски" -#: conf/global_settings.py:67 +#: conf/global_settings.py:80 msgid "Russian" msgstr "Руски" -#: conf/global_settings.py:68 +#: conf/global_settings.py:81 msgid "Slovak" msgstr "Словачки" -#: conf/global_settings.py:69 +#: conf/global_settings.py:82 msgid "Slovenian" msgstr "Словенечки" -#: conf/global_settings.py:70 +#: conf/global_settings.py:83 msgid "Serbian" msgstr "Српски" -#: conf/global_settings.py:71 +#: conf/global_settings.py:84 msgid "Swedish" msgstr "Шведски" -#: conf/global_settings.py:72 +#: conf/global_settings.py:85 msgid "Tamil" msgstr "Тамил" -#: conf/global_settings.py:73 +#: conf/global_settings.py:86 msgid "Telugu" msgstr "Телугу" -#: conf/global_settings.py:74 +#: conf/global_settings.py:87 msgid "Turkish" msgstr "Турски" -#: conf/global_settings.py:75 +#: conf/global_settings.py:88 msgid "Ukrainian" msgstr "Украински" -#: conf/global_settings.py:76 +#: conf/global_settings.py:89 msgid "Simplified Chinese" msgstr "Упростен кинески" -#: conf/global_settings.py:77 +#: conf/global_settings.py:90 msgid "Traditional Chinese" msgstr "Традиционален кинески" -#: core/validators.py:64 -msgid "This value must contain only letters, numbers and underscores." -msgstr "Оваа вредност смее да има само букви, бројки или долни црти." - -#: core/validators.py:68 -msgid "" -"This value must contain only letters, numbers, underscores, dashes or " -"slashes." -msgstr "Оваа вредност смее да има само букви, бројки, долни црти, црти или коси црти." - -#: core/validators.py:72 -msgid "This value must contain only letters, numbers, underscores or hyphens." -msgstr "Оваа вредност смее да содржи само букви, бројки, долни црти или црти." - -#: core/validators.py:76 -msgid "Uppercase letters are not allowed here." -msgstr "Големи букви не се дозволени." - -#: core/validators.py:80 -msgid "Lowercase letters are not allowed here." -msgstr "Мали букви не се дозволени." - -#: core/validators.py:87 -msgid "Enter only digits separated by commas." -msgstr "Внесете само цифри одделени со запирки." - -#: core/validators.py:99 -msgid "Enter valid e-mail addresses separated by commas." -msgstr "Внесете валидни адреси за е-пошта одделени со запирки." - -#: core/validators.py:103 -msgid "Please enter a valid IP address." -msgstr "Ве молам внесете валидна ИП адреса." - -#: core/validators.py:107 -msgid "Empty values are not allowed here." -msgstr "Празни вредности не се дозволени." - -#: core/validators.py:111 -msgid "Non-numeric characters aren't allowed here." -msgstr "Ненумерички знаци не се дозволени тука." - -#: core/validators.py:115 -msgid "This value can't be comprised solely of digits." -msgstr "Оваа вредност не смее да биде само од цифри." - -#: core/validators.py:120 newforms/fields.py:128 -msgid "Enter a whole number." -msgstr "Внеси цел број." - -#: core/validators.py:124 -msgid "Only alphabetical characters are allowed here." -msgstr "Дозволени се само букви." - -#: core/validators.py:139 -msgid "Year must be 1900 or later." -msgstr "Годината мора да биде 1900 или покасно." - -#: core/validators.py:143 -#, python-format -msgid "Invalid date: %s" -msgstr "Неправилен датум: %s" - -#: core/validators.py:153 -msgid "Enter a valid time in HH:MM format." -msgstr "Внесете правилно време во форматот HH:MM." - -#: core/validators.py:162 newforms/fields.py:271 -msgid "Enter a valid e-mail address." -msgstr "Внесeте правилна адреса за е-пошта." - -#: core/validators.py:174 core/validators.py:445 oldforms/__init__.py:672 -msgid "No file was submitted. Check the encoding type on the form." -msgstr "Не беше пратена датотека. Проверете го типот на енкодирање на формата." - -#: core/validators.py:178 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" -"Качете валидна фотографија. Датотеката која ја качивте или не беше " -"фотографија или беше расипана датотеката." - -#: core/validators.py:185 -#, python-format -msgid "The URL %s does not point to a valid image." -msgstr "Адресата %s не покажува кон валидна фотографија." - -#: core/validators.py:189 -#, python-format -msgid "Phone numbers must be in XXX-XXX-XXXX format. \"%s\" is invalid." -msgstr "" -"Телефонските броеви мора да бидат во XXX-XXX-XXXX форматот. „%s“ не е " -"валиден." - -#: core/validators.py:197 -#, python-format -msgid "The URL %s does not point to a valid QuickTime video." -msgstr "Адресата „%s“ не покажува кон QuickTime видео." - -#: core/validators.py:201 -msgid "A valid URL is required." -msgstr "Задолжителна е правилна адреса." - -#: core/validators.py:215 -#, python-format -msgid "" -"Valid HTML is required. Specific errors are:\n" -"%s" -msgstr "" -"Задолжителен е правилен HTML. Грешките се:\n" -"%s" - -#: core/validators.py:222 -#, python-format -msgid "Badly formed XML: %s" -msgstr "Неправилно формиран XML: %s" - -#: core/validators.py:239 -#, python-format -msgid "Invalid URL: %s" -msgstr "Неправилна адреса: %s" - -#: core/validators.py:244 core/validators.py:246 -#, python-format -msgid "The URL %s is a broken link." -msgstr "Адресата %s е скршена врска." - -#: core/validators.py:252 -msgid "Enter a valid U.S. state abbreviation." -msgstr "Внесете правилна скратеница за држава во САД." - -#: core/validators.py:266 -#, python-format -msgid "Watch your mouth! The word %s is not allowed here." -msgid_plural "Watch your mouth! The words %s are not allowed here." -msgstr[0] "Внимавајте на јазикот. Тука не е дозволен зборот %s." -msgstr[1] "Внимавајте на јазикот. Тука не се дозволени зборовите %s." - -#: core/validators.py:273 -#, python-format -msgid "This field must match the '%s' field." -msgstr "Ова поле мора да соодејствува со полето „%s“." - -#: core/validators.py:292 -msgid "Please enter something for at least one field." -msgstr "Ве молам внесете нешто во барем едно поле." - -#: core/validators.py:301 core/validators.py:312 -msgid "Please enter both fields or leave them both empty." -msgstr "Ве молам внесете во двете полиња или оставете ги двете празни." - -#: core/validators.py:320 -#, python-format -msgid "This field must be given if %(field)s is %(value)s" -msgstr "Ова поле мора да биде зададено ако %(field)s е %(value)s" - -#: core/validators.py:333 -#, python-format -msgid "This field must be given if %(field)s is not %(value)s" -msgstr "Ова поле мора да биде зададено ако %(field)s не е %(value)s" - -#: core/validators.py:352 -msgid "Duplicate values are not allowed." -msgstr "Дупликат вредности не се дозволени." - -#: core/validators.py:367 -#, python-format -msgid "This value must be between %(lower)s and %(upper)s." -msgstr "Оваа вредноста мора да биде помеѓу %(lower)s и %(upper)s." - -#: core/validators.py:369 -#, python-format -msgid "This value must be at least %s." -msgstr "Оваа вредноста мора да биде најмалку %s." - -#: core/validators.py:371 -#, python-format -msgid "This value must be no more than %s." -msgstr "Оваа вредност не смее да биде поголема од %s." - -#: core/validators.py:407 -#, python-format -msgid "This value must be a power of %s." -msgstr "Оваа вредноста мора да биде степен од %s." - -#: core/validators.py:418 -msgid "Please enter a valid decimal number." -msgstr "Ве молам внесете правилен децимален број." - -#: core/validators.py:422 -#, python-format -msgid "Please enter a valid decimal number with at most %s total digit." -msgid_plural "Please enter a valid decimal number with at most %s total digits." -msgstr[0] "Ве молам внесете правилен децимален број со најмногу %s цифрa." -msgstr[1] "Ве молам внесете правилен децимален број со најмногу %s вкупно цифри." - -#: core/validators.py:425 -#, python-format -msgid "Please enter a valid decimal number with a whole part of at most %s digit." -msgid_plural "Please enter a valid decimal number with a whole part of at most %s digits." -msgstr[0] "" -"Ве молам внесете правилен децимален број кој во целиот број има најмногу %s " -"цифра." -msgstr[1] "" -"Ве молам внесете правилен децимален број кој во целиот број има најмногу %s " -"цифри." - -#: core/validators.py:428 -#, python-format -msgid "Please enter a valid decimal number with at most %s decimal place." -msgid_plural "Please enter a valid decimal number with at most %s decimal places." -msgstr[0] "Ве молам внесете правилен децимален број кој има најмногу %s децимална цифра." -msgstr[1] "Ве молам внесете правилен децимален број кој има најмногу %s децимални цифри." - -#: core/validators.py:438 -#, python-format -msgid "Make sure your uploaded file is at least %s bytes big." -msgstr "Потврдете дека качената датотека има најмалку %s бајти." - -#: core/validators.py:439 -#, python-format -msgid "Make sure your uploaded file is at most %s bytes big." -msgstr "Потврдете дека качената датотека има најмногу %s бајти." - -#: core/validators.py:456 -msgid "The format for this field is wrong." -msgstr "Форматот за ова поле е грешен." - -#: core/validators.py:471 -msgid "This field is invalid." -msgstr "Ова поле не е правилно." - -#: core/validators.py:507 -#, python-format -msgid "Could not retrieve anything from %s." -msgstr "Неможев да извадам ништо од %s." - -#: core/validators.py:510 -#, python-format -msgid "The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'." -msgstr "Адресата %(url)s врати неправилно заглавје Content-Type „%(contenttype)s“." - -#: core/validators.py:543 -#, python-format -msgid "" -"Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with " -"\"%(start)s\".)" -msgstr "" -"Ве молам затворете го отворениот %(tag)s таг од линијата %(line)s. (линијата " -"почнува со „%(start)s“.)" - -#: core/validators.py:547 -#, python-format -msgid "" -"Some text starting on line %(line)s is not allowed in that context. (Line " -"starts with \"%(start)s\".)" -msgstr "" -"Некој текст кој почнува на линијата %(line)s не е дозволен во тој контекст. " -"(Линијата започнува со „%(start)s“.)" - -#: core/validators.py:552 -#, python-format -msgid "" -"\"%(attr)s\" on line %(line)s is an invalid attribute. (Line starts with \"%" -"(start)s\".)" -msgstr "" -"„%(attr)s“ на линија %(line)s е неправилен атрибут. (линијата започнува со „%" -"(start)s“.)" - -#: core/validators.py:557 -#, python-format -msgid "" -"\"<%(tag)s>\" on line %(line)s is an invalid tag. (Line starts with \"%" -"(start)s\".)" -msgstr "" -"„<%(tag)s>“ на линија %(line)s е неправилен таг. (линијата започнува со „%" -"(start)s“.)" - -#: core/validators.py:561 -#, python-format -msgid "" -"A tag on line %(line)s is missing one or more required attributes. (Line " -"starts with \"%(start)s\".)" -msgstr "" -"На таг од линијата %(line)s му недостасува еден или повеќе од потребните " -"атрибути (линијата започнува со „%(start)s“)." - -#: core/validators.py:566 -#, python-format -msgid "" -"The \"%(attr)s\" attribute on line %(line)s has an invalid value. (Line " -"starts with \"%(start)s\".)" -msgstr "" -"Атрибутот „%(attr)s“ на линијата %(line)s има неправилна вредност (линијата " -"започнува со „%(start)s“)." - -#: contrib/auth/forms.py:17 contrib/auth/forms.py:138 -msgid "The two password fields didn't match." -msgstr "Двете полиња со лозинките не се совпаѓаат." - -#: contrib/auth/forms.py:25 -msgid "A user with that username already exists." -msgstr "Веќе постои корисник со тоа корисничко име." - -#: contrib/auth/forms.py:53 -msgid "" -"Your Web browser doesn't appear to have cookies enabled. Cookies are " -"required for logging in." -msgstr "" -"Не изгледа дека вашиот прелистувач има овозможено колачиња. Колачињата се " -"потребни за да се најавите." - -#: contrib/auth/forms.py:60 contrib/admin/views/decorators.py:10 -msgid "" -"Please enter a correct username and password. Note that both fields are case-" -"sensitive." -msgstr "" -"Ве молам внесете точно корисничко име и лозинка. Имајте на ум дека и во " -"двете полиња се битни големите и малите букви." - -#: contrib/auth/forms.py:62 -msgid "This account is inactive." -msgstr "Оваа сметка е неактивна." - -#: contrib/auth/forms.py:85 -msgid "" -"That e-mail address doesn't have an associated user account. Are you sure " -"you've registered?" -msgstr "" -"Нема регистрирано корисник со оваа адреса за е-пошта. Сигурни ли сте дека " -"сте регистрирани?" - -#: contrib/auth/forms.py:117 -msgid "The two 'new password' fields didn't match." -msgstr "Двете нови лозинки не се совпаѓаат." - -#: contrib/auth/forms.py:124 -msgid "Your old password was entered incorrectly. Please enter it again." -msgstr "Не ја внесовте точно вашата стара лозинка. Ве молам внесете ја повторно." - -#: contrib/auth/models.py:38 contrib/auth/models.py:58 -msgid "name" -msgstr "име" - -#: contrib/auth/models.py:40 -msgid "codename" -msgstr "кодно име" - -#: contrib/auth/models.py:43 -msgid "permission" -msgstr "привилегија" - -#: contrib/auth/models.py:44 contrib/auth/models.py:59 -msgid "permissions" -msgstr "привилегии" - -#: contrib/auth/models.py:62 -msgid "group" -msgstr "група" - -#: contrib/auth/models.py:63 contrib/auth/models.py:103 -msgid "groups" -msgstr "групи" - -#: contrib/auth/models.py:93 -msgid "username" -msgstr "корисничко име" - -#: contrib/auth/models.py:93 -msgid "" -"Required. 30 characters or fewer. Alphanumeric characters only (letters, " -"digits and underscores)." -msgstr "" -"Дозволени се најмногу 30 знаци. Дозволени се само алфанумерички знаци " -"(букви, цифри и долна црта)." - -#: contrib/auth/models.py:94 -msgid "first name" -msgstr "име" - -#: contrib/auth/models.py:95 -msgid "last name" -msgstr "презиме" - -#: contrib/auth/models.py:96 -msgid "e-mail address" -msgstr "е-пошта" - -#: contrib/auth/models.py:97 -msgid "password" -msgstr "лозинка" - -#: contrib/auth/models.py:97 -msgid "" -"Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change " -"password form</a>." -msgstr "" -"Користете '[algo]$[salt]$[hexdigest]' или користете ја <a href=\"password/" -"\">формата за промена на лозинката</a>." - -#: contrib/auth/models.py:98 -msgid "staff status" -msgstr "статус на администраторите" - -#: contrib/auth/models.py:98 -msgid "Designates whether the user can log into this admin site." -msgstr "Означува дали корисникот може да се логира во сајтот за администрација." - -#: contrib/auth/models.py:99 -msgid "active" -msgstr "активен" - -#: contrib/auth/models.py:99 -msgid "" -"Designates whether this user can log into the Django admin. Unselect this " -"instead of deleting accounts." -msgstr "" -"Означува дали корисникот може да се логира. Одштиклирајте го ова наместо да " -"бришете корисници." - -#: contrib/auth/models.py:100 -msgid "superuser status" -msgstr "статус на суперкорисник" - -#: contrib/auth/models.py:100 -msgid "" -"Designates that this user has all permissions without explicitly assigning " -"them." -msgstr "" -"Означува дека овој корисник ги има сите привилегии без експлицитно да се " -"доделуваат сите." - -#: contrib/auth/models.py:101 -msgid "last login" -msgstr "последна најава" - -#: contrib/auth/models.py:102 -msgid "date joined" -msgstr "датум на зачленување" - -#: contrib/auth/models.py:104 -msgid "" -"In addition to the permissions manually assigned, this user will also get " -"all permissions granted to each group he/she is in." -msgstr "" -"Како дополнување на рачно доделени привилегии, овој корисник ќе ги добие " -"автоматски и сите привилегии за секоја група во која тој/таа членува." - -#: contrib/auth/models.py:105 -msgid "user permissions" -msgstr "кориснички привилегии" - -#: contrib/auth/models.py:109 -msgid "user" -msgstr "корисник" - -#: contrib/auth/models.py:110 -msgid "users" -msgstr "корисници" - -#: contrib/auth/models.py:116 -msgid "Personal info" -msgstr "Лични информации" - -#: contrib/auth/models.py:117 -msgid "Permissions" -msgstr "Привилегии" - -#: contrib/auth/models.py:118 -msgid "Important dates" -msgstr "Важни датуми" - -#: contrib/auth/models.py:119 -msgid "Groups" -msgstr "Групи" - -#: contrib/auth/models.py:263 -msgid "message" -msgstr "порака" - -#: contrib/auth/views.py:39 -msgid "Logged out" -msgstr "Одјавен" - -#: contrib/admin/models.py:16 -msgid "action time" -msgstr "време на акција" - -#: contrib/admin/models.py:19 -msgid "object id" -msgstr "идентификационен број на објект" - -#: contrib/admin/models.py:20 -msgid "object repr" -msgstr "object repr" - -#: contrib/admin/models.py:21 -msgid "action flag" -msgstr "знакче за акција" - -#: contrib/admin/models.py:22 -msgid "change message" -msgstr "измени ја пораката" - -#: contrib/admin/models.py:25 -msgid "log entry" -msgstr "ставка во записникот" - -#: contrib/admin/models.py:26 -msgid "log entries" -msgstr "ставки во записникот" - -#: contrib/admin/filterspecs.py:40 +#: contrib/admin/filterspecs.py:44 #, python-format msgid "" "<h3>By %s:</h3>\n" @@ -767,411 +212,188 @@ msgstr "" "<h3>Од %s:</h3>\n" "<ul>\n" -#: contrib/admin/filterspecs.py:70 contrib/admin/filterspecs.py:88 -#: contrib/admin/filterspecs.py:143 contrib/admin/filterspecs.py:169 +#: contrib/admin/filterspecs.py:74 contrib/admin/filterspecs.py:92 +#: contrib/admin/filterspecs.py:147 contrib/admin/filterspecs.py:173 msgid "All" msgstr "Сите" -#: contrib/admin/filterspecs.py:109 +#: contrib/admin/filterspecs.py:113 msgid "Any date" msgstr "Било кој датум" -#: contrib/admin/filterspecs.py:110 +#: contrib/admin/filterspecs.py:114 msgid "Today" msgstr "Денеска" -#: contrib/admin/filterspecs.py:113 +#: contrib/admin/filterspecs.py:117 msgid "Past 7 days" msgstr "Последните 7 дена" -#: contrib/admin/filterspecs.py:115 +#: contrib/admin/filterspecs.py:119 msgid "This month" msgstr "Овој месец" -#: contrib/admin/filterspecs.py:117 +#: contrib/admin/filterspecs.py:121 msgid "This year" msgstr "Оваа година" -#: contrib/admin/filterspecs.py:143 oldforms/__init__.py:577 -#: newforms/widgets.py:174 +#: contrib/admin/filterspecs.py:147 newforms/widgets.py:231 +#: oldforms/__init__.py:592 msgid "Yes" msgstr "Да" -#: contrib/admin/filterspecs.py:143 oldforms/__init__.py:577 -#: newforms/widgets.py:174 +#: contrib/admin/filterspecs.py:147 newforms/widgets.py:231 +#: oldforms/__init__.py:592 msgid "No" msgstr "Не" -#: contrib/admin/filterspecs.py:150 oldforms/__init__.py:577 -#: newforms/widgets.py:174 +#: contrib/admin/filterspecs.py:154 newforms/widgets.py:231 +#: oldforms/__init__.py:592 msgid "Unknown" msgstr "Непознато" -#: contrib/admin/views/decorators.py:24 -#: contrib/admin/templates/admin/login.html:25 -msgid "Log in" -msgstr "Најава" - -#: contrib/admin/views/decorators.py:62 -msgid "" -"Please log in again, because your session has expired. Don't worry: Your " -"submission has been saved." -msgstr "" -"Ве молам најавете се повторно бидејќи вашата сесија е истечена. Не се " -"грижете. Вашите внесови беа зачувани." - -#: contrib/admin/views/decorators.py:69 -msgid "" -"Looks like your browser isn't configured to accept cookies. Please enable " -"cookies, reload this page, and try again." -msgstr "" -"Изгледа дека вашиот прелистувач не е конфигуриран да прифаќа колачиња. Ве " -"молам овозможете ги колачињата, превчитајте ја страта и пробајте повторно." +#: contrib/admin/models.py:18 +msgid "action time" +msgstr "време на акција" -#: contrib/admin/views/decorators.py:83 -msgid "Usernames cannot contain the '@' character." -msgstr "Корисничките имиња неможе да го содржат „@“ знакот." +#: contrib/admin/models.py:21 +msgid "object id" +msgstr "идентификационен број на објект" -#: contrib/admin/views/decorators.py:85 -#, python-format -msgid "Your e-mail address is not your username. Try '%s' instead." -msgstr "Вашата е-пошта не е вашето корисничко име. Пробајте со „%s“." +#: contrib/admin/models.py:22 +msgid "object repr" +msgstr "object repr" -#: contrib/admin/views/main.py:223 -msgid "Site administration" -msgstr "Администрација на сајт" +#: contrib/admin/models.py:23 +msgid "action flag" +msgstr "знакче за акција" -#: contrib/admin/views/main.py:257 contrib/admin/views/auth.py:19 -#, python-format -msgid "The %(name)s \"%(obj)s\" was added successfully." -msgstr "%(name)s \"%(obj)s\" беше успешно додаден." +#: contrib/admin/models.py:24 +msgid "change message" +msgstr "измени ја пораката" -#: contrib/admin/views/main.py:261 contrib/admin/views/main.py:347 -#: contrib/admin/views/auth.py:24 -msgid "You may edit it again below." -msgstr "Подолу можете повторно да го уредите." +#: contrib/admin/models.py:27 +msgid "log entry" +msgstr "ставка во записникот" -#: contrib/admin/views/main.py:271 contrib/admin/views/main.py:356 -#, python-format -msgid "You may add another %s below." -msgstr "Подолу можете да додате уште еден %s." +#: contrib/admin/models.py:28 +msgid "log entries" +msgstr "ставки во записникот" -#: contrib/admin/views/main.py:289 -#, python-format -msgid "Add %s" -msgstr "Додади %s" +#: contrib/admin/templates/admin/404.html:4 +#: contrib/admin/templates/admin/404.html:8 +msgid "Page not found" +msgstr "Страницата не е најдена" -#: contrib/admin/views/main.py:335 -#, python-format -msgid "Added %s." -msgstr "Додадено %s." +#: contrib/admin/templates/admin/404.html:10 +msgid "We're sorry, but the requested page could not be found." +msgstr "Се извинуваме, но неможе да ја најдеме страницата која ја баравте." -#: contrib/admin/views/main.py:337 -#, python-format -msgid "Changed %s." -msgstr "Изменета %s." +#: contrib/admin/templates/admin/500.html:4 +#: contrib/admin/templates/admin/base.html:37 +#: contrib/admin/templates/admin/change_form.html:12 +#: contrib/admin/templates/admin/change_list.html:5 +#: contrib/admin/templates/admin/delete_confirmation.html:5 +#: contrib/admin/templates/admin/invalid_setup.html:4 +#: contrib/admin/templates/admin/object_history.html:4 +#: contrib/admin/templates/admin/auth/user/change_password.html:11 +#: contrib/admin/templates/admin_doc/bookmarklets.html:3 +#: contrib/admin/templates/registration/logged_out.html:4 +#: contrib/admin/templates/registration/password_change_done.html:3 +#: contrib/admin/templates/registration/password_change_form.html:3 +#: contrib/admin/templates/registration/password_reset_done.html:4 +#: contrib/admin/templates/registration/password_reset_form.html:4 +msgid "Home" +msgstr "Дома" -#: contrib/admin/views/main.py:339 -#, python-format -msgid "Deleted %s." -msgstr "Избришана %s." +#: contrib/admin/templates/admin/500.html:4 +msgid "Server error" +msgstr "Грешка со серверот" -#: contrib/admin/views/main.py:342 -msgid "No fields changed." -msgstr "Не беше изменето ниедно поле." +#: contrib/admin/templates/admin/500.html:6 +msgid "Server error (500)" +msgstr "Грешка со серверот (500)" -#: contrib/admin/views/main.py:345 -#, python-format -msgid "The %(name)s \"%(obj)s\" was changed successfully." -msgstr "%(name)s \"%(obj)s\" беше успешно изменета." +#: contrib/admin/templates/admin/500.html:9 +msgid "Server Error <em>(500)</em>" +msgstr "Грешка со серверот <em>(500)</em>" -#: contrib/admin/views/main.py:353 -#, python-format -msgid "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." +#: contrib/admin/templates/admin/500.html:10 +msgid "" +"There's been an error. It's been reported to the site administrators via e-" +"mail and should be fixed shortly. Thanks for your patience." msgstr "" -"%(name)s \"%(obj)s\" беше успешно додадена.Подолу можете повторно да ја " -"уредите." - -#: contrib/admin/views/main.py:391 -#, python-format -msgid "Change %s" -msgstr "Измени %s" - -#: contrib/admin/views/main.py:476 -#, python-format -msgid "One or more %(fieldname)s in %(name)s: %(obj)s" -msgstr "Еден или повеќе %(fieldname)s во %(name)s: %(obj)s" - -#: contrib/admin/views/main.py:481 -#, python-format -msgid "One or more %(fieldname)s in %(name)s:" -msgstr "Еден или повеќе %(fieldname)s во %(name)s:" - -#: contrib/admin/views/main.py:514 -#, python-format -msgid "The %(name)s \"%(obj)s\" was deleted successfully." -msgstr "%(name)s \"%(obj)s\" беше избришана успешно." - -#: contrib/admin/views/main.py:517 -msgid "Are you sure?" -msgstr "Сигурни сте?" - -#: contrib/admin/views/main.py:539 -#, python-format -msgid "Change history: %s" -msgstr "Историја на измени: %s" - -#: contrib/admin/views/main.py:573 -#, python-format -msgid "Select %s" -msgstr "Изберет %s" - -#: contrib/admin/views/main.py:573 -#, python-format -msgid "Select %s to change" -msgstr "Изберете %s за измена" - -#: contrib/admin/views/main.py:768 -msgid "Database error" -msgstr "Грешка во базата со податоци" - -#: contrib/admin/views/doc.py:46 contrib/admin/views/doc.py:48 -#: contrib/admin/views/doc.py:50 -msgid "tag:" -msgstr "таг:" - -#: contrib/admin/views/doc.py:77 contrib/admin/views/doc.py:79 -#: contrib/admin/views/doc.py:81 -msgid "filter:" -msgstr "филтер:" - -#: contrib/admin/views/doc.py:135 contrib/admin/views/doc.py:137 -#: contrib/admin/views/doc.py:139 -msgid "view:" -msgstr "поглед:" - -#: contrib/admin/views/doc.py:164 -#, python-format -msgid "App %r not found" -msgstr "Не е најдена апликацијата %r" - -#: contrib/admin/views/doc.py:171 -#, python-format -msgid "Model %(name)r not found in app %(label)r" -msgstr "Моделот %(name)r не е најден во апликацијата %(label)r" - -#: contrib/admin/views/doc.py:183 -#, python-format -msgid "the related `%(label)s.%(type)s` object" -msgstr "поврзаниот`%(label)s.%(type)s` објект" - -#: contrib/admin/views/doc.py:183 contrib/admin/views/doc.py:205 -#: contrib/admin/views/doc.py:219 contrib/admin/views/doc.py:224 -msgid "model:" -msgstr "модел:" - -#: contrib/admin/views/doc.py:214 -#, python-format -msgid "related `%(label)s.%(name)s` objects" -msgstr "поврзани `%(label)s.%(name)s` објекти" - -#: contrib/admin/views/doc.py:219 -#, python-format -msgid "all %s" -msgstr "сите %s" - -#: contrib/admin/views/doc.py:224 -#, python-format -msgid "number of %s" -msgstr "број на %s" - -#: contrib/admin/views/doc.py:229 -#, python-format -msgid "Fields on %s objects" -msgstr "Полиња на %s објекти" - -#: contrib/admin/views/doc.py:291 contrib/admin/views/doc.py:301 -#: contrib/admin/views/doc.py:303 contrib/admin/views/doc.py:309 -#: contrib/admin/views/doc.py:310 contrib/admin/views/doc.py:312 -msgid "Integer" -msgstr "Цел број" - -#: contrib/admin/views/doc.py:292 -msgid "Boolean (Either True or False)" -msgstr "Логичка (или точно или неточно)" - -#: contrib/admin/views/doc.py:293 contrib/admin/views/doc.py:311 -#, python-format -msgid "String (up to %(maxlength)s)" -msgstr "Збор (до %(maxlength)s)" - -#: contrib/admin/views/doc.py:294 -msgid "Comma-separated integers" -msgstr "Целобројни вредности одделени со запирка" - -#: contrib/admin/views/doc.py:295 -msgid "Date (without time)" -msgstr "Датум (без час)" - -#: contrib/admin/views/doc.py:296 -msgid "Date (with time)" -msgstr "Датум (со час)" - -#: contrib/admin/views/doc.py:297 -msgid "E-mail address" -msgstr "Адреса на е-пошта" - -#: contrib/admin/views/doc.py:298 contrib/admin/views/doc.py:299 -#: contrib/admin/views/doc.py:302 -msgid "File path" -msgstr "Патека на датотека" - -#: contrib/admin/views/doc.py:300 -msgid "Decimal number" -msgstr "Децимален број" +"Имаше грешка. Истата беше пријавена на администраторите и ќе биде поправена " +"во брзо време. Ви благодариме за вашето трпение." -#: contrib/admin/views/doc.py:304 contrib/comments/models.py:85 -msgid "IP address" -msgstr "ИП адреса" +#: contrib/admin/templates/admin/base.html:26 +msgid "Welcome," +msgstr "Добредојдовте," -#: contrib/admin/views/doc.py:306 -msgid "Boolean (Either True, False or None)" -msgstr "Логичка (точно,неточно или празно)" +#: contrib/admin/templates/admin/base.html:28 +#: contrib/admin/templates/admin_doc/bookmarklets.html:3 +msgid "Documentation" +msgstr "Документација" -#: contrib/admin/views/doc.py:307 -msgid "Relation to parent model" -msgstr "Релација со родителскиот модел" +#: contrib/admin/templates/admin/base.html:29 +#: contrib/admin/templates/admin/auth/user/change_password.html:14 +#: contrib/admin/templates/admin/auth/user/change_password.html:45 +msgid "Change password" +msgstr "Промени лозинка" -#: contrib/admin/views/doc.py:308 -msgid "Phone number" -msgstr "Телефонски број" +#: contrib/admin/templates/admin/base.html:30 +#: contrib/comments/templates/comments/form.html:6 +msgid "Log out" +msgstr "Одјава" -#: contrib/admin/views/doc.py:313 -msgid "Text" -msgstr "Текст" +#: contrib/admin/templates/admin/base_site.html:4 +msgid "Django site admin" +msgstr "Џанго администрација на сајт" -#: contrib/admin/views/doc.py:314 -msgid "Time" -msgstr "Час" +#: contrib/admin/templates/admin/base_site.html:7 +msgid "Django administration" +msgstr "Џанго администрација" -#: contrib/admin/views/doc.py:315 contrib/flatpages/models.py:7 -msgid "URL" -msgstr "URL" +#: contrib/admin/templates/admin/change_form.html:14 +#: contrib/admin/templates/admin/index.html:28 +msgid "Add" +msgstr "Додади" -#: contrib/admin/views/doc.py:316 -msgid "U.S. state (two uppercase letters)" -msgstr "Држава во САД (две големи букви)" +#: contrib/admin/templates/admin/change_form.html:20 +#: contrib/admin/templates/admin/object_history.html:4 +msgid "History" +msgstr "Историја" -#: contrib/admin/views/doc.py:317 -msgid "XML text" -msgstr "XML текст" +#: contrib/admin/templates/admin/change_form.html:21 +msgid "View on site" +msgstr "Погледни на сајтот" -#: contrib/admin/views/doc.py:343 -#, python-format -msgid "%s does not appear to be a urlpattern object" -msgstr "%s не изгледа дека е url објект" +#: contrib/admin/templates/admin/change_form.html:31 +#: contrib/admin/templates/admin/auth/user/change_password.html:23 +msgid "Please correct the error below." +msgid_plural "Please correct the errors below." +msgstr[0] "Ве молам поправете ја грешката подолу." +msgstr[1] "Ве молам поправете ги грешките подолу." -#: contrib/admin/views/auth.py:30 -msgid "Add user" -msgstr "Додади корисник" +#: contrib/admin/templates/admin/change_form.html:49 +msgid "Ordering" +msgstr "Подредување" -#: contrib/admin/views/auth.py:57 -msgid "Password changed successfully." -msgstr "Успешна промена на лозинката." +#: contrib/admin/templates/admin/change_form.html:52 +msgid "Order:" +msgstr "Подреди:" -#: contrib/admin/views/auth.py:64 +#: contrib/admin/templates/admin/change_list.html:11 #, python-format -msgid "Change password: %s" -msgstr "Промени лозинка: %s" - -#: contrib/admin/templatetags/admin_list.py:247 -msgid "All dates" -msgstr "Сите датуми" - -#: contrib/admin/templates/admin/pagination.html:10 -msgid "Show all" -msgstr "Прикажи ги сите" - -#: contrib/admin/templates/admin/delete_confirmation.html:3 -#: contrib/admin/templates/admin/change_form.html:10 -#: contrib/admin/templates/admin/change_list.html:5 -#: contrib/admin/templates/admin/object_history.html:3 -#: contrib/admin/templates/admin/base.html:25 -#: contrib/admin/templates/admin/auth/user/change_password.html:9 -#: contrib/admin/templates/admin_doc/bookmarklets.html:3 -#: contrib/admin/templates/registration/password_change_form.html:3 -#: contrib/admin/templates/registration/password_change_done.html:3 -msgid "Documentation" -msgstr "Документација" - -#: contrib/admin/templates/admin/delete_confirmation.html:3 -#: contrib/admin/templates/admin/change_form.html:10 -#: contrib/admin/templates/admin/change_list.html:5 -#: contrib/admin/templates/admin/object_history.html:3 -#: contrib/admin/templates/admin/base.html:25 -#: contrib/admin/templates/admin/auth/user/change_password.html:9 -#: contrib/admin/templates/admin/auth/user/change_password.html:15 -#: contrib/admin/templates/admin/auth/user/change_password.html:46 -#: contrib/admin/templates/admin_doc/template_filter_index.html:5 -#: contrib/admin/templates/admin_doc/bookmarklets.html:4 -#: contrib/admin/templates/admin_doc/template_tag_index.html:5 -#: contrib/admin/templates/admin_doc/index.html:4 -#: contrib/admin/templates/admin_doc/model_detail.html:3 -#: contrib/admin/templates/admin_doc/missing_docutils.html:4 -#: contrib/admin/templates/admin_doc/template_detail.html:4 -#: contrib/admin/templates/admin_doc/view_index.html:5 -#: contrib/admin/templates/admin_doc/model_index.html:5 -#: contrib/admin/templates/admin_doc/view_detail.html:4 -#: contrib/admin/templates/registration/password_change_form.html:3 -#: contrib/admin/templates/registration/password_change_done.html:3 -msgid "Change password" -msgstr "Промени лозинка" - -#: contrib/admin/templates/admin/delete_confirmation.html:3 -#: contrib/admin/templates/admin/change_form.html:10 -#: contrib/admin/templates/admin/change_list.html:5 -#: contrib/admin/templates/admin/object_history.html:3 -#: contrib/admin/templates/admin/base.html:25 -#: contrib/admin/templates/admin/auth/user/change_password.html:9 -#: contrib/admin/templates/admin_doc/template_filter_index.html:5 -#: contrib/admin/templates/admin_doc/bookmarklets.html:4 -#: contrib/admin/templates/admin_doc/template_tag_index.html:5 -#: contrib/admin/templates/admin_doc/index.html:4 -#: contrib/admin/templates/admin_doc/model_detail.html:3 -#: contrib/admin/templates/admin_doc/missing_docutils.html:4 -#: contrib/admin/templates/admin_doc/template_detail.html:4 -#: contrib/admin/templates/admin_doc/view_index.html:5 -#: contrib/admin/templates/admin_doc/model_index.html:5 -#: contrib/admin/templates/admin_doc/view_detail.html:4 -#: contrib/admin/templates/registration/password_change_form.html:3 -#: contrib/admin/templates/registration/password_change_done.html:3 -#: contrib/comments/templates/comments/form.html:6 -msgid "Log out" -msgstr "Одјава" - -#: contrib/admin/templates/admin/delete_confirmation.html:6 -#: contrib/admin/templates/admin/change_form.html:13 -#: contrib/admin/templates/admin/change_list.html:6 -#: contrib/admin/templates/admin/object_history.html:5 -#: contrib/admin/templates/admin/500.html:4 -#: contrib/admin/templates/admin/invalid_setup.html:4 -#: contrib/admin/templates/admin/base.html:30 -#: contrib/admin/templates/admin/auth/user/change_password.html:12 -#: contrib/admin/templates/admin_doc/bookmarklets.html:3 -#: contrib/admin/templates/registration/password_reset_form.html:4 -#: contrib/admin/templates/registration/logged_out.html:4 -#: contrib/admin/templates/registration/password_reset_done.html:4 -#: contrib/admin/templates/registration/password_change_form.html:4 -#: contrib/admin/templates/registration/password_change_done.html:4 -msgid "Home" -msgstr "Дома" +msgid "Add %(name)s" +msgstr "Додади %(name)s" -#: contrib/admin/templates/admin/delete_confirmation.html:9 +#: contrib/admin/templates/admin/delete_confirmation.html:8 #: contrib/admin/templates/admin/submit_line.html:3 msgid "Delete" msgstr "Избриши" -#: contrib/admin/templates/admin/delete_confirmation.html:14 +#: contrib/admin/templates/admin/delete_confirmation.html:13 #, python-format msgid "" "Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " @@ -1182,7 +404,7 @@ msgstr "" "поврзаните објекти, но со вашата сметка немате доволно привилегии да ги " "бришете следните типови на објекти:" -#: contrib/admin/templates/admin/delete_confirmation.html:21 +#: contrib/admin/templates/admin/delete_confirmation.html:20 #, python-format msgid "" "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " @@ -1191,73 +413,18 @@ msgstr "" "Сигурне сте дека сакате да ги бришете %(object_name)s „%(escaped_object)s“? " "Сите овие ставки ќе бидат избришани:" -#: contrib/admin/templates/admin/delete_confirmation.html:26 +#: contrib/admin/templates/admin/delete_confirmation.html:25 msgid "Yes, I'm sure" msgstr "Да, сигурен сум" -#: contrib/admin/templates/admin/404.html:4 -#: contrib/admin/templates/admin/404.html:8 -msgid "Page not found" -msgstr "Страницата не е најдена" - -#: contrib/admin/templates/admin/404.html:10 -msgid "We're sorry, but the requested page could not be found." -msgstr "Се извинуваме, но неможе да ја најдеме страницата која ја баравте." - -#: contrib/admin/templates/admin/change_form.html:15 -#: contrib/admin/templates/admin/index.html:28 -msgid "Add" -msgstr "Додади" - -#: contrib/admin/templates/admin/change_form.html:21 -#: contrib/admin/templates/admin/object_history.html:5 -msgid "History" -msgstr "Историја" - -#: contrib/admin/templates/admin/change_form.html:22 -msgid "View on site" -msgstr "Погледни на сајтот" - -#: contrib/admin/templates/admin/change_form.html:32 -#: contrib/admin/templates/admin/auth/user/change_password.html:24 -msgid "Please correct the error below." -msgid_plural "Please correct the errors below." -msgstr[0] "Ве молам поправете ја грешката подолу." -msgstr[1] "Ве молам поправете ги грешките подолу." - -#: contrib/admin/templates/admin/change_form.html:50 -msgid "Ordering" -msgstr "Подредување" - -#: contrib/admin/templates/admin/change_form.html:53 -msgid "Order:" -msgstr "Подреди:" - #: contrib/admin/templates/admin/filter.html:2 #, python-format msgid " By %(filter_title)s " msgstr " Од %(filter_title)s " -#: contrib/admin/templates/admin/submit_line.html:4 -msgid "Save as new" -msgstr "Сними како нова" - -#: contrib/admin/templates/admin/submit_line.html:5 -msgid "Save and add another" -msgstr "Сними и додади уште" - -#: contrib/admin/templates/admin/submit_line.html:6 -msgid "Save and continue editing" -msgstr "Сними и продолжи со уредување" - -#: contrib/admin/templates/admin/submit_line.html:7 -msgid "Save" -msgstr "Сними" - -#: contrib/admin/templates/admin/change_list.html:12 -#, python-format -msgid "Add %(name)s" -msgstr "Додади %(name)s" +#: contrib/admin/templates/admin/filters.html:4 +msgid "Filter" +msgstr "Филтер" #: contrib/admin/templates/admin/index.html:17 #, python-format @@ -1289,31 +456,49 @@ msgstr "Мои акции" msgid "None available" msgstr "Ништо не е достапно" -#: contrib/admin/templates/admin/base_site.html:4 -msgid "Django site admin" -msgstr "Џанго администрација на сајт" +#: contrib/admin/templates/admin/invalid_setup.html:8 +msgid "" +"Something's wrong with your database installation. Make sure the appropriate " +"database tables have been created, and make sure the database is readable by " +"the appropriate user." +msgstr "" +"Нешто не е во ред со инсталацијата на базата на податоци. Потврдете дека " +"соодветни табели во базата се направени и потврдете дека базата може да биде " +"прочитана од соодветниот корисник." -#: contrib/admin/templates/admin/base_site.html:7 -msgid "Django administration" -msgstr "Џанго администрација" +#: contrib/admin/templates/admin/login.html:17 +#: contrib/comments/templates/comments/form.html:6 +#: contrib/comments/templates/comments/form.html:8 +msgid "Username:" +msgstr "Корисник:" -#: contrib/admin/templates/admin/object_history.html:18 +#: contrib/admin/templates/admin/login.html:20 +#: contrib/comments/templates/comments/form.html:8 +msgid "Password:" +msgstr "Лозинка:" + +#: contrib/admin/templates/admin/login.html:25 +#: contrib/admin/views/decorators.py:31 +msgid "Log in" +msgstr "Најава" + +#: contrib/admin/templates/admin/object_history.html:17 msgid "Date/time" msgstr "Датум/час" -#: contrib/admin/templates/admin/object_history.html:19 +#: contrib/admin/templates/admin/object_history.html:18 msgid "User" msgstr "Корисник" -#: contrib/admin/templates/admin/object_history.html:20 +#: contrib/admin/templates/admin/object_history.html:19 msgid "Action" msgstr "Акција" -#: contrib/admin/templates/admin/object_history.html:26 +#: contrib/admin/templates/admin/object_history.html:25 msgid "DATE_WITH_TIME_FULL" msgstr "j M Y, H:i" -#: contrib/admin/templates/admin/object_history.html:36 +#: contrib/admin/templates/admin/object_history.html:35 msgid "" "This object doesn't have a change history. It probably wasn't added via this " "admin site." @@ -1321,35 +506,9 @@ msgstr "" "Овој објект нема историја на измени. Најверојатно не бил додаден со админ " "сајтот." -#: contrib/admin/templates/admin/500.html:4 -msgid "Server error" -msgstr "Грешка со серверот" - -#: contrib/admin/templates/admin/500.html:6 -msgid "Server error (500)" -msgstr "Грешка со серверот (500)" - -#: contrib/admin/templates/admin/500.html:9 -msgid "Server Error <em>(500)</em>" -msgstr "Грешка со серверот <em>(500)</em>" - -#: contrib/admin/templates/admin/500.html:10 -msgid "" -"There's been an error. It's been reported to the site administrators via e-" -"mail and should be fixed shortly. Thanks for your patience." -msgstr "" -"Имаше грешка. Истата беше пријавена на администраторите и ќе биде поправена " -"во брзо време. Ви благодариме за вашето трпение." - -#: contrib/admin/templates/admin/invalid_setup.html:8 -msgid "" -"Something's wrong with your database installation. Make sure the appropriate " -"database tables have been created, and make sure the database is readable by " -"the appropriate user." -msgstr "" -"Нешто не е во ред со инсталацијата на базата на податоци. Потврдете дека " -"соодветни табели во базата се направени и потврдете дека базата може да биде " -"прочитана од соодветниот корисник." +#: contrib/admin/templates/admin/pagination.html:10 +msgid "Show all" +msgstr "Прикажи ги сите" #: contrib/admin/templates/admin/search_form.html:8 msgid "Go" @@ -1367,24 +526,21 @@ msgstr[1] "%(counter)s резултати" msgid "%(full_result_count)s total" msgstr "вкупно %(full_result_count)s" -#: contrib/admin/templates/admin/filters.html:4 -msgid "Filter" -msgstr "Филтер" +#: contrib/admin/templates/admin/submit_line.html:4 +msgid "Save as new" +msgstr "Сними како нова" -#: contrib/admin/templates/admin/login.html:17 -#: contrib/comments/templates/comments/form.html:6 -#: contrib/comments/templates/comments/form.html:8 -msgid "Username:" -msgstr "Корисник:" +#: contrib/admin/templates/admin/submit_line.html:5 +msgid "Save and add another" +msgstr "Сними и додади уште" -#: contrib/admin/templates/admin/login.html:20 -#: contrib/comments/templates/comments/form.html:8 -msgid "Password:" -msgstr "Лозинка:" +#: contrib/admin/templates/admin/submit_line.html:6 +msgid "Save and continue editing" +msgstr "Сними и продолжи со уредување" -#: contrib/admin/templates/admin/base.html:25 -msgid "Welcome," -msgstr "Добредојдовте," +#: contrib/admin/templates/admin/submit_line.html:7 +msgid "Save" +msgstr "Сними" #: contrib/admin/templates/admin/auth/user/add_form.html:6 msgid "" @@ -1399,21 +555,21 @@ msgid "Username" msgstr "Корисник" #: contrib/admin/templates/admin/auth/user/add_form.html:18 -#: contrib/admin/templates/admin/auth/user/change_password.html:34 +#: contrib/admin/templates/admin/auth/user/change_password.html:33 msgid "Password" msgstr "Лозинка" #: contrib/admin/templates/admin/auth/user/add_form.html:23 -#: contrib/admin/templates/admin/auth/user/change_password.html:39 +#: contrib/admin/templates/admin/auth/user/change_password.html:38 msgid "Password (again)" msgstr "Лозинка (повторно)" #: contrib/admin/templates/admin/auth/user/add_form.html:24 -#: contrib/admin/templates/admin/auth/user/change_password.html:40 +#: contrib/admin/templates/admin/auth/user/change_password.html:39 msgid "Enter the same password as above, for verification." msgstr "Заради верификација внесете ја истата лозинка како и горе." -#: contrib/admin/templates/admin/auth/user/change_password.html:28 +#: contrib/admin/templates/admin/auth/user/change_password.html:27 #, python-format msgid "Enter a new password for the user <strong>%(username)s</strong>." msgstr "Внесете нова лозинка за корисникот <strong>%(username)s</strong>." @@ -1422,11 +578,11 @@ msgstr "Внесете нова лозинка за корисникот <strong msgid "Bookmarklets" msgstr "Обележувачи" -#: contrib/admin/templates/admin_doc/bookmarklets.html:5 +#: contrib/admin/templates/admin_doc/bookmarklets.html:4 msgid "Documentation bookmarklets" msgstr "Обележувачи на документација" -#: contrib/admin/templates/admin_doc/bookmarklets.html:9 +#: contrib/admin/templates/admin_doc/bookmarklets.html:8 msgid "" "\n" "<p class=\"help\">To install bookmarklets, drag the link to your bookmarks\n" @@ -1447,11 +603,11 @@ msgstr "" "сајтот од компјутер кој е означен како „внатрешен“ (разговарајте со вашиот \n" "администратор ако не сте сигурни).</p>\n" -#: contrib/admin/templates/admin_doc/bookmarklets.html:19 +#: contrib/admin/templates/admin_doc/bookmarklets.html:18 msgid "Documentation for this page" msgstr "Документација за оваа страница" -#: contrib/admin/templates/admin_doc/bookmarklets.html:20 +#: contrib/admin/templates/admin_doc/bookmarklets.html:19 msgid "" "Jumps you from any page to the documentation for the view that generates " "that page." @@ -1459,11 +615,11 @@ msgstr "" "Ве носи од било која страница од документацијата до погледот кој ја генерира " "таа страница." -#: contrib/admin/templates/admin_doc/bookmarklets.html:22 +#: contrib/admin/templates/admin_doc/bookmarklets.html:21 msgid "Show object ID" msgstr "Прикажи идентификационен број на објектот" -#: contrib/admin/templates/admin_doc/bookmarklets.html:23 +#: contrib/admin/templates/admin_doc/bookmarklets.html:22 msgid "" "Shows the content-type and unique ID for pages that represent a single " "object." @@ -1471,60 +627,89 @@ msgstr "" "Го прикажува типот на содржината и уникатниот идентификационен број за " "страници кои претставуваат единечен објект." -#: contrib/admin/templates/admin_doc/bookmarklets.html:25 +#: contrib/admin/templates/admin_doc/bookmarklets.html:24 msgid "Edit this object (current window)" msgstr "Уреди го овој објект (во овој прозорец)" -#: contrib/admin/templates/admin_doc/bookmarklets.html:26 +#: contrib/admin/templates/admin_doc/bookmarklets.html:25 msgid "Jumps to the admin page for pages that represent a single object." msgstr "Скокнува до админ страницата за страници кои претставуваат единечен објект." -#: contrib/admin/templates/admin_doc/bookmarklets.html:28 +#: contrib/admin/templates/admin_doc/bookmarklets.html:27 msgid "Edit this object (new window)" msgstr "Уреди го овој објект (во нов прозорец)" -#: contrib/admin/templates/admin_doc/bookmarklets.html:29 +#: contrib/admin/templates/admin_doc/bookmarklets.html:28 msgid "As above, but opens the admin page in a new window." msgstr "Како погоре, но ја отвара админ страницата во нов прозорец." -#: contrib/admin/templates/widget/date_time.html:3 -msgid "Date:" -msgstr "Датум:" +#: contrib/admin/templates/registration/logged_out.html:8 +msgid "Thanks for spending some quality time with the Web site today." +msgstr "Ви благодариме што денеска поминавте квалитетно време со интернет страницава." -#: contrib/admin/templates/widget/date_time.html:4 -msgid "Time:" -msgstr "Време:" +#: contrib/admin/templates/registration/logged_out.html:10 +msgid "Log in again" +msgstr "Логирајте се повторно" -#: contrib/admin/templates/widget/file.html:2 -msgid "Currently:" -msgstr "Моментално:" +#: contrib/admin/templates/registration/password_change_done.html:3 +#: contrib/admin/templates/registration/password_change_form.html:3 +#: contrib/admin/templates/registration/password_change_form.html:5 +#: contrib/admin/templates/registration/password_change_form.html:9 +msgid "Password change" +msgstr "Измена на лозинка" -#: contrib/admin/templates/widget/file.html:3 -msgid "Change:" -msgstr "Измена:" +#: contrib/admin/templates/registration/password_change_done.html:5 +#: contrib/admin/templates/registration/password_change_done.html:9 +msgid "Password change successful" +msgstr "Успешна промена на лозинката" + +#: contrib/admin/templates/registration/password_change_done.html:11 +msgid "Your password was changed." +msgstr "Вашата лозинка беше сменета." + +#: contrib/admin/templates/registration/password_change_form.html:11 +msgid "" +"Please enter your old password, for security's sake, and then enter your new " +"password twice so we can verify you typed it in correctly." +msgstr "" +"Заради сигурност ве молам внесете ја вашата стара лозинка и потоа внесете ја " +"новата двапати за да може да се потврди дека правилно сте ја искуцале." + +#: contrib/admin/templates/registration/password_change_form.html:16 +msgid "Old password:" +msgstr "Стара лозинка:" + +#: contrib/admin/templates/registration/password_change_form.html:18 +msgid "New password:" +msgstr "Нова лозинка:" + +#: contrib/admin/templates/registration/password_change_form.html:20 +msgid "Confirm password:" +msgstr "Потврди лозинка:" + +#: contrib/admin/templates/registration/password_change_form.html:22 +msgid "Change my password" +msgstr "Промени ја мојата лозинка" +#: contrib/admin/templates/registration/password_reset_done.html:4 #: contrib/admin/templates/registration/password_reset_form.html:4 #: contrib/admin/templates/registration/password_reset_form.html:6 #: contrib/admin/templates/registration/password_reset_form.html:10 -#: contrib/admin/templates/registration/password_reset_done.html:4 msgid "Password reset" msgstr "Ресетирање на лозинка" -#: contrib/admin/templates/registration/password_reset_form.html:12 +#: contrib/admin/templates/registration/password_reset_done.html:6 +#: contrib/admin/templates/registration/password_reset_done.html:10 +msgid "Password reset successful" +msgstr "Успешно е ресетирањето на лозинката" + +#: contrib/admin/templates/registration/password_reset_done.html:12 msgid "" -"Forgotten your password? Enter your e-mail address below, and we'll reset " -"your password and e-mail the new one to you." +"We've e-mailed a new password to the e-mail address you submitted. You " +"should be receiving it shortly." msgstr "" -"Сте ја заборавиле вашата лозинка? Внесете ја вашата е-пошта подолу, ќе ја " -"ресетираме вашата лозинка и новата ќе ви ја пратиме по е-пошта." - -#: contrib/admin/templates/registration/password_reset_form.html:16 -msgid "E-mail address:" -msgstr "Е-пошта:" - -#: contrib/admin/templates/registration/password_reset_form.html:16 -msgid "Reset my password" -msgstr "Ресетирај ја мојата лозинка" +"Ви пративме нова лозинка на адресата која ја внесовте.Треба да ја примите за " +"кратко време." #: contrib/admin/templates/registration/password_reset_email.html:2 msgid "You're receiving this e-mail because you requested a password reset" @@ -1557,298 +742,579 @@ msgstr "Ви благодариме што го користите овој са msgid "The %(site_name)s team" msgstr "Тимот на %(site_name)s" -#: contrib/admin/templates/registration/logged_out.html:8 -msgid "Thanks for spending some quality time with the Web site today." -msgstr "Ви благодариме што денеска поминавте квалитетно време со интернет страницава." +#: contrib/admin/templates/registration/password_reset_form.html:12 +msgid "" +"Forgotten your password? Enter your e-mail address below, and we'll reset " +"your password and e-mail the new one to you." +msgstr "" +"Сте ја заборавиле вашата лозинка? Внесете ја вашата е-пошта подолу, ќе ја " +"ресетираме вашата лозинка и новата ќе ви ја пратиме по е-пошта." -#: contrib/admin/templates/registration/logged_out.html:10 -msgid "Log in again" -msgstr "Логирајте се повторно" +#: contrib/admin/templates/registration/password_reset_form.html:16 +msgid "E-mail address:" +msgstr "Е-пошта:" -#: contrib/admin/templates/registration/password_reset_done.html:6 -#: contrib/admin/templates/registration/password_reset_done.html:10 -msgid "Password reset successful" -msgstr "Успешно е ресетирањето на лозинката" +#: contrib/admin/templates/registration/password_reset_form.html:16 +msgid "Reset my password" +msgstr "Ресетирај ја мојата лозинка" -#: contrib/admin/templates/registration/password_reset_done.html:12 +#: contrib/admin/templates/widget/date_time.html:3 +msgid "Date:" +msgstr "Датум:" + +#: contrib/admin/templates/widget/date_time.html:4 +msgid "Time:" +msgstr "Време:" + +#: contrib/admin/templates/widget/file.html:2 +msgid "Currently:" +msgstr "Моментално:" + +#: contrib/admin/templates/widget/file.html:3 +msgid "Change:" +msgstr "Измена:" + +#: contrib/admin/templatetags/admin_list.py:257 +msgid "All dates" +msgstr "Сите датуми" + +#: contrib/admin/views/auth.py:20 contrib/admin/views/main.py:267 +#, python-format +msgid "The %(name)s \"%(obj)s\" was added successfully." +msgstr "%(name)s \"%(obj)s\" беше успешно додаден." + +#: contrib/admin/views/auth.py:25 contrib/admin/views/main.py:271 +#: contrib/admin/views/main.py:356 +msgid "You may edit it again below." +msgstr "Подолу можете повторно да го уредите." + +#: contrib/admin/views/auth.py:31 +msgid "Add user" +msgstr "Додади корисник" + +#: contrib/admin/views/auth.py:58 +msgid "Password changed successfully." +msgstr "Успешна промена на лозинката." + +#: contrib/admin/views/auth.py:65 +#, python-format +msgid "Change password: %s" +msgstr "Промени лозинка: %s" + +#: contrib/admin/views/decorators.py:17 contrib/auth/forms.py:60 msgid "" -"We've e-mailed a new password to the e-mail address you submitted. You " -"should be receiving it shortly." +"Please enter a correct username and password. Note that both fields are case-" +"sensitive." msgstr "" -"Ви пративме нова лозинка на адресата која ја внесовте.Треба да ја примите за " -"кратко време." +"Ве молам внесете точно корисничко име и лозинка. Имајте на ум дека и во " +"двете полиња се битни големите и малите букви." -#: contrib/admin/templates/registration/password_change_form.html:4 -#: contrib/admin/templates/registration/password_change_form.html:6 -#: contrib/admin/templates/registration/password_change_form.html:10 -#: contrib/admin/templates/registration/password_change_done.html:4 -msgid "Password change" -msgstr "Измена на лозинка" +#: contrib/admin/views/decorators.py:69 +msgid "" +"Please log in again, because your session has expired. Don't worry: Your " +"submission has been saved." +msgstr "" +"Ве молам најавете се повторно бидејќи вашата сесија е истечена. Не се " +"грижете. Вашите внесови беа зачувани." -#: contrib/admin/templates/registration/password_change_form.html:12 +#: contrib/admin/views/decorators.py:76 msgid "" -"Please enter your old password, for security's sake, and then enter your new " -"password twice so we can verify you typed it in correctly." +"Looks like your browser isn't configured to accept cookies. Please enable " +"cookies, reload this page, and try again." msgstr "" -"Заради сигурност ве молам внесете ја вашата стара лозинка и потоа внесете ја " -"новата двапати за да може да се потврди дека правилно сте ја искуцале." +"Изгледа дека вашиот прелистувач не е конфигуриран да прифаќа колачиња. Ве " +"молам овозможете ги колачињата, превчитајте ја страта и пробајте повторно." -#: contrib/admin/templates/registration/password_change_form.html:17 -msgid "Old password:" -msgstr "Стара лозинка:" +#: contrib/admin/views/decorators.py:89 +#, python-format +msgid "Your e-mail address is not your username. Try '%s' instead." +msgstr "Вашата е-пошта не е вашето корисничко име. Пробајте со „%s“." -#: contrib/admin/templates/registration/password_change_form.html:19 -msgid "New password:" -msgstr "Нова лозинка:" +#: contrib/admin/views/decorators.py:93 +msgid "Usernames cannot contain the '@' character." +msgstr "Корисничките имиња неможе да го содржат „@“ знакот." -#: contrib/admin/templates/registration/password_change_form.html:21 -msgid "Confirm password:" -msgstr "Потврди лозинка:" +#: contrib/admin/views/doc.py:48 contrib/admin/views/doc.py:50 +#: contrib/admin/views/doc.py:52 +msgid "tag:" +msgstr "таг:" -#: contrib/admin/templates/registration/password_change_form.html:23 -msgid "Change my password" -msgstr "Промени ја мојата лозинка" +#: contrib/admin/views/doc.py:79 contrib/admin/views/doc.py:81 +#: contrib/admin/views/doc.py:83 +msgid "filter:" +msgstr "филтер:" -#: contrib/admin/templates/registration/password_change_done.html:6 -#: contrib/admin/templates/registration/password_change_done.html:10 -msgid "Password change successful" -msgstr "Успешна промена на лозинката" +#: contrib/admin/views/doc.py:137 contrib/admin/views/doc.py:139 +#: contrib/admin/views/doc.py:141 +msgid "view:" +msgstr "поглед:" -#: contrib/admin/templates/registration/password_change_done.html:12 -msgid "Your password was changed." -msgstr "Вашата лозинка беше сменета." +#: contrib/admin/views/doc.py:166 +#, python-format +msgid "App %r not found" +msgstr "Не е најдена апликацијата %r" -#: contrib/sites/models.py:10 -msgid "domain name" -msgstr "домејн" +#: contrib/admin/views/doc.py:173 +#, python-format +msgid "Model %(name)r not found in app %(label)r" +msgstr "Моделот %(name)r не е најден во апликацијата %(label)r" -#: contrib/sites/models.py:11 -msgid "display name" -msgstr "име кое се прикажува" +#: contrib/admin/views/doc.py:185 +#, python-format +msgid "the related `%(label)s.%(type)s` object" +msgstr "поврзаниот`%(label)s.%(type)s` објект" -#: contrib/sites/models.py:15 -msgid "site" -msgstr "сајт" +#: contrib/admin/views/doc.py:185 contrib/admin/views/doc.py:207 +#: contrib/admin/views/doc.py:221 contrib/admin/views/doc.py:226 +msgid "model:" +msgstr "модел:" -#: contrib/sites/models.py:16 -msgid "sites" -msgstr "сајтови" +#: contrib/admin/views/doc.py:216 +#, python-format +msgid "related `%(label)s.%(name)s` objects" +msgstr "поврзани `%(label)s.%(name)s` објекти" -#: contrib/humanize/templatetags/humanize.py:17 -msgid "th" -msgstr "th" +#: contrib/admin/views/doc.py:221 +#, python-format +msgid "all %s" +msgstr "сите %s" -#: contrib/humanize/templatetags/humanize.py:17 -msgid "st" -msgstr "st" +#: contrib/admin/views/doc.py:226 +#, python-format +msgid "number of %s" +msgstr "број на %s" -#: contrib/humanize/templatetags/humanize.py:17 -msgid "nd" -msgstr "nd" +#: contrib/admin/views/doc.py:231 +#, python-format +msgid "Fields on %s objects" +msgstr "Полиња на %s објекти" -#: contrib/humanize/templatetags/humanize.py:17 -msgid "rd" -msgstr "rd" +#: contrib/admin/views/doc.py:293 contrib/admin/views/doc.py:304 +#: contrib/admin/views/doc.py:306 contrib/admin/views/doc.py:312 +#: contrib/admin/views/doc.py:313 contrib/admin/views/doc.py:315 +msgid "Integer" +msgstr "Цел број" -#: contrib/humanize/templatetags/humanize.py:47 +#: contrib/admin/views/doc.py:294 +msgid "Boolean (Either True or False)" +msgstr "Логичка (или точно или неточно)" + +#: contrib/admin/views/doc.py:295 contrib/admin/views/doc.py:314 #, python-format -msgid "%(value).1f million" -msgid_plural "%(value).1f million" -msgstr[0] "%(value).1f милион" -msgstr[1] "%(value).1f милион" +msgid "String (up to %(max_length)s)" +msgstr "Збор (до %(max_length)s)" + +#: contrib/admin/views/doc.py:296 +msgid "Comma-separated integers" +msgstr "Целобројни вредности одделени со запирка" -#: contrib/humanize/templatetags/humanize.py:50 +#: contrib/admin/views/doc.py:297 +msgid "Date (without time)" +msgstr "Датум (без час)" + +#: contrib/admin/views/doc.py:298 +msgid "Date (with time)" +msgstr "Датум (со час)" + +#: contrib/admin/views/doc.py:299 +msgid "Decimal number" +msgstr "Децимален број" + +#: contrib/admin/views/doc.py:300 +msgid "E-mail address" +msgstr "Адреса на е-пошта" + +#: contrib/admin/views/doc.py:301 contrib/admin/views/doc.py:302 +#: contrib/admin/views/doc.py:305 +msgid "File path" +msgstr "Патека на датотека" + +#: contrib/admin/views/doc.py:303 +msgid "Floating point number" +msgstr "Децимален број" + +#: contrib/admin/views/doc.py:307 contrib/comments/models.py:89 +msgid "IP address" +msgstr "ИП адреса" + +#: contrib/admin/views/doc.py:309 +msgid "Boolean (Either True, False or None)" +msgstr "Логичка (точно,неточно или празно)" + +#: contrib/admin/views/doc.py:310 +msgid "Relation to parent model" +msgstr "Релација со родителскиот модел" + +#: contrib/admin/views/doc.py:311 +msgid "Phone number" +msgstr "Телефонски број" + +#: contrib/admin/views/doc.py:316 +msgid "Text" +msgstr "Текст" + +#: contrib/admin/views/doc.py:317 +msgid "Time" +msgstr "Час" + +#: contrib/admin/views/doc.py:318 contrib/flatpages/models.py:8 +msgid "URL" +msgstr "URL" + +#: contrib/admin/views/doc.py:319 +msgid "U.S. state (two uppercase letters)" +msgstr "Држава во САД (две големи букви)" + +#: contrib/admin/views/doc.py:320 +msgid "XML text" +msgstr "XML текст" + +#: contrib/admin/views/doc.py:346 #, python-format -msgid "%(value).1f billion" -msgid_plural "%(value).1f billion" -msgstr[0] "%(value).1f милијарда" -msgstr[1] "%(value).1f милијарда" +msgid "%s does not appear to be a urlpattern object" +msgstr "%s не изгледа дека е url објект" -#: contrib/humanize/templatetags/humanize.py:53 +#: contrib/admin/views/main.py:233 +msgid "Site administration" +msgstr "Администрација на сајт" + +#: contrib/admin/views/main.py:280 contrib/admin/views/main.py:365 #, python-format -msgid "%(value).1f trillion" -msgid_plural "%(value).1f trillion" -msgstr[0] "%(value).1f трилион" -msgstr[1] "%(value).1f трилион" +msgid "You may add another %s below." +msgstr "Подолу можете да додате уште еден %s." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "one" -msgstr "еден" +#: contrib/admin/views/main.py:298 +#, python-format +msgid "Add %s" +msgstr "Додади %s" -#: contrib/humanize/templatetags/humanize.py:68 -msgid "two" -msgstr "два" +#: contrib/admin/views/main.py:344 +#, python-format +msgid "Added %s." +msgstr "Додадено %s." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "three" -msgstr "три" +#: contrib/admin/views/main.py:344 contrib/admin/views/main.py:346 +#: contrib/admin/views/main.py:348 core/validators.py:283 +#: db/models/manipulators.py:309 +msgid "and" +msgstr "и" -#: contrib/humanize/templatetags/humanize.py:68 -msgid "four" -msgstr "четири" +#: contrib/admin/views/main.py:346 +#, python-format +msgid "Changed %s." +msgstr "Изменета %s." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "five" -msgstr "пет" +#: contrib/admin/views/main.py:348 +#, python-format +msgid "Deleted %s." +msgstr "Избришана %s." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "six" -msgstr "шест" +#: contrib/admin/views/main.py:351 +msgid "No fields changed." +msgstr "Не беше изменето ниедно поле." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "seven" -msgstr "седум" +#: contrib/admin/views/main.py:354 +#, python-format +msgid "The %(name)s \"%(obj)s\" was changed successfully." +msgstr "%(name)s \"%(obj)s\" беше успешно изменета." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "eight" -msgstr "осум" +#: contrib/admin/views/main.py:362 +#, python-format +msgid "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." +msgstr "" +"%(name)s \"%(obj)s\" беше успешно додадена.Подолу можете повторно да ја " +"уредите." -#: contrib/humanize/templatetags/humanize.py:68 -msgid "nine" -msgstr "девет" +#: contrib/admin/views/main.py:400 +#, python-format +msgid "Change %s" +msgstr "Измени %s" -#: contrib/flatpages/models.py:8 -msgid "Example: '/about/contact/'. Make sure to have leading and trailing slashes." +#: contrib/admin/views/main.py:487 +#, python-format +msgid "One or more %(fieldname)s in %(name)s: %(obj)s" +msgstr "Еден или повеќе %(fieldname)s во %(name)s: %(obj)s" + +#: contrib/admin/views/main.py:492 +#, python-format +msgid "One or more %(fieldname)s in %(name)s:" +msgstr "Еден или повеќе %(fieldname)s во %(name)s:" + +#: contrib/admin/views/main.py:524 +#, python-format +msgid "The %(name)s \"%(obj)s\" was deleted successfully." +msgstr "%(name)s \"%(obj)s\" беше избришана успешно." + +#: contrib/admin/views/main.py:527 +msgid "Are you sure?" +msgstr "Сигурни сте?" + +#: contrib/admin/views/main.py:549 +#, python-format +msgid "Change history: %s" +msgstr "Историја на измени: %s" + +#: contrib/admin/views/main.py:583 +#, python-format +msgid "Select %s" +msgstr "Изберет %s" + +#: contrib/admin/views/main.py:583 +#, python-format +msgid "Select %s to change" +msgstr "Изберете %s за измена" + +#: contrib/admin/views/main.py:765 +msgid "Database error" +msgstr "Грешка во базата со податоци" + +#: contrib/auth/forms.py:17 contrib/auth/forms.py:138 +msgid "The two password fields didn't match." +msgstr "Двете полиња со лозинките не се совпаѓаат." + +#: contrib/auth/forms.py:25 +msgid "A user with that username already exists." +msgstr "Веќе постои корисник со тоа корисничко име." + +#: contrib/auth/forms.py:53 +msgid "" +"Your Web browser doesn't appear to have cookies enabled. Cookies are " +"required for logging in." msgstr "" -"На пр. „/za/kontakt/“. Осигурајте се да имате коса црта и на крајот и на " -"почетокот." +"Не изгледа дека вашиот прелистувач има овозможено колачиња. Колачињата се " +"потребни за да се најавите." -#: contrib/flatpages/models.py:9 -msgid "title" -msgstr "наслов" +#: contrib/auth/forms.py:62 +msgid "This account is inactive." +msgstr "Оваа сметка е неактивна." -#: contrib/flatpages/models.py:10 -msgid "content" -msgstr "содржина" +#: contrib/auth/forms.py:84 +msgid "" +"That e-mail address doesn't have an associated user account. Are you sure " +"you've registered?" +msgstr "" +"Нема регистрирано корисник со оваа адреса за е-пошта. Сигурни ли сте дека " +"сте регистрирани?" -#: contrib/flatpages/models.py:11 -msgid "enable comments" -msgstr "овозможи коментари" +#: contrib/auth/forms.py:107 +#, python-format +msgid "Password reset on %s" +msgstr "Ресетирање на лозинка на %s" -#: contrib/flatpages/models.py:12 -msgid "template name" -msgstr "име на шаблонот" +#: contrib/auth/forms.py:117 +msgid "The two 'new password' fields didn't match." +msgstr "Двете нови лозинки не се совпаѓаат." -#: contrib/flatpages/models.py:13 +#: contrib/auth/forms.py:124 +msgid "Your old password was entered incorrectly. Please enter it again." +msgstr "Не ја внесовте точно вашата стара лозинка. Ве молам внесете ја повторно." + +#: contrib/auth/models.py:73 contrib/auth/models.py:93 +msgid "name" +msgstr "име" + +#: contrib/auth/models.py:75 +msgid "codename" +msgstr "кодно име" + +#: contrib/auth/models.py:78 +msgid "permission" +msgstr "привилегија" + +#: contrib/auth/models.py:79 contrib/auth/models.py:94 +msgid "permissions" +msgstr "привилегии" + +#: contrib/auth/models.py:97 +msgid "group" +msgstr "група" + +#: contrib/auth/models.py:98 contrib/auth/models.py:148 +msgid "groups" +msgstr "групи" + +#: contrib/auth/models.py:138 +msgid "username" +msgstr "корисничко име" + +#: contrib/auth/models.py:138 msgid "" -"Example: 'flatpages/contact_page.html'. If this isn't provided, the system " -"will use 'flatpages/default.html'." +"Required. 30 characters or fewer. Alphanumeric characters only (letters, " +"digits and underscores)." msgstr "" -"На пр. „flatpages/kontakt.html'. Ако не го внесете ова, системот ќе користи " -"„flatpages/default.html“." +"Дозволени се најмногу 30 знаци. Дозволени се само алфанумерички знаци " +"(букви, цифри и долна црта)." -#: contrib/flatpages/models.py:14 -msgid "registration required" -msgstr "потребна е регистрација" +#: contrib/auth/models.py:139 +msgid "first name" +msgstr "име" -#: contrib/flatpages/models.py:14 -msgid "If this is checked, only logged-in users will be able to view the page." +#: contrib/auth/models.py:140 +msgid "last name" +msgstr "презиме" + +#: contrib/auth/models.py:141 +msgid "e-mail address" +msgstr "е-пошта" + +#: contrib/auth/models.py:142 +msgid "password" +msgstr "лозинка" + +#: contrib/auth/models.py:142 +msgid "" +"Use '[algo]$[salt]$[hexdigest]' or use the <a href=\"password/\">change " +"password form</a>." msgstr "" -"Ако ова е штиклирано, само најавените корисници ќе можат да ја гледаат оваа " -"страница." +"Користете '[algo]$[salt]$[hexdigest]' или користете ја <a href=\"password/" +"\">формата за промена на лозинката</a>." -#: contrib/flatpages/models.py:18 -msgid "flat page" -msgstr "статична страница" +#: contrib/auth/models.py:143 +msgid "staff status" +msgstr "статус на администраторите" -#: contrib/flatpages/models.py:19 -msgid "flat pages" -msgstr "статични страници" +#: contrib/auth/models.py:143 +msgid "Designates whether the user can log into this admin site." +msgstr "Означува дали корисникот може да се логира во сајтот за администрација." -#: contrib/redirects/models.py:7 -msgid "redirect from" -msgstr "пренасочено од" +#: contrib/auth/models.py:144 +msgid "active" +msgstr "активен" -#: contrib/redirects/models.py:8 +#: contrib/auth/models.py:144 msgid "" -"This should be an absolute path, excluding the domain name. Example: '/" -"events/search/'." +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." msgstr "" -"Ова треба да биде апсолутна патека без името на домејнот. На пр. „/nastani/" -"prebaraj/“." +"Означува дали корисникот треба да биде активен. Одштиклирајте го ова наместо да " +"бришете корисници." -#: contrib/redirects/models.py:9 -msgid "redirect to" -msgstr "пренасочи кон" +#: contrib/auth/models.py:145 +msgid "superuser status" +msgstr "статус на суперкорисник" -#: contrib/redirects/models.py:10 +#: contrib/auth/models.py:145 msgid "" -"This can be either an absolute path (as above) or a full URL starting with " -"'http://'." +"Designates that this user has all permissions without explicitly assigning " +"them." msgstr "" -"Ова може да биде или апсолутна патека (како погоре) или цела адреса " -"почувајќи со „http://“." +"Означува дека овој корисник ги има сите привилегии без експлицитно да се " +"доделуваат сите." -#: contrib/redirects/models.py:13 -msgid "redirect" -msgstr "пренасочување" +#: contrib/auth/models.py:146 +msgid "last login" +msgstr "последна најава" -#: contrib/redirects/models.py:14 -msgid "redirects" -msgstr "пренасочувања" +#: contrib/auth/models.py:147 +msgid "date joined" +msgstr "датум на зачленување" + +#: 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." +msgstr "" +"Како дополнување на рачно доделени привилегии, овој корисник ќе ги добие " +"автоматски и сите привилегии за секоја група во која тој/таа членува." + +#: contrib/auth/models.py:150 +msgid "user permissions" +msgstr "кориснички привилегии" + +#: contrib/auth/models.py:154 +msgid "user" +msgstr "корисник" + +#: contrib/auth/models.py:155 +msgid "users" +msgstr "корисници" + +#: contrib/auth/models.py:161 +msgid "Personal info" +msgstr "Лични информации" -#: contrib/comments/models.py:67 contrib/comments/models.py:166 +#: contrib/auth/models.py:162 +msgid "Permissions" +msgstr "Привилегии" + +#: contrib/auth/models.py:163 +msgid "Important dates" +msgstr "Важни датуми" + +#: contrib/auth/models.py:164 +msgid "Groups" +msgstr "Групи" + +#: contrib/auth/models.py:323 +msgid "message" +msgstr "порака" + +#: contrib/auth/views.py:47 +msgid "Logged out" +msgstr "Одјавен" + +#: contrib/comments/models.py:71 contrib/comments/models.py:176 msgid "object ID" msgstr "object ID" -#: contrib/comments/models.py:68 +#: contrib/comments/models.py:72 msgid "headline" msgstr "наслов" -#: contrib/comments/models.py:69 contrib/comments/models.py:90 -#: contrib/comments/models.py:167 +#: contrib/comments/models.py:73 contrib/comments/models.py:95 +#: contrib/comments/models.py:177 msgid "comment" msgstr "коментар" -#: contrib/comments/models.py:70 +#: contrib/comments/models.py:74 msgid "rating #1" msgstr "популарност #1" -#: contrib/comments/models.py:71 +#: contrib/comments/models.py:75 msgid "rating #2" msgstr "популарност #2" -#: contrib/comments/models.py:72 +#: contrib/comments/models.py:76 msgid "rating #3" msgstr "популарност #3" -#: contrib/comments/models.py:73 +#: contrib/comments/models.py:77 msgid "rating #4" msgstr "популарност #4" -#: contrib/comments/models.py:74 +#: contrib/comments/models.py:78 msgid "rating #5" msgstr "популарност #5" -#: contrib/comments/models.py:75 +#: contrib/comments/models.py:79 msgid "rating #6" msgstr "популарност #6" -#: contrib/comments/models.py:76 +#: contrib/comments/models.py:80 msgid "rating #7" msgstr "популарност #7" -#: contrib/comments/models.py:77 +#: contrib/comments/models.py:81 msgid "rating #8" msgstr "популарност #8" -#: contrib/comments/models.py:82 +#: contrib/comments/models.py:86 msgid "is valid rating" msgstr "е валидна популарност" -#: contrib/comments/models.py:83 contrib/comments/models.py:169 +#: contrib/comments/models.py:87 contrib/comments/models.py:179 msgid "date/time submitted" msgstr "датум/време пријавен" -#: contrib/comments/models.py:84 contrib/comments/models.py:170 +#: contrib/comments/models.py:88 contrib/comments/models.py:180 msgid "is public" msgstr "е јавен" -#: contrib/comments/models.py:86 +#: contrib/comments/models.py:90 msgid "is removed" msgstr "е отстранет" -#: contrib/comments/models.py:86 +#: contrib/comments/models.py:90 msgid "" "Check this box if the comment is inappropriate. A \"This comment has been " "removed\" message will be displayed instead." @@ -1856,15 +1322,15 @@ msgstr "" "Штиклирајте го ова поле ако коментарот не е пригоден. Наместо него пораката " "„Овој коментар беше отстранет“ ќе биде прикажана." -#: contrib/comments/models.py:91 +#: contrib/comments/models.py:96 msgid "comments" msgstr "коментари" -#: contrib/comments/models.py:131 contrib/comments/models.py:207 +#: contrib/comments/models.py:140 contrib/comments/models.py:222 msgid "Content object" msgstr "Content објект" -#: contrib/comments/models.py:159 +#: contrib/comments/models.py:168 #, python-format msgid "" "Posted by %(user)s at %(date)s\n" @@ -1879,48 +1345,48 @@ msgstr "" "\n" "http://%(domain)s%(url)s" -#: contrib/comments/models.py:168 +#: contrib/comments/models.py:178 msgid "person's name" msgstr "име на личноста" -#: contrib/comments/models.py:171 +#: contrib/comments/models.py:181 msgid "ip address" msgstr "ип адреса" -#: contrib/comments/models.py:173 +#: contrib/comments/models.py:183 msgid "approved by staff" msgstr "одобрено од администраторите" -#: contrib/comments/models.py:176 +#: contrib/comments/models.py:187 msgid "free comment" msgstr "слободен коментар" -#: contrib/comments/models.py:177 +#: contrib/comments/models.py:188 msgid "free comments" msgstr "слободни коментари" -#: contrib/comments/models.py:233 +#: contrib/comments/models.py:250 msgid "score" msgstr "поени" -#: contrib/comments/models.py:234 +#: contrib/comments/models.py:251 msgid "score date" msgstr "датум поени" -#: contrib/comments/models.py:237 +#: contrib/comments/models.py:255 msgid "karma score" msgstr "карма поен" -#: contrib/comments/models.py:238 +#: contrib/comments/models.py:256 msgid "karma scores" msgstr "карма поени" -#: contrib/comments/models.py:242 +#: contrib/comments/models.py:260 #, python-format msgid "%(score)d rating by %(user)s" msgstr "%(score)d гласање за популарност од %(user)s" -#: contrib/comments/models.py:258 +#: contrib/comments/models.py:277 #, python-format msgid "" "This comment was flagged by %(user)s:\n" @@ -1931,59 +1397,83 @@ msgstr "" "\n" "%(text)s" -#: contrib/comments/models.py:265 +#: contrib/comments/models.py:285 msgid "flag date" msgstr "датум на означување" -#: contrib/comments/models.py:268 +#: contrib/comments/models.py:289 msgid "user flag" msgstr "корисничка ознака" -#: contrib/comments/models.py:269 +#: contrib/comments/models.py:290 msgid "user flags" msgstr "кориснички ознаки" -#: contrib/comments/models.py:273 +#: contrib/comments/models.py:294 #, python-format msgid "Flag by %r" msgstr "Означено од %r" -#: contrib/comments/models.py:278 +#: contrib/comments/models.py:300 msgid "deletion date" msgstr "датум на бришење" -#: contrib/comments/models.py:280 +#: contrib/comments/models.py:303 msgid "moderator deletion" msgstr "бришење од модератор" -#: contrib/comments/models.py:281 +#: contrib/comments/models.py:304 msgid "moderator deletions" msgstr "бришења од модератор" -#: contrib/comments/models.py:285 +#: contrib/comments/models.py:308 #, python-format msgid "Moderator deletion by %r" msgstr "Бришење од модератор од %r" -#: contrib/comments/views/karma.py:19 -msgid "Anonymous users cannot vote" -msgstr "Анонимните корисници неможе да гласаат" +#: contrib/comments/templates/comments/form.html:8 +msgid "Forgotten your password?" +msgstr "Ја заборавите вашата лозинка?" -#: contrib/comments/views/karma.py:23 -msgid "Invalid comment ID" -msgstr "Невалидно ИД на коментарот" +#: contrib/comments/templates/comments/form.html:12 +msgid "Ratings" +msgstr "Популарност" -#: contrib/comments/views/karma.py:25 -msgid "No voting for yourself" -msgstr "Нема гласање за самиот себе" +#: contrib/comments/templates/comments/form.html:12 +#: contrib/comments/templates/comments/form.html:23 +msgid "Required" +msgstr "Потребно" + +#: contrib/comments/templates/comments/form.html:12 +#: contrib/comments/templates/comments/form.html:23 +msgid "Optional" +msgstr "По желба" + +#: contrib/comments/templates/comments/form.html:23 +msgid "Post a photo" +msgstr "Објави фотографија" + +#: contrib/comments/templates/comments/form.html:28 +#: contrib/comments/templates/comments/freeform.html:5 +msgid "Comment:" +msgstr "Коментар:" + +#: contrib/comments/templates/comments/form.html:35 +#: contrib/comments/templates/comments/freeform.html:10 +msgid "Preview comment" +msgstr "Прегледај" -#: contrib/comments/views/comments.py:27 +#: contrib/comments/templates/comments/freeform.html:4 +msgid "Your name:" +msgstr "Вашето име:" + +#: contrib/comments/views/comments.py:28 msgid "This rating is required because you've entered at least one other rating." msgstr "" "Ова гласање за популарност е потребно бидејќи внесовте најмалку уште едно " "друго." -#: contrib/comments/views/comments.py:111 +#: contrib/comments/views/comments.py:112 #, python-format msgid "" "This comment was posted by a user who has posted fewer than %(count)s " @@ -2006,7 +1496,7 @@ msgstr[1] "" "\n" "%(text)s" -#: contrib/comments/views/comments.py:116 +#: contrib/comments/views/comments.py:117 #, python-format msgid "" "This comment was posted by a sketchy user:\n" @@ -2017,23 +1507,23 @@ msgstr "" "\n" "%(text)s" -#: contrib/comments/views/comments.py:188 -#: contrib/comments/views/comments.py:280 +#: contrib/comments/views/comments.py:190 +#: contrib/comments/views/comments.py:283 msgid "Only POSTs are allowed" msgstr "Дозволено е само POST" -#: contrib/comments/views/comments.py:192 -#: contrib/comments/views/comments.py:284 +#: contrib/comments/views/comments.py:194 +#: contrib/comments/views/comments.py:287 msgid "One or more of the required fields wasn't submitted" msgstr "Едно или повеќе од потребните полиња не беше пополнето" -#: contrib/comments/views/comments.py:196 -#: contrib/comments/views/comments.py:286 +#: contrib/comments/views/comments.py:198 +#: contrib/comments/views/comments.py:289 msgid "Somebody tampered with the comment form (security violation)" msgstr "Некој ја променил формата за коментари (сигурносен прекршок)" -#: contrib/comments/views/comments.py:206 -#: contrib/comments/views/comments.py:292 +#: contrib/comments/views/comments.py:208 +#: contrib/comments/views/comments.py:295 msgid "" "The comment form had an invalid 'target' parameter -- the object ID was " "invalid" @@ -2041,65 +1531,363 @@ msgstr "" "Формата за коментар имаше неправилен „target“ параметар - идентификациониот " "број на објектот беше неправилен" -#: contrib/comments/views/comments.py:257 -#: contrib/comments/views/comments.py:321 +#: contrib/comments/views/comments.py:259 +#: contrib/comments/views/comments.py:324 msgid "The comment form didn't provide either 'preview' or 'post'" msgstr "Формата за коментар не овозможи ниту „преглед“ ниту „праќање“" -#: contrib/comments/templates/comments/form.html:8 -msgid "Forgotten your password?" -msgstr "Ја заборавите вашата лозинка?" +#: contrib/comments/views/karma.py:21 +msgid "Anonymous users cannot vote" +msgstr "Анонимните корисници неможе да гласаат" -#: contrib/comments/templates/comments/form.html:12 -msgid "Ratings" -msgstr "Популарност" +#: contrib/comments/views/karma.py:25 +msgid "Invalid comment ID" +msgstr "Невалидно ИД на коментарот" -#: contrib/comments/templates/comments/form.html:12 -#: contrib/comments/templates/comments/form.html:23 -msgid "Required" -msgstr "Потребно" +#: contrib/comments/views/karma.py:27 +msgid "No voting for yourself" +msgstr "Нема гласање за самиот себе" -#: contrib/comments/templates/comments/form.html:12 -#: contrib/comments/templates/comments/form.html:23 -msgid "Optional" -msgstr "По желба" +#: contrib/contenttypes/models.py:67 +msgid "python model class name" +msgstr "има на класата на питон моделите" -#: contrib/comments/templates/comments/form.html:23 -msgid "Post a photo" -msgstr "Објави фотографија" +#: contrib/contenttypes/models.py:71 +msgid "content type" +msgstr "content type" -#: contrib/comments/templates/comments/form.html:28 -#: contrib/comments/templates/comments/freeform.html:5 -msgid "Comment:" -msgstr "Коментар:" +#: contrib/contenttypes/models.py:72 +msgid "content types" +msgstr "content types" -#: contrib/comments/templates/comments/form.html:35 -#: contrib/comments/templates/comments/freeform.html:10 -msgid "Preview comment" -msgstr "Прегледај" +#: contrib/flatpages/models.py:9 +msgid "Example: '/about/contact/'. Make sure to have leading and trailing slashes." +msgstr "" +"На пр. „/za/kontakt/“. Осигурајте се да имате коса црта и на крајот и на " +"почетокот." -#: contrib/comments/templates/comments/freeform.html:4 -msgid "Your name:" -msgstr "Вашето име:" +#: contrib/flatpages/models.py:10 +msgid "title" +msgstr "наслов" + +#: contrib/flatpages/models.py:11 +msgid "content" +msgstr "содржина" + +#: contrib/flatpages/models.py:12 +msgid "enable comments" +msgstr "овозможи коментари" + +#: contrib/flatpages/models.py:13 +msgid "template name" +msgstr "име на шаблонот" + +#: contrib/flatpages/models.py:14 +msgid "" +"Example: 'flatpages/contact_page.html'. If this isn't provided, the system " +"will use 'flatpages/default.html'." +msgstr "" +"На пр. „flatpages/kontakt.html'. Ако не го внесете ова, системот ќе користи " +"„flatpages/default.html“." + +#: contrib/flatpages/models.py:15 +msgid "registration required" +msgstr "потребна е регистрација" + +#: contrib/flatpages/models.py:15 +msgid "If this is checked, only logged-in users will be able to view the page." +msgstr "" +"Ако ова е штиклирано, само најавените корисници ќе можат да ја гледаат оваа " +"страница." + +#: contrib/flatpages/models.py:20 +msgid "flat page" +msgstr "статична страница" + +#: contrib/flatpages/models.py:21 +msgid "flat pages" +msgstr "статични страници" + +#: contrib/flatpages/models.py:27 +msgid "Advanced options" +msgstr "Напредни можности" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "th" +msgstr "th" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "st" +msgstr "st" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "nd" +msgstr "nd" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "rd" +msgstr "rd" + +#: contrib/humanize/templatetags/humanize.py:51 +#, python-format +msgid "%(value).1f million" +msgid_plural "%(value).1f million" +msgstr[0] "%(value).1f милион" +msgstr[1] "%(value).1f милион" + +#: contrib/humanize/templatetags/humanize.py:54 +#, python-format +msgid "%(value).1f billion" +msgid_plural "%(value).1f billion" +msgstr[0] "%(value).1f милијарда" +msgstr[1] "%(value).1f милијарда" + +#: contrib/humanize/templatetags/humanize.py:57 +#, python-format +msgid "%(value).1f trillion" +msgid_plural "%(value).1f trillion" +msgstr[0] "%(value).1f трилион" +msgstr[1] "%(value).1f трилион" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "one" +msgstr "еден" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "two" +msgstr "два" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "three" +msgstr "три" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "four" +msgstr "четири" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "five" +msgstr "пет" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "six" +msgstr "шест" -#: contrib/localflavor/br/forms.py:18 +#: contrib/humanize/templatetags/humanize.py:73 +msgid "seven" +msgstr "седум" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "eight" +msgstr "осум" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "nine" +msgstr "девет" + +#: contrib/humanize/templatetags/humanize.py:93 +msgid "today" +msgstr "денеска" + +#: contrib/humanize/templatetags/humanize.py:95 +msgid "tomorrow" +msgstr "утре" + +#: contrib/humanize/templatetags/humanize.py:97 +msgid "yesterday" +msgstr "вчера" + +#: contrib/localflavor/ar/forms.py:27 +msgid "Enter a postal code in the format NNNN or ANNNNAAA." +msgstr "Внесете поштенски број во форматот NNNN или ANNNNAAA." + +#: contrib/localflavor/ar/forms.py:49 contrib/localflavor/br/forms.py:96 +#: contrib/localflavor/br/forms.py:135 contrib/localflavor/pe/forms.py:23 +#: contrib/localflavor/pe/forms.py:51 +msgid "This field requires only numbers." +msgstr "Во ова поле смее да бидат само бројки." + +#: contrib/localflavor/ar/forms.py:50 +msgid "This field requires 7 or 8 digits." +msgstr "Во ова поле смее да има 7 или 8 цифри." + +#: contrib/localflavor/ar/forms.py:79 +msgid "Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format." +msgstr "Внесете правилен CUIT во XX-XXXXXXXX-X or XXXXXXXXXXXX формат." + +#: contrib/localflavor/ar/forms.py:80 +msgid "Invalid CUIT." +msgstr "Неправилен CUIT." + +#: contrib/localflavor/au/forms.py:16 +msgid "Enter a 4 digit post code." +msgstr "Внесете 4 цифрен поштенски број." + +#: contrib/localflavor/br/forms.py:21 msgid "Enter a zip code in the format XXXXX-XXX." -msgstr "Внесете правилен поштенски број во форматот XXXXX-XXX." +msgstr "Внесете правилен поштенски број во форматот XXXXX-XXX." #: contrib/localflavor/br/forms.py:30 msgid "Phone numbers must be in XX-XXXX-XXXX format." msgstr "Телефонските броеви мора да бидат во XX-XXXX-XXXX форматот." -#: contrib/localflavor/de/forms.py:16 contrib/localflavor/fi/forms.py:14 -#: contrib/localflavor/fr/forms.py:17 contrib/localflavor/it/forms.py:14 -msgid "Enter a zip code in the format XXXXX." -msgstr "Внесете правилен поштенски број во формат XXXXXX." +#: contrib/localflavor/br/forms.py:58 +msgid "" +"Select a valid brazilian state. That state is not one of the available " +"states." +msgstr "Изберете правилна бразилска држава. Оваа држава не е од достапните држави." + +#: contrib/localflavor/br/forms.py:94 +msgid "Invalid CPF number." +msgstr "Неправилен CPF број." + +#: contrib/localflavor/br/forms.py:95 +msgid "This field requires at most 11 digits or 14 characters." +msgstr "Во ова поле смее да има најмногу 11 цифри или 14 знаци." + +#: contrib/localflavor/br/forms.py:134 +msgid "Invalid CNPJ number." +msgstr "Неправилен CNPJ број." + +#: contrib/localflavor/br/forms.py:136 +msgid "This field requires at least 14 digits" +msgstr "Во ова поле треба да има најмалку 14 цифри" + +#: contrib/localflavor/ca/forms.py:17 +msgid "Enter a postal code in the format XXX XXX." +msgstr "Внесете правилен поштенски број во формат XXX XXX." + +#: contrib/localflavor/ca/forms.py:88 +msgid "Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format." +msgstr "Внесте правилен канадски број за осигурување во XXX-XXX-XXX форматот." + +#: contrib/localflavor/ch/ch_states.py:5 +msgid "Aargau" +msgstr "Aargau" + +#: contrib/localflavor/ch/ch_states.py:6 +msgid "Appenzell Innerrhoden" +msgstr "Appenzell Innerrhoden" + +#: contrib/localflavor/ch/ch_states.py:7 +msgid "Appenzell Ausserrhoden" +msgstr "Appenzell Ausserrhoden" + +#: contrib/localflavor/ch/ch_states.py:8 +msgid "Basel-Stadt" +msgstr "Basel-Stadt" + +#: contrib/localflavor/ch/ch_states.py:9 +msgid "Basel-Land" +msgstr "Basel-Land" + +#: contrib/localflavor/ch/ch_states.py:10 +msgid "Berne" +msgstr "Berne" + +#: contrib/localflavor/ch/ch_states.py:11 +msgid "Fribourg" +msgstr "Fribourg" + +#: contrib/localflavor/ch/ch_states.py:12 +msgid "Geneva" +msgstr "Женева" + +#: contrib/localflavor/ch/ch_states.py:13 +msgid "Glarus" +msgstr "Glarus" + +#: contrib/localflavor/ch/ch_states.py:14 +msgid "Graubuenden" +msgstr "Graubuenden" + +#: contrib/localflavor/ch/ch_states.py:15 +msgid "Jura" +msgstr "Jura" + +#: contrib/localflavor/ch/ch_states.py:16 +msgid "Lucerne" +msgstr "Lucerne" + +#: contrib/localflavor/ch/ch_states.py:17 +msgid "Neuchatel" +msgstr "Neuchatel" + +#: contrib/localflavor/ch/ch_states.py:18 +msgid "Nidwalden" +msgstr "Nidwalden" + +#: contrib/localflavor/ch/ch_states.py:19 +msgid "Obwalden" +msgstr "Obwalden" -#: contrib/localflavor/de/forms.py:60 +#: contrib/localflavor/ch/ch_states.py:20 +msgid "Schaffhausen" +msgstr "Schaffhausen" + +#: contrib/localflavor/ch/ch_states.py:21 +msgid "Schwyz" +msgstr "Schwyz" + +#: contrib/localflavor/ch/ch_states.py:22 +msgid "Solothurn" +msgstr "Solothurn" + +#: contrib/localflavor/ch/ch_states.py:23 +msgid "St. Gallen" +msgstr "St. Gallen" + +#: contrib/localflavor/ch/ch_states.py:24 +msgid "Thurgau" +msgstr "Thurgau" + +#: contrib/localflavor/ch/ch_states.py:25 +msgid "Ticino" +msgstr "Ticino" + +#: contrib/localflavor/ch/ch_states.py:26 +msgid "Uri" +msgstr "Uri" + +#: contrib/localflavor/ch/ch_states.py:27 +msgid "Valais" +msgstr "Valais" + +#: contrib/localflavor/ch/ch_states.py:28 +msgid "Vaud" +msgstr "Vaud" + +#: contrib/localflavor/ch/ch_states.py:29 +msgid "Zug" +msgstr "Zug" + +#: contrib/localflavor/ch/ch_states.py:30 +msgid "Zurich" +msgstr "Цирих" + +#: contrib/localflavor/ch/forms.py:16 contrib/localflavor/no/forms.py:12 +msgid "Enter a zip code in the format XXXX." +msgstr "Внесете правилен поштенски број во форматот XXXX." + +#: contrib/localflavor/ch/forms.py:64 msgid "" -"Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X " -"format." -msgstr "Внесете правилен број на лична карта во Германија во XXXXXXXXXXX-XXXXXXX-XXXXXXX-X форматот." +"Enter a valid Swiss identity or passport card number in X1234567<0 or " +"1234567890 format." +msgstr "" +"Внесете правилен швајцарски број на пасош во X1234567<0 или " +" 1234567890 формат." + +#: contrib/localflavor/cl/forms.py:29 +msgid "Enter a valid Chilean RUT." +msgstr "Внесете правилeн RUT за Чиле." + +#: contrib/localflavor/cl/forms.py:30 +msgid "Enter a valid Chilean RUT. The format is XX.XXX.XXX-X." +msgstr "Внесете правилен RUT за Чиле. Форматот е Xx.XXX.XXX-X." + +#: contrib/localflavor/cl/forms.py:31 +msgid "The Chilean RUT is not valid." +msgstr "RUT бројот за Чиле е невалиден." #: contrib/localflavor/de/de_states.py:5 msgid "Baden-Wuerttemberg" @@ -2165,11 +1953,353 @@ msgstr "Шлесвиг-Холштајн" msgid "Thuringia" msgstr "Турингиа" -#: contrib/localflavor/fi/forms.py:40 contrib/localflavor/fi/forms.py:46 +#: contrib/localflavor/de/forms.py:14 contrib/localflavor/fi/forms.py:12 +#: contrib/localflavor/fr/forms.py:15 +msgid "Enter a zip code in the format XXXXX." +msgstr "Внесете правилен поштенски број во формат XXXXXX." + +#: contrib/localflavor/de/forms.py:41 +msgid "" +"Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X " +"format." +msgstr "" +"Внесете правилен број на лична карта во Германија во XXXXXXXXXXX-XXXXXXX-" +"XXXXXXX-X форматот." + +#: contrib/localflavor/es/es_provinces.py:5 +msgid "Arava" +msgstr "Arava" + +#: contrib/localflavor/es/es_provinces.py:6 +msgid "Albacete" +msgstr "Albacete" + +#: contrib/localflavor/es/es_provinces.py:7 +msgid "Alacant" +msgstr "Alacant" + +#: contrib/localflavor/es/es_provinces.py:8 +msgid "Almeria" +msgstr "Almeria" + +#: contrib/localflavor/es/es_provinces.py:9 +msgid "Avila" +msgstr "Авила" + +#: contrib/localflavor/es/es_provinces.py:10 +msgid "Badajoz" +msgstr "Badajoz" + +#: contrib/localflavor/es/es_provinces.py:11 +msgid "Illes Balears" +msgstr "Illes Balears" + +#: contrib/localflavor/es/es_provinces.py:12 +msgid "Barcelona" +msgstr "Барцелона" + +#: contrib/localflavor/es/es_provinces.py:13 +msgid "Burgos" +msgstr "Burgos" + +#: contrib/localflavor/es/es_provinces.py:14 +msgid "Caceres" +msgstr "Caceres" + +#: contrib/localflavor/es/es_provinces.py:15 +msgid "Cadiz" +msgstr "Cadiz" + +#: contrib/localflavor/es/es_provinces.py:16 +msgid "Castello" +msgstr "Castello" + +#: contrib/localflavor/es/es_provinces.py:17 +msgid "Ciudad Real" +msgstr "Ciudad Real" + +#: contrib/localflavor/es/es_provinces.py:18 +msgid "Cordoba" +msgstr "Кордоба" + +#: contrib/localflavor/es/es_provinces.py:19 +msgid "A Coruna" +msgstr "A Coruna" + +#: contrib/localflavor/es/es_provinces.py:20 +msgid "Cuenca" +msgstr "Cuenca" + +#: contrib/localflavor/es/es_provinces.py:21 +msgid "Girona" +msgstr "Girona" + +#: contrib/localflavor/es/es_provinces.py:22 +msgid "Granada" +msgstr "Granada" + +#: contrib/localflavor/es/es_provinces.py:23 +msgid "Guadalajara" +msgstr "Guadalajara" + +#: contrib/localflavor/es/es_provinces.py:24 +msgid "Guipuzkoa" +msgstr "Guipuzkoa" + +#: contrib/localflavor/es/es_provinces.py:25 +msgid "Huelva" +msgstr "Huelva" + +#: contrib/localflavor/es/es_provinces.py:26 +msgid "Huesca" +msgstr "Huesca" + +#: contrib/localflavor/es/es_provinces.py:27 +msgid "Jaen" +msgstr "Jaen" + +#: contrib/localflavor/es/es_provinces.py:28 +msgid "Leon" +msgstr "Леон" + +#: contrib/localflavor/es/es_provinces.py:29 +msgid "Lleida" +msgstr "Lleida" + +#: contrib/localflavor/es/es_provinces.py:30 +#: contrib/localflavor/es/es_regions.py:17 +msgid "La Rioja" +msgstr "La Rioja" + +#: contrib/localflavor/es/es_provinces.py:31 +msgid "Lugo" +msgstr "Луго" + +#: contrib/localflavor/es/es_provinces.py:32 +#: contrib/localflavor/es/es_regions.py:18 +msgid "Madrid" +msgstr "Мадрид" + +#: contrib/localflavor/es/es_provinces.py:33 +msgid "Malaga" +msgstr "Малага" + +#: contrib/localflavor/es/es_provinces.py:34 +msgid "Murcia" +msgstr "Murcia" + +#: contrib/localflavor/es/es_provinces.py:35 +msgid "Navarre" +msgstr "Navarre" + +#: contrib/localflavor/es/es_provinces.py:36 +msgid "Ourense" +msgstr "Ourense" + +#: contrib/localflavor/es/es_provinces.py:37 +msgid "Asturias" +msgstr "Asturias" + +#: contrib/localflavor/es/es_provinces.py:38 +msgid "Palencia" +msgstr "Palencia" + +#: contrib/localflavor/es/es_provinces.py:39 +msgid "Las Palmas" +msgstr "Las Palmas" + +#: contrib/localflavor/es/es_provinces.py:40 +msgid "Pontevedra" +msgstr "Pontevedra" + +#: contrib/localflavor/es/es_provinces.py:41 +msgid "Salamanca" +msgstr "Salamanca" + +#: contrib/localflavor/es/es_provinces.py:42 +msgid "Santa Cruz de Tenerife" +msgstr "Санта Круз и Тенерифе" + +#: contrib/localflavor/es/es_provinces.py:43 +#: contrib/localflavor/es/es_regions.py:11 +msgid "Cantabria" +msgstr "Cantabria" + +#: contrib/localflavor/es/es_provinces.py:44 +msgid "Segovia" +msgstr "Segovia" + +#: contrib/localflavor/es/es_provinces.py:45 +msgid "Seville" +msgstr "Севиља" + +#: contrib/localflavor/es/es_provinces.py:46 +msgid "Soria" +msgstr "Soria" + +#: contrib/localflavor/es/es_provinces.py:47 +msgid "Tarragona" +msgstr "Tarragona" + +#: contrib/localflavor/es/es_provinces.py:48 +msgid "Teruel" +msgstr "Teruel" + +#: contrib/localflavor/es/es_provinces.py:49 +msgid "Toledo" +msgstr "Толедо" + +#: contrib/localflavor/es/es_provinces.py:50 +msgid "Valencia" +msgstr "Валенсија" + +#: contrib/localflavor/es/es_provinces.py:51 +msgid "Valladolid" +msgstr "Valladolid" + +#: contrib/localflavor/es/es_provinces.py:52 +msgid "Bizkaia" +msgstr "Bizkaia" + +#: contrib/localflavor/es/es_provinces.py:53 +msgid "Zamora" +msgstr "Zamora" + +#: contrib/localflavor/es/es_provinces.py:54 +msgid "Zaragoza" +msgstr "Сарагоса" + +#: contrib/localflavor/es/es_provinces.py:55 +msgid "Ceuta" +msgstr "Ceuta" + +#: contrib/localflavor/es/es_provinces.py:56 +msgid "Melilla" +msgstr "Melilla" + +#: contrib/localflavor/es/es_regions.py:5 +msgid "Andalusia" +msgstr "Андалузија" + +#: contrib/localflavor/es/es_regions.py:6 +msgid "Aragon" +msgstr "Арагон" + +#: contrib/localflavor/es/es_regions.py:7 +msgid "Principality of Asturias" +msgstr "Principality of Asturias" + +#: contrib/localflavor/es/es_regions.py:8 +msgid "Balearic Islands" +msgstr "Острови Балеарик" + +#: contrib/localflavor/es/es_regions.py:9 +msgid "Basque Country" +msgstr "Баскија" + +#: contrib/localflavor/es/es_regions.py:10 +msgid "Canary Islands" +msgstr "Канарски острови" + +#: contrib/localflavor/es/es_regions.py:12 +msgid "Castile-La Mancha" +msgstr "Кастиља ла Манча" + +#: contrib/localflavor/es/es_regions.py:13 +msgid "Castile and Leon" +msgstr "Кастиња и Леон" + +#: contrib/localflavor/es/es_regions.py:14 +msgid "Catalonia" +msgstr "Каталонија" + +#: contrib/localflavor/es/es_regions.py:15 +msgid "Extremadura" +msgstr "Екстремадура" + +#: contrib/localflavor/es/es_regions.py:16 +msgid "Galicia" +msgstr "Галиција" + +#: contrib/localflavor/es/es_regions.py:19 +msgid "Region of Murcia" +msgstr "Регион Мурција" + +#: contrib/localflavor/es/es_regions.py:20 +msgid "Foral Community of Navarre" +msgstr "Foral Community of Navarre" + +#: contrib/localflavor/es/es_regions.py:21 +msgid "Valencian Community" +msgstr "Valencian Community" + +#: contrib/localflavor/es/forms.py:19 +msgid "Enter a valid postal code in the range and format 01XXX - 52XXX." +msgstr "Внесете поштенски број во опсег и формат 01XXX - 52XXX." + +#: contrib/localflavor/es/forms.py:39 +msgid "" +"Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or " +"9XXXXXXXX." +msgstr "Внесете правилен телефонски број во еден од формативе 6XXXXXXXX, 8XXXXXXXX или 9XXXXXXXX." + +#: contrib/localflavor/es/forms.py:66 +msgid "Please enter a valid NIF, NIE, or CIF." +msgstr "Ве молам внесете правиелн NIF, NIE или CIF." + +#: contrib/localflavor/es/forms.py:67 +msgid "Please enter a valid NIF or NIE." +msgstr "Ве молам внесете валиден NIF или NIE." + +#: contrib/localflavor/es/forms.py:68 +msgid "Invalid checksum for NIF." +msgstr "Неправилна контролна сума за NIF." + +#: contrib/localflavor/es/forms.py:69 +msgid "Invalid checksum for NIE." +msgstr "Неправилна контролна сума за NIE." + +#: contrib/localflavor/es/forms.py:70 +msgid "Invalid checksum for CIF." +msgstr "Неправилна контролна сум за CIF." + +#: contrib/localflavor/es/forms.py:142 +msgid "Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX." +msgstr "Внесете правилна банкарска сметка во формат XXXX-XXXX-XX-XXXXXXXXXX." + +#: contrib/localflavor/es/forms.py:143 +msgid "Invalid checksum for bank account number." +msgstr "Неправилна контролна сума за бројот на банкарската сметка." + +#: contrib/localflavor/fi/forms.py:28 msgid "Enter a valid Finnish social security number." msgstr "Внесте правилен фински матичен број." -#: contrib/localflavor/jp/forms.py:21 +#: contrib/localflavor/in_/forms.py:14 +msgid "Enter a zip code in the format XXXXXXX." +msgstr "Внесете правилен поштенски број во форматот XXXXXXXX." + +#: contrib/localflavor/is_/forms.py:17 +msgid "Enter a valid Icelandic identification number. The format is XXXXXX-XXXX." +msgstr "Внесете валиден идентификационен број од Исланд. Форматот е XXXXXX-XXXX." + +#: contrib/localflavor/is_/forms.py:18 +msgid "The Icelandic identification number is not valid." +msgstr "Исландскиот идентификационент број е невалиден." + +#: contrib/localflavor/it/forms.py:14 +msgid "Enter a valid zip code." +msgstr "Внесете правилен поштенски број." + +#: contrib/localflavor/it/forms.py:43 +msgid "Enter a valid Social Security number." +msgstr "Внесете правилен осигурителен број." + +#: contrib/localflavor/it/forms.py:68 +msgid "Enter a valid VAT number." +msgstr "Внесете правилен даночен број." + +#: contrib/localflavor/jp/forms.py:17 msgid "Enter a postal code in the format XXXXXXX or XXX-XXXX." msgstr "Внесете поштенски број во форматот XXXXXXX или XXX-XXXX." @@ -2361,104 +2491,1596 @@ msgstr "Кагошима" msgid "Okinawa" msgstr "Окинава" -#: contrib/localflavor/no/forms.py:15 -msgid "Enter a zip code in the format XXXX." -msgstr "Внесете правилен поштенски број во форматот XXXX." +#: contrib/localflavor/mx/mx_states.py:12 +msgid "Aguascalientes" +msgstr "Aguascalientes" + +#: contrib/localflavor/mx/mx_states.py:13 +msgid "Baja California" +msgstr "Баја Калифорнија" + +#: contrib/localflavor/mx/mx_states.py:14 +msgid "Baja California Sur" +msgstr "Baja California Sur" + +#: contrib/localflavor/mx/mx_states.py:15 +msgid "Campeche" +msgstr "Кампеш" + +#: contrib/localflavor/mx/mx_states.py:16 +msgid "Chihuahua" +msgstr "Chihuahua" + +#: contrib/localflavor/mx/mx_states.py:17 +msgid "Chiapas" +msgstr "Чиапас" + +#: contrib/localflavor/mx/mx_states.py:18 +msgid "Coahuila" +msgstr "Coahuila" + +#: contrib/localflavor/mx/mx_states.py:19 +msgid "Colima" +msgstr "Колима" + +#: contrib/localflavor/mx/mx_states.py:20 +msgid "Distrito Federal" +msgstr "Distrito Federal" + +#: contrib/localflavor/mx/mx_states.py:21 +msgid "Durango" +msgstr "Дуранго" + +#: contrib/localflavor/mx/mx_states.py:22 +msgid "Guerrero" +msgstr "Гуереро" + +#: contrib/localflavor/mx/mx_states.py:23 +msgid "Guanajuato" +msgstr "Guanajuato" + +#: contrib/localflavor/mx/mx_states.py:24 +msgid "Hidalgo" +msgstr "Хидалго" + +#: contrib/localflavor/mx/mx_states.py:25 +msgid "Jalisco" +msgstr "Џалиско" + +#: contrib/localflavor/mx/mx_states.py:26 +msgid "Estado de México" +msgstr "Estado de México" -#: contrib/localflavor/no/forms.py:36 +#: contrib/localflavor/mx/mx_states.py:27 +msgid "Michoacán" +msgstr "Michoacán" + +#: contrib/localflavor/mx/mx_states.py:28 +msgid "Morelos" +msgstr "Morelos" + +#: contrib/localflavor/mx/mx_states.py:29 +msgid "Nayarit" +msgstr "Nayarit" + +#: contrib/localflavor/mx/mx_states.py:30 +msgid "Nuevo León" +msgstr "Nuevo León" + +#: contrib/localflavor/mx/mx_states.py:31 +msgid "Oaxaca" +msgstr "Oaxaca" + +#: contrib/localflavor/mx/mx_states.py:32 +msgid "Puebla" +msgstr "Puebla" + +#: contrib/localflavor/mx/mx_states.py:33 +msgid "Querétaro" +msgstr "Querétaro" + +#: contrib/localflavor/mx/mx_states.py:34 +msgid "Quintana Roo" +msgstr "Куантана Ро" + +#: contrib/localflavor/mx/mx_states.py:35 +msgid "Sinaloa" +msgstr "Синалоа" + +#: contrib/localflavor/mx/mx_states.py:36 +msgid "San Luis Potosí" +msgstr "Сан Луис Потоси" + +#: contrib/localflavor/mx/mx_states.py:37 +msgid "Sonora" +msgstr "Сонора" + +#: contrib/localflavor/mx/mx_states.py:38 +msgid "Tabasco" +msgstr "Табаско" + +#: contrib/localflavor/mx/mx_states.py:39 +msgid "Tamaulipas" +msgstr "Тамаулипас" + +#: contrib/localflavor/mx/mx_states.py:40 +msgid "Tlaxcala" +msgstr "Tlaxcala" + +#: contrib/localflavor/mx/mx_states.py:41 +msgid "Veracruz" +msgstr "Веракруз" + +#: contrib/localflavor/mx/mx_states.py:42 +msgid "Yucatán" +msgstr "Јукатан" + +#: contrib/localflavor/mx/mx_states.py:43 +msgid "Zacatecas" +msgstr "Зацатекас" + +#: contrib/localflavor/nl/forms.py:21 +msgid "Enter a valid postal code" +msgstr "Внесете правилен поштенски код" + +#: contrib/localflavor/nl/forms.py:52 +msgid "Enter a valid phone number" +msgstr "Внесете валиден телефонски број" + +#: contrib/localflavor/nl/forms.py:78 +msgid "Enter a valid SoFi number" +msgstr "Внесете валиден осигурителен број" + +#: contrib/localflavor/nl/nl_provinces.py:4 +msgid "Drente" +msgstr "Дренте" + +#: contrib/localflavor/nl/nl_provinces.py:5 +msgid "Flevoland" +msgstr "Флеволанд" + +#: contrib/localflavor/nl/nl_provinces.py:6 +msgid "Friesland" +msgstr "Фраисланд" + +#: contrib/localflavor/nl/nl_provinces.py:7 +msgid "Gelderland" +msgstr "Гелдерланд" + +#: contrib/localflavor/nl/nl_provinces.py:8 +msgid "Groningen" +msgstr "Гронинген" + +#: contrib/localflavor/nl/nl_provinces.py:9 +msgid "Limburg" +msgstr "Лимбург" + +#: contrib/localflavor/nl/nl_provinces.py:10 +msgid "Noord-Brabant" +msgstr "Noord-Brabant" + +#: contrib/localflavor/nl/nl_provinces.py:11 +msgid "Noord-Holland" +msgstr "Noord-Holland" + +#: contrib/localflavor/nl/nl_provinces.py:12 +msgid "Overijssel" +msgstr "Overijssel" + +#: contrib/localflavor/nl/nl_provinces.py:13 +msgid "Utrecht" +msgstr "Утрехт" + +#: contrib/localflavor/nl/nl_provinces.py:14 +msgid "Zeeland" +msgstr "Зиланд" + +#: contrib/localflavor/nl/nl_provinces.py:15 +msgid "Zuid-Holland" +msgstr "Zuid-Holland" + +#: contrib/localflavor/no/forms.py:33 msgid "Enter a valid Norwegian social security number." msgstr "Ве молам внесете правилен норвешки матичен број." -#: contrib/localflavor/uk/forms.py:18 -msgid "Enter a postcode. A space is required between the two postcode parts." -msgstr "Внесете поштенски број. Задолжително е празно место помеѓу двата дела на поштенскиот број." +#: contrib/localflavor/pe/forms.py:24 +msgid "This field requires 8 digits." +msgstr "Во ова поле мора да се внесат 8 цифри." + +#: contrib/localflavor/pe/forms.py:52 +msgid "This field requires 11 digits." +msgstr "Во ова поле се потребни 11 цифри." + +#: contrib/localflavor/pl/forms.py:39 +msgid "National Identification Number consists of 11 digits." +msgstr "Националниот идентификационен број се состои од 11 цифири." + +#: contrib/localflavor/pl/forms.py:40 +msgid "Wrong checksum for the National Identification Number." +msgstr "Погрешна проверка за Националниот идентификационен број." + +#: contrib/localflavor/pl/forms.py:72 +msgid "Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX." +msgstr "Внесете даночен број (NIP) во форматот XXX-XXX-XX-XX или XX-XX-XXX-XXX." + +#: contrib/localflavor/pl/forms.py:73 +msgid "Wrong checksum for the Tax Number (NIP)." +msgstr "Даночниот број (NIP) е погрешен." + +#: contrib/localflavor/pl/forms.py:112 +msgid "National Business Register Number (REGON) consists of 7 or 9 digits." +msgstr "Националниот бизнис регистрациски број (REGON) се состои од 7 или 9 цифри." + +#: contrib/localflavor/pl/forms.py:113 +msgid "Wrong checksum for the National Business Register Number (REGON)." +msgstr "Неправилна контролна сум за Националниот бизнис регистрационен број (REGON)." + +#: contrib/localflavor/pl/forms.py:156 +msgid "Enter a postal code in the format XX-XXX." +msgstr "Внесете правилен поштенски број во формат XX-XXX." + +#: contrib/localflavor/pl/pl_voivodeships.py:8 +msgid "Lower Silesia" +msgstr "Долна Силесиа" + +#: contrib/localflavor/pl/pl_voivodeships.py:9 +msgid "Kuyavia-Pomerania" +msgstr "Kuyavia-Pomerania" + +#: contrib/localflavor/pl/pl_voivodeships.py:10 +msgid "Lublin" +msgstr "Лублин" + +#: contrib/localflavor/pl/pl_voivodeships.py:11 +msgid "Lubusz" +msgstr "Лубус" + +#: contrib/localflavor/pl/pl_voivodeships.py:12 +msgid "Lodz" +msgstr "Лодз" + +#: contrib/localflavor/pl/pl_voivodeships.py:13 +msgid "Lesser Poland" +msgstr "Lesser Poland" + +#: contrib/localflavor/pl/pl_voivodeships.py:14 +msgid "Masovia" +msgstr "Масовиа" + +#: contrib/localflavor/pl/pl_voivodeships.py:15 +msgid "Opole" +msgstr "Ополе" + +#: contrib/localflavor/pl/pl_voivodeships.py:16 +msgid "Subcarpatia" +msgstr "Субкарпатиа" + +#: contrib/localflavor/pl/pl_voivodeships.py:17 +msgid "Podlasie" +msgstr "Подласи" + +#: contrib/localflavor/pl/pl_voivodeships.py:18 +msgid "Pomerania" +msgstr "Померанија" + +#: contrib/localflavor/pl/pl_voivodeships.py:19 +msgid "Silesia" +msgstr "Силесиа" + +#: contrib/localflavor/pl/pl_voivodeships.py:20 +msgid "Swietokrzyskie" +msgstr "Swietokrzyskie" + +#: contrib/localflavor/pl/pl_voivodeships.py:21 +msgid "Warmia-Masuria" +msgstr "Warmia-Masuria" + +#: contrib/localflavor/pl/pl_voivodeships.py:22 +msgid "Greater Poland" +msgstr "Greater Poland" + +#: contrib/localflavor/pl/pl_voivodeships.py:23 +msgid "West Pomerania" +msgstr "Западна Померанија" + +#: contrib/localflavor/sk/forms.py:30 +msgid "Enter a postal code in the format XXXXX or XXX XX." +msgstr "Внесете поштенски број во форматот XXXXX или XXX XX." + +#: contrib/localflavor/sk/sk_districts.py:8 +msgid "Banska Bystrica" +msgstr "Банска Бистрица" + +#: contrib/localflavor/sk/sk_districts.py:9 +msgid "Banska Stiavnica" +msgstr "Банска Стиавница" + +#: contrib/localflavor/sk/sk_districts.py:10 +msgid "Bardejov" +msgstr "Бардејов" + +#: contrib/localflavor/sk/sk_districts.py:11 +msgid "Banovce nad Bebravou" +msgstr "Бановце над Бебраво" + +#: contrib/localflavor/sk/sk_districts.py:12 +msgid "Brezno" +msgstr "Брезно" + +#: contrib/localflavor/sk/sk_districts.py:13 +msgid "Bratislava I" +msgstr "Братислава I" + +#: contrib/localflavor/sk/sk_districts.py:14 +msgid "Bratislava II" +msgstr "Братислава II" + +#: contrib/localflavor/sk/sk_districts.py:15 +msgid "Bratislava III" +msgstr "Братислава III" + +#: contrib/localflavor/sk/sk_districts.py:16 +msgid "Bratislava IV" +msgstr "Братислава IV" + +#: contrib/localflavor/sk/sk_districts.py:17 +msgid "Bratislava V" +msgstr "Братислава V" + +#: contrib/localflavor/sk/sk_districts.py:18 +msgid "Bytca" +msgstr "Битка" + +#: contrib/localflavor/sk/sk_districts.py:19 +msgid "Cadca" +msgstr "Кадка" + +#: contrib/localflavor/sk/sk_districts.py:20 +msgid "Detva" +msgstr "Детва" + +#: contrib/localflavor/sk/sk_districts.py:21 +msgid "Dolny Kubin" +msgstr "Dolny Kubin" + +#: contrib/localflavor/sk/sk_districts.py:22 +msgid "Dunajska Streda" +msgstr "Dunajska Streda" + +#: contrib/localflavor/sk/sk_districts.py:23 +msgid "Galanta" +msgstr "Galanta" + +#: contrib/localflavor/sk/sk_districts.py:24 +msgid "Gelnica" +msgstr "Гелника" + +#: contrib/localflavor/sk/sk_districts.py:25 +msgid "Hlohovec" +msgstr "Хлоховец" + +#: contrib/localflavor/sk/sk_districts.py:26 +msgid "Humenne" +msgstr "Хумен" + +#: contrib/localflavor/sk/sk_districts.py:27 +msgid "Ilava" +msgstr "Ilava" + +#: contrib/localflavor/sk/sk_districts.py:28 +msgid "Kezmarok" +msgstr "Кезмарок" + +#: contrib/localflavor/sk/sk_districts.py:29 +msgid "Komarno" +msgstr "Комарно" + +#: contrib/localflavor/sk/sk_districts.py:30 +msgid "Kosice I" +msgstr "Кошице I" + +#: contrib/localflavor/sk/sk_districts.py:31 +msgid "Kosice II" +msgstr "Кошице II" + +#: contrib/localflavor/sk/sk_districts.py:32 +msgid "Kosice III" +msgstr "Кошице III" + +#: contrib/localflavor/sk/sk_districts.py:33 +msgid "Kosice IV" +msgstr "Кошице IV" + +#: contrib/localflavor/sk/sk_districts.py:34 +msgid "Kosice - okolie" +msgstr "Кошице - околина" -#: contrib/localflavor/usa/forms.py:18 +#: contrib/localflavor/sk/sk_districts.py:35 +msgid "Krupina" +msgstr "Крупина" + +#: contrib/localflavor/sk/sk_districts.py:36 +msgid "Kysucke Nove Mesto" +msgstr "Kysucke Nove Mesto" + +#: contrib/localflavor/sk/sk_districts.py:37 +msgid "Levice" +msgstr "Левице" + +#: contrib/localflavor/sk/sk_districts.py:38 +msgid "Levoca" +msgstr "Левока" + +#: contrib/localflavor/sk/sk_districts.py:39 +msgid "Liptovsky Mikulas" +msgstr "Liptovsky Mikulas" + +#: contrib/localflavor/sk/sk_districts.py:40 +msgid "Lucenec" +msgstr "Луценец" + +#: contrib/localflavor/sk/sk_districts.py:41 +msgid "Malacky" +msgstr "Malacky" + +#: contrib/localflavor/sk/sk_districts.py:42 +msgid "Martin" +msgstr "Мартин" + +#: contrib/localflavor/sk/sk_districts.py:43 +msgid "Medzilaborce" +msgstr "Medzilaborce" + +#: contrib/localflavor/sk/sk_districts.py:44 +msgid "Michalovce" +msgstr "Michalovce" + +#: contrib/localflavor/sk/sk_districts.py:45 +msgid "Myjava" +msgstr "Myjava" + +#: contrib/localflavor/sk/sk_districts.py:46 +msgid "Namestovo" +msgstr "Наместово" + +#: contrib/localflavor/sk/sk_districts.py:47 +msgid "Nitra" +msgstr "Нитра" + +#: contrib/localflavor/sk/sk_districts.py:48 +msgid "Nove Mesto nad Vahom" +msgstr "Nove Mesto nad Vahom" + +#: contrib/localflavor/sk/sk_districts.py:49 +msgid "Nove Zamky" +msgstr "Nove Zamky" + +#: contrib/localflavor/sk/sk_districts.py:50 +msgid "Partizanske" +msgstr "Партизанске" + +#: contrib/localflavor/sk/sk_districts.py:51 +msgid "Pezinok" +msgstr "Пезинок" + +#: contrib/localflavor/sk/sk_districts.py:52 +msgid "Piestany" +msgstr "Piestany" + +#: contrib/localflavor/sk/sk_districts.py:53 +msgid "Poltar" +msgstr "Полтар" + +#: contrib/localflavor/sk/sk_districts.py:54 +msgid "Poprad" +msgstr "Попрад" + +#: contrib/localflavor/sk/sk_districts.py:55 +msgid "Povazska Bystrica" +msgstr "Povazska Bystrica" + +#: contrib/localflavor/sk/sk_districts.py:56 +msgid "Presov" +msgstr "Пресов" + +#: contrib/localflavor/sk/sk_districts.py:57 +msgid "Prievidza" +msgstr "Prievidza" + +#: contrib/localflavor/sk/sk_districts.py:58 +msgid "Puchov" +msgstr "Пучов" + +#: contrib/localflavor/sk/sk_districts.py:59 +msgid "Revuca" +msgstr "Ревука" + +#: contrib/localflavor/sk/sk_districts.py:60 +msgid "Rimavska Sobota" +msgstr "Rimavska Sobota" + +#: contrib/localflavor/sk/sk_districts.py:61 +msgid "Roznava" +msgstr "Рознава" + +#: contrib/localflavor/sk/sk_districts.py:62 +msgid "Ruzomberok" +msgstr "Рузомберок" + +#: contrib/localflavor/sk/sk_districts.py:63 +msgid "Sabinov" +msgstr "Сабинов" + +#: contrib/localflavor/sk/sk_districts.py:64 +msgid "Senec" +msgstr "Сенец" + +#: contrib/localflavor/sk/sk_districts.py:65 +msgid "Senica" +msgstr "Сеника" + +#: contrib/localflavor/sk/sk_districts.py:66 +msgid "Skalica" +msgstr "Скалица" + +#: contrib/localflavor/sk/sk_districts.py:67 +msgid "Snina" +msgstr "Шнина" + +#: contrib/localflavor/sk/sk_districts.py:68 +msgid "Sobrance" +msgstr "Собранце" + +#: contrib/localflavor/sk/sk_districts.py:69 +msgid "Spisska Nova Ves" +msgstr "Spisska Nova Ves" + +#: contrib/localflavor/sk/sk_districts.py:70 +msgid "Stara Lubovna" +msgstr "Стара Љубовна" + +#: contrib/localflavor/sk/sk_districts.py:71 +msgid "Stropkov" +msgstr "Стропков" + +#: contrib/localflavor/sk/sk_districts.py:72 +msgid "Svidnik" +msgstr "Свидник" + +#: contrib/localflavor/sk/sk_districts.py:73 +msgid "Sala" +msgstr "Сала" + +#: contrib/localflavor/sk/sk_districts.py:74 +msgid "Topolcany" +msgstr "Тополчани" + +#: contrib/localflavor/sk/sk_districts.py:75 +msgid "Trebisov" +msgstr "Требисов" + +#: contrib/localflavor/sk/sk_districts.py:76 +msgid "Trencin" +msgstr "Тренцин" + +#: contrib/localflavor/sk/sk_districts.py:77 +msgid "Trnava" +msgstr "Трнава" + +#: contrib/localflavor/sk/sk_districts.py:78 +msgid "Turcianske Teplice" +msgstr "Turcianske Teplice" + +#: contrib/localflavor/sk/sk_districts.py:79 +msgid "Tvrdosin" +msgstr "Тврдосин" + +#: contrib/localflavor/sk/sk_districts.py:80 +msgid "Velky Krtis" +msgstr "Velky Krtis" + +#: contrib/localflavor/sk/sk_districts.py:81 +msgid "Vranov nad Toplou" +msgstr "Vranov nad Toplou" + +#: contrib/localflavor/sk/sk_districts.py:82 +msgid "Zlate Moravce" +msgstr "Злате Моравце" + +#: contrib/localflavor/sk/sk_districts.py:83 +msgid "Zvolen" +msgstr "Зволен" + +#: contrib/localflavor/sk/sk_districts.py:84 +msgid "Zarnovica" +msgstr "Зарновица" + +#: contrib/localflavor/sk/sk_districts.py:85 +msgid "Ziar nad Hronom" +msgstr "Ziar nad Hronom" + +#: contrib/localflavor/sk/sk_districts.py:86 +msgid "Zilina" +msgstr "Зилина" + +#: contrib/localflavor/sk/sk_regions.py:8 +msgid "Banska Bystrica region" +msgstr "Banska Bystrica region" + +#: contrib/localflavor/sk/sk_regions.py:9 +msgid "Bratislava region" +msgstr "Братиславски регион" + +#: contrib/localflavor/sk/sk_regions.py:10 +msgid "Kosice region" +msgstr "Кошице регион" + +#: contrib/localflavor/sk/sk_regions.py:11 +msgid "Nitra region" +msgstr "Нитра регион" + +#: contrib/localflavor/sk/sk_regions.py:12 +msgid "Presov region" +msgstr "Пресов регион" + +#: contrib/localflavor/sk/sk_regions.py:13 +msgid "Trencin region" +msgstr "Тренцин регион" + +#: contrib/localflavor/sk/sk_regions.py:14 +msgid "Trnava region" +msgstr "Трнава регион" + +#: contrib/localflavor/sk/sk_regions.py:15 +msgid "Zilina region" +msgstr "Зилина регион" + +#: contrib/localflavor/uk/forms.py:21 +msgid "Enter a valid postcode." +msgstr "Внесете правилен поштенски код." + +#: contrib/localflavor/uk/uk_regions.py:11 +msgid "Bedfordshire" +msgstr "Бедфордшир" + +#: contrib/localflavor/uk/uk_regions.py:12 +msgid "Buckinghamshire" +msgstr "Букингхамшир" + +#: contrib/localflavor/uk/uk_regions.py:14 +msgid "Cheshire" +msgstr "Чешир" + +#: contrib/localflavor/uk/uk_regions.py:15 +msgid "Cornwall and Isles of Scilly" +msgstr "Cornwall and Isles of Scilly" + +#: contrib/localflavor/uk/uk_regions.py:16 +msgid "Cumbria" +msgstr "Кумбриа" + +#: contrib/localflavor/uk/uk_regions.py:17 +msgid "Derbyshire" +msgstr "Дербишир" + +#: contrib/localflavor/uk/uk_regions.py:18 +msgid "Devon" +msgstr "Девон" + +#: contrib/localflavor/uk/uk_regions.py:19 +msgid "Dorset" +msgstr "Дорсет" + +#: contrib/localflavor/uk/uk_regions.py:20 +msgid "Durham" +msgstr "Дурам" + +#: contrib/localflavor/uk/uk_regions.py:21 +msgid "East Sussex" +msgstr "Источен Сасекс" + +#: contrib/localflavor/uk/uk_regions.py:22 +msgid "Essex" +msgstr "Есекс" + +#: contrib/localflavor/uk/uk_regions.py:23 +msgid "Gloucestershire" +msgstr "Gloucestershire" + +#: contrib/localflavor/uk/uk_regions.py:24 +msgid "Greater London" +msgstr "Поширок Лондон" + +#: contrib/localflavor/uk/uk_regions.py:25 +msgid "Greater Manchester" +msgstr "Поширок Манчестер" + +#: contrib/localflavor/uk/uk_regions.py:26 +msgid "Hampshire" +msgstr "Хемпшир" + +#: contrib/localflavor/uk/uk_regions.py:27 +msgid "Hertfordshire" +msgstr "Хертфордшир" + +#: contrib/localflavor/uk/uk_regions.py:28 +msgid "Kent" +msgstr "Кент" + +#: contrib/localflavor/uk/uk_regions.py:29 +msgid "Lancashire" +msgstr "Ланкашир" + +#: contrib/localflavor/uk/uk_regions.py:30 +msgid "Leicestershire" +msgstr "Leicestershire" + +#: contrib/localflavor/uk/uk_regions.py:31 +msgid "Lincolnshire" +msgstr "Линколншир" + +#: contrib/localflavor/uk/uk_regions.py:32 +msgid "Merseyside" +msgstr "Мерсејсајд" + +#: contrib/localflavor/uk/uk_regions.py:33 +msgid "Norfolk" +msgstr "Норфолк" + +#: contrib/localflavor/uk/uk_regions.py:34 +msgid "North Yorkshire" +msgstr "Северен Јоркшир" + +#: contrib/localflavor/uk/uk_regions.py:35 +msgid "Northamptonshire" +msgstr "Нортхамтоншир" + +#: contrib/localflavor/uk/uk_regions.py:36 +msgid "Northumberland" +msgstr "Northumberland" + +#: contrib/localflavor/uk/uk_regions.py:37 +msgid "Nottinghamshire" +msgstr "Нотингхамшир" + +#: contrib/localflavor/uk/uk_regions.py:38 +msgid "Oxfordshire" +msgstr "Оксфордшир" + +#: contrib/localflavor/uk/uk_regions.py:39 +msgid "Shropshire" +msgstr "Шропшир" + +#: contrib/localflavor/uk/uk_regions.py:40 +msgid "Somerset" +msgstr "Сомерсет" + +#: contrib/localflavor/uk/uk_regions.py:41 +msgid "South Yorkshire" +msgstr "Јужен Јоркшир" + +#: contrib/localflavor/uk/uk_regions.py:42 +msgid "Staffordshire" +msgstr "Стафордшир" + +#: contrib/localflavor/uk/uk_regions.py:43 +msgid "Suffolk" +msgstr "Суфолк" + +#: contrib/localflavor/uk/uk_regions.py:44 +msgid "Surrey" +msgstr "Surrey" + +#: contrib/localflavor/uk/uk_regions.py:45 +msgid "Tyne and Wear" +msgstr "Tyne and Wear" + +#: contrib/localflavor/uk/uk_regions.py:46 +msgid "Warwickshire" +msgstr "Warwickshire" + +#: contrib/localflavor/uk/uk_regions.py:47 +msgid "West Midlands" +msgstr "Западен Мидландс" + +#: contrib/localflavor/uk/uk_regions.py:48 +msgid "West Sussex" +msgstr "Западен Сасекс" + +#: contrib/localflavor/uk/uk_regions.py:49 +msgid "West Yorkshire" +msgstr "Западен Јоркшир" + +#: contrib/localflavor/uk/uk_regions.py:50 +msgid "Wiltshire" +msgstr "Вилтшир" + +#: contrib/localflavor/uk/uk_regions.py:51 +msgid "Worcestershire" +msgstr "Worcestershire" + +#: contrib/localflavor/uk/uk_regions.py:55 +msgid "County Antrim" +msgstr "County Antrim" + +#: contrib/localflavor/uk/uk_regions.py:56 +msgid "County Armagh" +msgstr "County Armagh" + +#: contrib/localflavor/uk/uk_regions.py:57 +msgid "County Down" +msgstr "County Down" + +#: contrib/localflavor/uk/uk_regions.py:58 +msgid "County Fermanagh" +msgstr "County Fermanagh" + +#: contrib/localflavor/uk/uk_regions.py:59 +msgid "County Londonderry" +msgstr "County Londonderry" + +#: contrib/localflavor/uk/uk_regions.py:60 +msgid "County Tyrone" +msgstr "County Tyrone" + +#: contrib/localflavor/uk/uk_regions.py:64 +msgid "Clwyd" +msgstr "Clwyd" + +#: contrib/localflavor/uk/uk_regions.py:65 +msgid "Dyfed" +msgstr "Dyfed" + +#: contrib/localflavor/uk/uk_regions.py:66 +msgid "Gwent" +msgstr "Гвент" + +#: contrib/localflavor/uk/uk_regions.py:67 +msgid "Gwynedd" +msgstr "Gwynedd" + +#: contrib/localflavor/uk/uk_regions.py:68 +msgid "Mid Glamorgan" +msgstr "Mid Glamorgan" + +#: contrib/localflavor/uk/uk_regions.py:69 +msgid "Powys" +msgstr "Powys" + +#: contrib/localflavor/uk/uk_regions.py:70 +msgid "South Glamorgan" +msgstr "Јужен Гламорга" + +#: contrib/localflavor/uk/uk_regions.py:71 +msgid "West Glamorgan" +msgstr "Западен Гламорган" + +#: contrib/localflavor/uk/uk_regions.py:75 +msgid "Borders" +msgstr "Borders" + +#: contrib/localflavor/uk/uk_regions.py:76 +msgid "Central Scotland" +msgstr "Централна Шкотска" + +#: contrib/localflavor/uk/uk_regions.py:77 +msgid "Dumfries and Galloway" +msgstr "Dumfries and Galloway" + +#: contrib/localflavor/uk/uk_regions.py:78 +msgid "Fife" +msgstr "Fife" + +#: contrib/localflavor/uk/uk_regions.py:79 +msgid "Grampian" +msgstr "Грампиан" + +#: contrib/localflavor/uk/uk_regions.py:80 +msgid "Highland" +msgstr "Highland" + +#: contrib/localflavor/uk/uk_regions.py:81 +msgid "Lothian" +msgstr "Lothian" + +#: contrib/localflavor/uk/uk_regions.py:82 +msgid "Orkney Islands" +msgstr "Orkney Islands" + +#: contrib/localflavor/uk/uk_regions.py:83 +msgid "Shetland Islands" +msgstr "Shetland Islands" + +#: contrib/localflavor/uk/uk_regions.py:84 +msgid "Strathclyde" +msgstr "Strathclyde" + +#: contrib/localflavor/uk/uk_regions.py:85 +msgid "Tayside" +msgstr "Tayside" + +#: contrib/localflavor/uk/uk_regions.py:86 +msgid "Western Isles" +msgstr "Western Isles" + +#: contrib/localflavor/uk/uk_regions.py:90 +msgid "England" +msgstr "Англија" + +#: contrib/localflavor/uk/uk_regions.py:91 +msgid "Northern Ireland" +msgstr "Северна Ирска" + +#: contrib/localflavor/uk/uk_regions.py:92 +msgid "Scotland" +msgstr "Шкотска" + +#: contrib/localflavor/uk/uk_regions.py:93 +msgid "Wales" +msgstr "Велс" + +#: contrib/localflavor/us/forms.py:16 msgid "Enter a zip code in the format XXXXX or XXXXX-XXXX." msgstr "Внесете поштенски број во форматот XXXXX или XXXXX-XXXX." -#: contrib/localflavor/usa/forms.py:51 +#: contrib/localflavor/us/forms.py:54 msgid "Enter a valid U.S. Social Security number in XXX-XX-XXXX format." msgstr "Внесте правилен матичен број за САД во XXX-XX-XXXX форматот." -#: contrib/sessions/models.py:68 +#: contrib/localflavor/za/forms.py:20 +msgid "Enter a valid South African ID number" +msgstr "Внесете правилен јужно афрички број за идентификација" + +#: contrib/localflavor/za/forms.py:54 +msgid "Enter a valid South African postal code" +msgstr "Внесете правилен јужно афрички поштенски код" + +#: contrib/localflavor/za/za_provinces.py:4 +msgid "Eastern Cape" +msgstr "Источен Кејп" + +#: contrib/localflavor/za/za_provinces.py:5 +msgid "Free State" +msgstr "Free State" + +#: contrib/localflavor/za/za_provinces.py:6 +msgid "Gauteng" +msgstr "Gauteng" + +#: contrib/localflavor/za/za_provinces.py:7 +msgid "KwaZulu-Natal" +msgstr "KwaZulu-Natal" + +#: contrib/localflavor/za/za_provinces.py:8 +msgid "Limpopo" +msgstr "Limpopo" + +#: contrib/localflavor/za/za_provinces.py:9 +msgid "Mpumalanga" +msgstr "Mpumalanga" + +#: contrib/localflavor/za/za_provinces.py:10 +msgid "Northern Cape" +msgstr "Northern Cape" + +#: contrib/localflavor/za/za_provinces.py:11 +msgid "North West" +msgstr "North West" + +#: contrib/localflavor/za/za_provinces.py:12 +msgid "Western Cape" +msgstr "Western Cape" + +#: contrib/redirects/models.py:7 +msgid "redirect from" +msgstr "пренасочи од" + +#: contrib/redirects/models.py:8 +msgid "" +"This should be an absolute path, excluding the domain name. Example: '/" +"events/search/'." +msgstr "" +"Ова треба да биде апсолутна патека без името на домејнот. На пр. „/nastani/" +"prebaraj/“." + +#: contrib/redirects/models.py:9 +msgid "redirect to" +msgstr "пренасочи кон" + +#: contrib/redirects/models.py:10 +msgid "" +"This can be either an absolute path (as above) or a full URL starting with " +"'http://'." +msgstr "" +"Ова може да биде или апсолутна патека (како погоре) или цела адреса " +"почувајќи со „http://“." + +#: contrib/redirects/models.py:13 +msgid "redirect" +msgstr "пренасочување" + +#: contrib/redirects/models.py:14 +msgid "redirects" +msgstr "пренасочувања" + +#: contrib/sessions/models.py:41 msgid "session key" msgstr "клуч на сесијата" -#: contrib/sessions/models.py:69 +#: contrib/sessions/models.py:42 msgid "session data" msgstr "податоци од сесијата" -#: contrib/sessions/models.py:70 +#: contrib/sessions/models.py:43 msgid "expire date" msgstr "датум на истекување" -#: contrib/sessions/models.py:74 +#: contrib/sessions/models.py:48 msgid "session" msgstr "сесија" -#: contrib/sessions/models.py:75 +#: contrib/sessions/models.py:49 msgid "sessions" msgstr "сесии" -#: contrib/contenttypes/models.py:36 -msgid "python model class name" -msgstr "има на класата на питон моделите" +#: contrib/sites/models.py:32 +msgid "domain name" +msgstr "домејн" -#: contrib/contenttypes/models.py:39 -msgid "content type" -msgstr "content type" +#: contrib/sites/models.py:33 +msgid "display name" +msgstr "име кое се прикажува" -#: contrib/contenttypes/models.py:40 -msgid "content types" -msgstr "content types" +#: contrib/sites/models.py:37 +msgid "site" +msgstr "сајт" + +#: contrib/sites/models.py:38 +msgid "sites" +msgstr "сајтови" + +#: core/validators.py:72 +msgid "This value must contain only letters, numbers and underscores." +msgstr "Оваа вредност смее да има само букви, бројки или долни црти." + +#: core/validators.py:76 +msgid "" +"This value must contain only letters, numbers, underscores, dashes or " +"slashes." +msgstr "Оваа вредност смее да има само букви, бројки, долни црти, црти или коси црти." + +#: core/validators.py:80 +msgid "This value must contain only letters, numbers, underscores or hyphens." +msgstr "Оваа вредност смее да содржи само букви, бројки, долни црти или црти." + +#: core/validators.py:84 +msgid "Uppercase letters are not allowed here." +msgstr "Големи букви не се дозволени." + +#: core/validators.py:88 +msgid "Lowercase letters are not allowed here." +msgstr "Мали букви не се дозволени." + +#: core/validators.py:95 +msgid "Enter only digits separated by commas." +msgstr "Внесете само цифри одделени со запирки." + +#: core/validators.py:107 +msgid "Enter valid e-mail addresses separated by commas." +msgstr "Внесете валидни адреси за е-пошта одделени со запирки." + +#: core/validators.py:111 +msgid "Please enter a valid IP address." +msgstr "Ве молам внесете валидна ИП адреса." + +#: core/validators.py:115 +msgid "Empty values are not allowed here." +msgstr "Празни вредности не се дозволени." + +#: core/validators.py:119 +msgid "Non-numeric characters aren't allowed here." +msgstr "Ненумерички знаци не се дозволени тука." + +#: core/validators.py:123 +msgid "This value can't be comprised solely of digits." +msgstr "Оваа вредност не смее да биде само од цифри." + +#: core/validators.py:128 newforms/fields.py:152 +msgid "Enter a whole number." +msgstr "Внеси цел број." + +#: core/validators.py:132 +msgid "Only alphabetical characters are allowed here." +msgstr "Дозволени се само букви." + +#: core/validators.py:147 +msgid "Year must be 1900 or later." +msgstr "Годината мора да биде 1900 или покасно." + +#: core/validators.py:151 +#, python-format +msgid "Invalid date: %s" +msgstr "Неправилен датум: %s" + +#: core/validators.py:156 db/models/fields/__init__.py:548 +msgid "Enter a valid date in YYYY-MM-DD format." +msgstr "Внесете правилен датум во форматот ГГГГ-ММ-ДД." + +#: core/validators.py:161 +msgid "Enter a valid time in HH:MM format." +msgstr "Внесете правилно време во форматот HH:MM." + +#: core/validators.py:165 db/models/fields/__init__.py:625 +msgid "Enter a valid date/time in YYYY-MM-DD HH:MM format." +msgstr "Внесете правилен датум/време во форматот ГГГГ-ММ-ДД ЧЧ:ММ." + +#: core/validators.py:170 newforms/fields.py:403 +msgid "Enter a valid e-mail address." +msgstr "Внесeте правилна адреса за е-пошта." + +#: core/validators.py:182 core/validators.py:474 newforms/fields.py:433 +#: oldforms/__init__.py:687 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "Не беше пратена датотека. Проверете го типот на енкодирање на формата." + +#: core/validators.py:193 newforms/fields.py:459 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" +"Качете валидна фотографија. Датотеката која ја качивте или не беше " +"фотографија или беше расипана датотеката." + +#: core/validators.py:200 +#, python-format +msgid "The URL %s does not point to a valid image." +msgstr "Адресата %s не покажува кон валидна фотографија." + +#: core/validators.py:204 +#, python-format +msgid "Phone numbers must be in XXX-XXX-XXXX format. \"%s\" is invalid." +msgstr "" +"Телефонските броеви мора да бидат во XXX-XXX-XXXX форматот. „%s“ не е " +"валиден." + +#: core/validators.py:212 +#, python-format +msgid "The URL %s does not point to a valid QuickTime video." +msgstr "Адресата „%s“ не покажува кон QuickTime видео." + +#: core/validators.py:216 +msgid "A valid URL is required." +msgstr "Задолжителна е правилна адреса." + +#: core/validators.py:230 +#, python-format +msgid "" +"Valid HTML is required. Specific errors are:\n" +"%s" +msgstr "" +"Задолжителен е правилен HTML. Грешките се:\n" +"%s" + +#: core/validators.py:237 +#, python-format +msgid "Badly formed XML: %s" +msgstr "Неправилно формиран XML: %s" + +#: core/validators.py:254 +#, python-format +msgid "Invalid URL: %s" +msgstr "Неправилна адреса: %s" + +#: core/validators.py:259 core/validators.py:261 +#, python-format +msgid "The URL %s is a broken link." +msgstr "Адресата %s е скршена врска." + +#: core/validators.py:267 +msgid "Enter a valid U.S. state abbreviation." +msgstr "Внесете правилна скратеница за држава во САД." + +#: core/validators.py:281 +#, python-format +msgid "Watch your mouth! The word %s is not allowed here." +msgid_plural "Watch your mouth! The words %s are not allowed here." +msgstr[0] "Внимавајте на јазикот. Тука не е дозволен зборот %s." +msgstr[1] "Внимавајте на јазикот. Тука не се дозволени зборовите %s." + +#: core/validators.py:288 +#, python-format +msgid "This field must match the '%s' field." +msgstr "Ова поле мора да соодејствува со полето „%s“." + +#: core/validators.py:307 +msgid "Please enter something for at least one field." +msgstr "Ве молам внесете нешто во барем едно поле." + +#: core/validators.py:316 core/validators.py:327 +msgid "Please enter both fields or leave them both empty." +msgstr "Ве молам внесете во двете полиња или оставете ги двете празни." + +#: core/validators.py:335 +#, python-format +msgid "This field must be given if %(field)s is %(value)s" +msgstr "Ова поле мора да биде зададено ако %(field)s е %(value)s" + +#: core/validators.py:348 +#, python-format +msgid "This field must be given if %(field)s is not %(value)s" +msgstr "Ова поле мора да биде зададено ако %(field)s не е %(value)s" + +#: core/validators.py:367 +msgid "Duplicate values are not allowed." +msgstr "Дупликат вредности не се дозволени." + +#: core/validators.py:382 +#, python-format +msgid "This value must be between %(lower)s and %(upper)s." +msgstr "Оваа вредноста мора да биде помеѓу %(lower)s и %(upper)s." -#: oldforms/__init__.py:392 +#: core/validators.py:384 +#, python-format +msgid "This value must be at least %s." +msgstr "Оваа вредноста мора да биде најмалку %s." + +#: core/validators.py:386 +#, python-format +msgid "This value must be no more than %s." +msgstr "Оваа вредност не смее да биде поголема од %s." + +#: core/validators.py:427 +#, python-format +msgid "This value must be a power of %s." +msgstr "Оваа вредноста мора да биде степен од %s." + +#: core/validators.py:437 +msgid "Please enter a valid decimal number." +msgstr "Ве молам внесете правилен децимален број." + +#: core/validators.py:444 +#, python-format +msgid "Please enter a valid decimal number with at most %s total digit." +msgid_plural "Please enter a valid decimal number with at most %s total digits." +msgstr[0] "Ве молам внесете правилен децимален број со најмногу %s цифрa." +msgstr[1] "Ве молам внесете правилен децимален број со најмногу %s вкупно цифри." + +#: core/validators.py:447 +#, python-format +msgid "Please enter a valid decimal number with a whole part of at most %s digit." +msgid_plural "Please enter a valid decimal number with a whole part of at most %s digits." +msgstr[0] "" +"Ве молам внесете правилен децимален број кој во целиот број има најмногу %s " +"цифра." +msgstr[1] "" +"Ве молам внесете правилен децимален број кој во целиот број има најмногу %s " +"цифри." + +#: core/validators.py:450 +#, python-format +msgid "Please enter a valid decimal number with at most %s decimal place." +msgid_plural "Please enter a valid decimal number with at most %s decimal places." +msgstr[0] "Ве молам внесете правилен децимален број кој има најмногу %s децимална цифра." +msgstr[1] "Ве молам внесете правилен децимален број кој има најмногу %s децимални цифри." + +#: core/validators.py:458 +msgid "Please enter a valid floating point number." +msgstr "Ве молам внесете правилен децимален број." + +#: core/validators.py:467 +#, python-format +msgid "Make sure your uploaded file is at least %s bytes big." +msgstr "Потврдете дека качената датотека има најмалку %s бајти." + +#: core/validators.py:468 +#, python-format +msgid "Make sure your uploaded file is at most %s bytes big." +msgstr "Потврдете дека качената датотека има најмногу %s бајти." + +#: core/validators.py:485 +msgid "The format for this field is wrong." +msgstr "Форматот за ова поле е грешен." + +#: core/validators.py:500 +msgid "This field is invalid." +msgstr "Ова поле не е правилно." + +#: core/validators.py:536 +#, python-format +msgid "Could not retrieve anything from %s." +msgstr "Неможев да извадам ништо од %s." + +#: core/validators.py:539 +#, python-format +msgid "The URL %(url)s returned the invalid Content-Type header '%(contenttype)s'." +msgstr "Адресата %(url)s врати неправилно заглавје Content-Type „%(contenttype)s“." + +#: core/validators.py:572 +#, python-format +msgid "" +"Please close the unclosed %(tag)s tag from line %(line)s. (Line starts with " +"\"%(start)s\".)" +msgstr "" +"Ве молам затворете го отворениот %(tag)s таг од линијата %(line)s. (линијата " +"почнува со „%(start)s“.)" + +#: core/validators.py:576 +#, python-format +msgid "" +"Some text starting on line %(line)s is not allowed in that context. (Line " +"starts with \"%(start)s\".)" +msgstr "" +"Некој текст кој почнува на линијата %(line)s не е дозволен во тој контекст. " +"(Линијата започнува со „%(start)s“.)" + +#: core/validators.py:581 +#, python-format +msgid "" +"\"%(attr)s\" on line %(line)s is an invalid attribute. (Line starts with \"%" +"(start)s\".)" +msgstr "" +"„%(attr)s“ на линија %(line)s е неправилен атрибут. (линијата започнува со „%" +"(start)s“.)" + +#: core/validators.py:586 +#, python-format +msgid "" +"\"<%(tag)s>\" on line %(line)s is an invalid tag. (Line starts with \"%" +"(start)s\".)" +msgstr "" +"„<%(tag)s>“ на линија %(line)s е неправилен таг. (линијата започнува со „%" +"(start)s“.)" + +#: core/validators.py:590 +#, python-format +msgid "" +"A tag on line %(line)s is missing one or more required attributes. (Line " +"starts with \"%(start)s\".)" +msgstr "" +"На таг од линијата %(line)s му недостасува еден или повеќе од потребните " +"атрибути (линијата започнува со „%(start)s“)." + +#: core/validators.py:595 +#, python-format +msgid "" +"The \"%(attr)s\" attribute on line %(line)s has an invalid value. (Line " +"starts with \"%(start)s\".)" +msgstr "Атрибутот „%(attr)s“ на линијата %(line)s има неправилна вредност (линијата започнува со „%(start)s“)." + +#: db/models/manipulators.py:308 +#, python-format +msgid "%(object)s with this %(type)s already exists for the given %(field)s." +msgstr "%(object)s од овој тип %(type)s веќе постои за даденото %(field)s." + +#: db/models/fields/__init__.py:51 +#, python-format +msgid "%(optname)s with this %(fieldname)s already exists." +msgstr "%(optname)s со ова %(fieldname)s веќе постои." + +#: 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 "Ова поле е задолжително." + +#: db/models/fields/__init__.py:448 +msgid "This value must be an integer." +msgstr "Оваа вредност мора да биде цел број." + +#: db/models/fields/__init__.py:487 +msgid "This value must be either True or False." +msgstr "Оваа вредност мора да биде или точно или неточно." + +#: db/models/fields/__init__.py:511 +msgid "This field cannot be null." +msgstr "Оваа вредност неможе да биде null." + +#: db/models/fields/__init__.py:689 +msgid "This value must be a decimal number." +msgstr "Оваа вредност мора да биде децимален број." + +#: db/models/fields/__init__.py:800 +msgid "Enter a valid filename." +msgstr "Внесите правилно име на датотека." + +#: db/models/fields/__init__.py:981 +msgid "This value must be either None, True or False." +msgstr "Оваа вредност мора да биде празна, точно или неточно." + +#: db/models/fields/related.py:94 +#, python-format +msgid "Please enter a valid %s." +msgstr "Ве молам внесете правилно %s." + +#: db/models/fields/related.py:746 +msgid "Separate multiple IDs with commas." +msgstr "Одвојте ги идентификационите броеви со запирки." + +#: db/models/fields/related.py:748 +msgid "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "Држете го „Control“ или „Command“ на Мекинтош за да изберете повеќе од едно." + +#: db/models/fields/related.py:795 +#, python-format +msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." +msgid_plural "Please enter valid %(self)s IDs. The values %(value)r are invalid." +msgstr[0] "" +"Ве молам внесете правилен %(self)s идентификацион број. Оваа вредност %" +"(value)r е неправилна." +msgstr[1] "" +"Ве молам внесете правилен %(self)s идентификацион број. Вредностите %(value)" +"r се неправилни." + +#: newforms/fields.py:47 +msgid "Enter a valid value." +msgstr "Внесете правилна вредност." + +#: newforms/fields.py:124 +#, python-format +msgid "Ensure this value has at most %(max)d characters (it has %(length)d)." +msgstr "Осигурајте се дека оваа вредност има најмногу %(max)d знаци (има %(length)d)." + +#: newforms/fields.py:125 +#, python-format +msgid "Ensure this value has at least %(min)d characters (it has %(length)d)." +msgstr "Осигурајте се дека оваа вредност има најмалку %(min)d знаци (има %(length)d)." + +#: newforms/fields.py:153 newforms/fields.py:182 newforms/fields.py:211 +#, python-format +msgid "Ensure this value is less than or equal to %s." +msgstr "Осигурајте се дека оваа вредност е помала или еднаква со %s." + +#: newforms/fields.py:154 newforms/fields.py:183 newforms/fields.py:212 +#, python-format +msgid "Ensure this value is greater than or equal to %s." +msgstr "Осигурајте се дека оваа вредност е поголема или еднаква со %s." + +#: newforms/fields.py:181 newforms/fields.py:210 +msgid "Enter a number." +msgstr "Внесете број." + +#: newforms/fields.py:213 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "Осигурајте се дека нема вкупно повеќе од %s цифри." + +#: newforms/fields.py:214 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "Осигурајте се дека нема повеќе од %s децимални места." + +#: newforms/fields.py:215 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "Осигурајте се дека нема повеќе од %s цифри пред децималната запирка." + +#: newforms/fields.py:263 newforms/fields.py:750 +msgid "Enter a valid date." +msgstr "Внесете правилен датум." + +#: newforms/fields.py:296 newforms/fields.py:751 +msgid "Enter a valid time." +msgstr "Внесете правилно време." + +#: newforms/fields.py:335 +msgid "Enter a valid date/time." +msgstr "Внесете правилен датум со време." + +#: newforms/fields.py:434 +msgid "No file was submitted." +msgstr "Не беше пратена датотека." + +#: newforms/fields.py:435 oldforms/__init__.py:689 +msgid "The submitted file is empty." +msgstr "Пратената датотека е празна." + +#: newforms/fields.py:497 +msgid "Enter a valid URL." +msgstr "Внесете правилна адреса." + +#: newforms/fields.py:498 +msgid "This URL appears to be a broken link." +msgstr "Оваа адреса изгледа дека не е достапна." + +#: newforms/fields.py:559 newforms/models.py:299 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "Изберете правилно. Тоа не е едно од можните избори." + +#: newforms/fields.py:598 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "Внесете валиден избор. %(value)s не е еден од можните избори." + +#: newforms/fields.py:599 newforms/fields.py:661 newforms/models.py:365 +msgid "Enter a list of values." +msgstr "Внесете листа на вредности." + +#: newforms/fields.py:779 +msgid "Enter a valid IPv4 address." +msgstr "Внесeте правилна IPv4 адреса." + +#: newforms/models.py:366 +#, python-format +msgid "Select a valid choice. %s is not one of the available choices." +msgstr "Внесете правилно. %s не е еден од достапните вредности." + +#: oldforms/__init__.py:409 #, python-format msgid "Ensure your text is less than %s character." msgid_plural "Ensure your text is less than %s characters." msgstr[0] "Осигурајте се дека вашиот текст има помалку од %s знак." msgstr[1] "Осигурајте се дека вашиот текст има помалку од %s знаци." -#: oldforms/__init__.py:397 +#: oldforms/__init__.py:414 msgid "Line breaks are not allowed here." msgstr "Тука не се дозволени прекини на линија." -#: oldforms/__init__.py:498 oldforms/__init__.py:571 oldforms/__init__.py:610 +#: oldforms/__init__.py:512 oldforms/__init__.py:586 oldforms/__init__.py:625 #, python-format msgid "Select a valid choice; '%(data)s' is not in %(choices)s." msgstr "Изберете правилно, %(data)s' не е во %(choices)s." -#: oldforms/__init__.py:674 -msgid "The submitted file is empty." -msgstr "Пратената датотека е празна." - -#: oldforms/__init__.py:730 +#: oldforms/__init__.py:745 msgid "Enter a whole number between -32,768 and 32,767." msgstr "Внесете цел број помеѓу -32,768 и 32,767." -#: oldforms/__init__.py:740 +#: oldforms/__init__.py:755 msgid "Enter a positive number." msgstr "Внесете позитивен број." -#: oldforms/__init__.py:750 +#: oldforms/__init__.py:765 msgid "Enter a whole number between 0 and 32,767." msgstr "Внесете цел број помеѓу 0 и 32,767." -#: views/generic/create_update.py:43 +#: template/defaultfilters.py:698 +msgid "yes,no,maybe" +msgstr "да, не, можеби" + +#: template/defaultfilters.py:729 #, python-format -msgid "The %(verbose_name)s was created successfully." -msgstr "%(verbose_name)s беше успешно создаден." +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "%(size)d бајт" +msgstr[1] "%(size)d бајти" -#: views/generic/create_update.py:117 +#: template/defaultfilters.py:731 #, python-format -msgid "The %(verbose_name)s was updated successfully." -msgstr "%(verbose_name)s беше успешно ажуриран." +msgid "%.1f KB" +msgstr "%.1f КБ" -#: views/generic/create_update.py:184 +#: template/defaultfilters.py:733 #, python-format -msgid "The %(verbose_name)s was deleted." -msgstr "%(verbose_name)s беше избришан." +msgid "%.1f MB" +msgstr "%.1f МБ" + +#: template/defaultfilters.py:734 +#, python-format +msgid "%.1f GB" +msgstr "%.1f ГБ" + +#: utils/dateformat.py:41 +msgid "p.m." +msgstr "попладне" + +#: utils/dateformat.py:42 +msgid "a.m." +msgstr "наутро" + +#: utils/dateformat.py:47 +msgid "PM" +msgstr "попладне" + +#: utils/dateformat.py:48 +msgid "AM" +msgstr "наутро" + +#: utils/dateformat.py:97 +msgid "midnight" +msgstr "полноќ" + +#: utils/dateformat.py:99 +msgid "noon" +msgstr "пладне" #: utils/dates.py:6 msgid "Monday" @@ -2488,268 +4110,244 @@ msgstr "сабота" msgid "Sunday" msgstr "недела" -#: utils/dates.py:14 +#: utils/dates.py:10 +msgid "Mon" +msgstr "пон" + +#: utils/dates.py:10 +msgid "Tue" +msgstr "вто" + +#: utils/dates.py:10 +msgid "Wed" +msgstr "сре" + +#: utils/dates.py:10 +msgid "Thu" +msgstr "чет" + +#: utils/dates.py:10 +msgid "Fri" +msgstr "пет" + +#: utils/dates.py:11 +msgid "Sat" +msgstr "саб" + +#: utils/dates.py:11 +msgid "Sun" +msgstr "нед" + +#: utils/dates.py:18 msgid "January" msgstr "јануари" -#: utils/dates.py:14 +#: utils/dates.py:18 msgid "February" msgstr "февруари" -#: utils/dates.py:14 utils/dates.py:27 +#: utils/dates.py:18 utils/dates.py:31 msgid "March" msgstr "март" -#: utils/dates.py:14 utils/dates.py:27 +#: utils/dates.py:18 utils/dates.py:31 msgid "April" msgstr "април" -#: utils/dates.py:14 utils/dates.py:27 +#: utils/dates.py:18 utils/dates.py:31 msgid "May" msgstr "мај" -#: utils/dates.py:14 utils/dates.py:27 +#: utils/dates.py:18 utils/dates.py:31 msgid "June" msgstr "јуни" -#: utils/dates.py:15 utils/dates.py:27 +#: utils/dates.py:19 utils/dates.py:31 msgid "July" msgstr "јули" -#: utils/dates.py:15 +#: utils/dates.py:19 msgid "August" msgstr "август" -#: utils/dates.py:15 +#: utils/dates.py:19 msgid "September" msgstr "септември" -#: utils/dates.py:15 +#: utils/dates.py:19 msgid "October" msgstr "октомври" -#: utils/dates.py:15 +#: utils/dates.py:19 msgid "November" msgstr "ноември" -#: utils/dates.py:16 +#: utils/dates.py:20 msgid "December" msgstr "декември" -#: utils/dates.py:19 +#: utils/dates.py:23 msgid "jan" msgstr "јан" -#: utils/dates.py:19 +#: utils/dates.py:23 msgid "feb" msgstr "фев" -#: utils/dates.py:19 +#: utils/dates.py:23 msgid "mar" msgstr "мар" -#: utils/dates.py:19 +#: utils/dates.py:23 msgid "apr" msgstr "апр" -#: utils/dates.py:19 +#: utils/dates.py:23 msgid "may" msgstr "мај" -#: utils/dates.py:19 +#: utils/dates.py:23 msgid "jun" msgstr "јун" -#: utils/dates.py:20 +#: utils/dates.py:24 msgid "jul" msgstr "јул" -#: utils/dates.py:20 +#: utils/dates.py:24 msgid "aug" msgstr "авг" -#: utils/dates.py:20 +#: utils/dates.py:24 msgid "sep" msgstr "сеп" -#: utils/dates.py:20 +#: utils/dates.py:24 msgid "oct" msgstr "окт" -#: utils/dates.py:20 +#: utils/dates.py:24 msgid "nov" msgstr "ное" -#: utils/dates.py:20 +#: utils/dates.py:24 msgid "dec" msgstr "дек" -#: utils/dates.py:27 +#: utils/dates.py:31 msgid "Jan." msgstr "јан." -#: utils/dates.py:27 +#: utils/dates.py:31 msgid "Feb." msgstr "фев." -#: utils/dates.py:28 +#: utils/dates.py:32 msgid "Aug." msgstr "авг." -#: utils/dates.py:28 +#: utils/dates.py:32 msgid "Sept." msgstr "сеп." -#: utils/dates.py:28 +#: utils/dates.py:32 msgid "Oct." msgstr "окт." -#: utils/dates.py:28 +#: utils/dates.py:32 msgid "Nov." msgstr "ное." -#: utils/dates.py:28 +#: utils/dates.py:32 msgid "Dec." msgstr "дек." -#: utils/timesince.py:12 +#: utils/text.py:127 +msgid "or" +msgstr "или" + +#: utils/timesince.py:21 msgid "year" msgid_plural "years" msgstr[0] "година" msgstr[1] "години" -#: utils/timesince.py:13 +#: utils/timesince.py:22 msgid "month" msgid_plural "months" msgstr[0] "месец" msgstr[1] "месеци" -#: utils/timesince.py:14 +#: utils/timesince.py:23 msgid "week" msgid_plural "weeks" msgstr[0] "седмица" msgstr[1] "седмици" -#: utils/timesince.py:15 +#: utils/timesince.py:24 msgid "day" msgid_plural "days" msgstr[0] "ден" msgstr[1] "дена" -#: utils/timesince.py:16 +#: utils/timesince.py:25 msgid "hour" msgid_plural "hours" msgstr[0] "час" msgstr[1] "часа" -#: utils/timesince.py:17 +#: utils/timesince.py:26 msgid "minute" msgid_plural "minutes" msgstr[0] "минута" msgstr[1] "минути" -#: utils/dateformat.py:40 -msgid "p.m." -msgstr "попладне" - -#: utils/dateformat.py:41 -msgid "a.m." -msgstr "наутро" - -#: utils/dateformat.py:46 -msgid "PM" -msgstr "попладне" +#: utils/timesince.py:46 +msgid "minutes" +msgstr "минути" -#: utils/dateformat.py:47 -msgid "AM" -msgstr "наутро" - -#: utils/dateformat.py:95 -msgid "midnight" -msgstr "полноќ" +#: utils/timesince.py:51 +#, python-format +msgid "%(number)d %(type)s" +msgstr "%(number)d %(type)s" -#: utils/dateformat.py:97 -msgid "noon" -msgstr "пладне" +#: utils/timesince.py:57 +#, python-format +msgid ", %(number)d %(type)s" +msgstr ", %(number)d %(type)s" -#: utils/translation/trans_real.py:358 +#: utils/translation/trans_real.py:403 msgid "DATE_FORMAT" msgstr "j M Y" -#: utils/translation/trans_real.py:359 +#: utils/translation/trans_real.py:404 msgid "DATETIME_FORMAT" msgstr "j M Y, P" -#: utils/translation/trans_real.py:360 +#: utils/translation/trans_real.py:405 msgid "TIME_FORMAT" msgstr "P" -#: utils/translation/trans_real.py:376 +#: utils/translation/trans_real.py:421 msgid "YEAR_MONTH_FORMAT" msgstr "F Y" -#: utils/translation/trans_real.py:377 +#: utils/translation/trans_real.py:422 msgid "MONTH_DAY_FORMAT" msgstr "F j" -#: template/defaultfilters.py:491 -msgid "yes,no,maybe" -msgstr "да, не, можеби" - -#: newforms/fields.py:103 newforms/fields.py:256 -#, python-format -msgid "Ensure this value has at most %d characters." -msgstr "Осигурајте се дека оваа вредност има најмногу %d знаци." - -#: newforms/fields.py:105 newforms/fields.py:258 -#, python-format -msgid "Ensure this value has at least %d characters." -msgstr "Осигурајте се дека оваа вредност има најмалку %d знаци." - -#: newforms/fields.py:130 +#: views/generic/create_update.py:43 #, python-format -msgid "Ensure this value is less than or equal to %s." -msgstr "Осигурајте се дека оваа вредност е помала или еднаква на %s." +msgid "The %(verbose_name)s was created successfully." +msgstr "%(verbose_name)s беше успешно создаден." -#: newforms/fields.py:132 +#: views/generic/create_update.py:117 #, python-format -msgid "Ensure this value is greater than or equal to %s." -msgstr "Осигурајте се дека оваа вредност е поголема или еднаква со %s." - -#: newforms/fields.py:165 -msgid "Enter a valid date." -msgstr "Внесете правилен датум." - -#: newforms/fields.py:192 -msgid "Enter a valid time." -msgstr "Внесете правилно време." - -#: newforms/fields.py:228 -msgid "Enter a valid date/time." -msgstr "Внесете правилен датум со време." - -#: newforms/fields.py:242 -msgid "Enter a valid value." -msgstr "Внесете правилна вредност." - -#: newforms/fields.py:289 newforms/fields.py:311 -msgid "Enter a valid URL." -msgstr "Внесете правилна адреса." - -#: newforms/fields.py:313 -msgid "This URL appears to be a broken link." -msgstr "Оваа адреса изгледа дека не е достапна." - -#: newforms/fields.py:362 newforms/models.py:165 -msgid "Select a valid choice. That choice is not one of the available choices." -msgstr "Изберете правилно. Тоа не е едно од можните избори." - -#: newforms/fields.py:380 newforms/fields.py:456 newforms/models.py:182 -msgid "Enter a list of values." -msgstr "Внесете листа на вредности." +msgid "The %(verbose_name)s was updated successfully." +msgstr "%(verbose_name)s беше успешно ажуриран." -#: newforms/fields.py:389 newforms/models.py:188 +#: views/generic/create_update.py:184 #, python-format -msgid "Select a valid choice. %s is not one of the available choices." -msgstr "Внесете правилно. %s не е еден од достапните вредности." +msgid "The %(verbose_name)s was deleted." +msgstr "%(verbose_name)s беше избришан." diff --git a/django/conf/locale/tr/LC_MESSAGES/django.mo b/django/conf/locale/tr/LC_MESSAGES/django.mo Binary files differindex 2d409001f4..bb3b1af83c 100644 --- a/django/conf/locale/tr/LC_MESSAGES/django.mo +++ b/django/conf/locale/tr/LC_MESSAGES/django.mo diff --git a/django/conf/locale/tr/LC_MESSAGES/django.po b/django/conf/locale/tr/LC_MESSAGES/django.po index dd2c592cfb..224cc7fba5 100644 --- a/django/conf/locale/tr/LC_MESSAGES/django.po +++ b/django/conf/locale/tr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-03-14 01:38+0200\n" -"PO-Revision-Date: 2007-12-30 12:15+0200\n" +"PO-Revision-Date: 2008-07-02 23:37+0200\n" "Last-Translator: Can Burak Çilingir <canburak@cs.bilgi.edu.tr>\n" "Language-Team: Turkish <bahadir@pardus.org.tr>\n" "MIME-Version: 1.0\n" @@ -2466,3 +2466,8 @@ msgstr "evet,hayır,olabilir" #~ msgid "Have you <a href=\"/password_reset/\">forgotten your password</a>?" #~ msgstr "<a href=\"/password_reset/\">Şifrenizi mi unuttunuz?</a>" + +#: contrib/auth/forms.py:107 +#, python-format +msgid "Password reset on %s" +msgstr "%s sitesindeki hesabınızın parolasının sıfırlanması" diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index bd14c9b4b9..bba883ba78 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -1,14 +1,15 @@ +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + from django.db import connection from django.contrib.auth.models import User -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - + class ModelBackend(object): """ - Authenticate against django.contrib.auth.models.User + Authenticates against django.contrib.auth.models.User. """ # TODO: Model, login attribute name and password attribute name should be # configurable. @@ -21,7 +22,10 @@ class ModelBackend(object): return None def get_group_permissions(self, user_obj): - "Returns a list of permission strings that this user has through his/her groups." + """ + Returns a set of permission strings that this user has through his/her + groups. + """ if not hasattr(user_obj, '_group_perm_cache'): cursor = connection.cursor() # The SQL below works out to the following, after DB quoting: @@ -50,7 +54,7 @@ class ModelBackend(object): cursor.execute(sql, [user_obj.id]) user_obj._group_perm_cache = set(["%s.%s" % (row[0], row[1]) for row in cursor.fetchall()]) return user_obj._group_perm_cache - + def get_all_permissions(self, user_obj): if not hasattr(user_obj, '_perm_cache'): user_obj._perm_cache = set([u"%s.%s" % (p.content_type.app_label, p.codename) for p in user_obj.user_permissions.select_related()]) @@ -61,7 +65,13 @@ class ModelBackend(object): return perm in self.get_all_permissions(user_obj) def has_module_perms(self, user_obj, app_label): - return bool(len([p for p in self.get_all_permissions(user_obj) if p[:p.index('.')] == app_label])) + """ + Returns True if user_obj has any permissions in the given app_label. + """ + for perm in self.get_all_permissions(user_obj): + if perm[:perm.index('.')] == app_label: + return True + return False def get_user(self, user_id): try: diff --git a/django/contrib/auth/fixtures/authtestdata.json b/django/contrib/auth/fixtures/authtestdata.json new file mode 100644 index 0000000000..e0bdc24e72 --- /dev/null +++ b/django/contrib/auth/fixtures/authtestdata.json @@ -0,0 +1,56 @@ +[ + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "testclient", + "first_name": "Test", + "last_name": "Client", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "2", + "model": "auth.user", + "fields": { + "username": "inactive", + "first_name": "Inactive", + "last_name": "User", + "is_active": false, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { + "pk": "3", + "model": "auth.user", + "fields": { + "username": "staff", + "first_name": "Staff", + "last_name": "Member", + "is_active": true, + "is_superuser": false, + "is_staff": true, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "staffmember@example.com", + "date_joined": "2006-12-17 07:03:31" + } + } +] diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index f74d1d7761..379a9f4c64 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -96,10 +96,10 @@ class Group(models.Model): class Meta: verbose_name = _('group') verbose_name_plural = _('groups') - ordering = ('name',) class Admin: search_fields = ('name',) + ordering = ('name',) def __unicode__(self): return self.name @@ -153,7 +153,6 @@ class User(models.Model): class Meta: verbose_name = _('user') verbose_name_plural = _('users') - ordering = ('username',) class Admin: fields = ( @@ -166,6 +165,7 @@ class User(models.Model): list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') list_filter = ('is_staff', 'is_superuser') search_fields = ('username', 'first_name', 'last_name', 'email') + ordering = ('username',) def __unicode__(self): return self.username diff --git a/django/contrib/auth/tests.py b/django/contrib/auth/tests.py index 81094ca85f..ea1ac26c21 100644 --- a/django/contrib/auth/tests.py +++ b/django/contrib/auth/tests.py @@ -1,5 +1,5 @@ """ ->>> from models import User, AnonymousUser +>>> from django.contrib.auth.models import User, AnonymousUser >>> u = User.objects.create_user('testuser', 'test@example.com', 'testpw') >>> u.has_usable_password() True @@ -52,4 +52,24 @@ Superuser created successfully. u'joe@somewhere.org' >>> u.password u'!' -"""
\ No newline at end of file +""" + +from django.test import TestCase +from django.core import mail + +class PasswordResetTest(TestCase): + fixtures = ['authtestdata.json'] + urls = 'django.contrib.auth.urls' + def test_email_not_found(self): + "Error is raised if the provided email address isn't currently registered" + response = self.client.get('/password_reset/') + self.assertEquals(response.status_code, 200) + response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'}) + self.assertContains(response, "That e-mail address doesn't have an associated user account") + self.assertEquals(len(mail.outbox), 0) + + def test_email_found(self): + "Email is sent if a valid email address is provided for password reset" + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEquals(response.status_code, 302) + self.assertEquals(len(mail.outbox), 1) diff --git a/django/contrib/auth/urls.py b/django/contrib/auth/urls.py new file mode 100644 index 0000000000..5ddfcf15e8 --- /dev/null +++ b/django/contrib/auth/urls.py @@ -0,0 +1,13 @@ +# These URLs are normally mapped to /admin/urls.py. This URLs file is +# provided as a convenience to those who want to deploy these URLs elsewhere. +# This file is also used to provide a reliable view deployment for test purposes. + +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + ('^logout/$', 'django.contrib.auth.views.logout'), + ('^password_change/$', 'django.contrib.auth.views.password_change'), + ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'), + ('^password_reset/$', 'django.contrib.auth.views.password_reset') +) + diff --git a/django/contrib/flatpages/models.py b/django/contrib/flatpages/models.py index ddd5678379..d61e9a3b1c 100644 --- a/django/contrib/flatpages/models.py +++ b/django/contrib/flatpages/models.py @@ -8,7 +8,7 @@ class FlatPage(models.Model): url = models.CharField(_('URL'), max_length=100, validator_list=[validators.isAlphaNumericURL], db_index=True, help_text=_("Example: '/about/contact/'. Make sure to have leading and trailing slashes.")) title = models.CharField(_('title'), max_length=200) - content = models.TextField(_('content')) + content = models.TextField(_('content'), blank=True) enable_comments = models.BooleanField(_('enable comments')) template_name = models.CharField(_('template name'), max_length=70, blank=True, help_text=_("Example: 'flatpages/contact_page.html'. If this isn't provided, the system will use 'flatpages/default.html'.")) diff --git a/django/contrib/formtools/tests.py b/django/contrib/formtools/tests.py index 143857ad58..ba241e9dc6 100644 --- a/django/contrib/formtools/tests.py +++ b/django/contrib/formtools/tests.py @@ -21,18 +21,14 @@ class TestForm(forms.Form): class PreviewTests(TestCase): + urls = 'django.contrib.formtools.test_urls' def setUp(self): - self._old_root_urlconf = settings.ROOT_URLCONF - settings.ROOT_URLCONF = 'django.contrib.formtools.test_urls' # Create a FormPreview instance to share between tests self.preview = preview.FormPreview(TestForm) input_template = '<input type="hidden" name="%s" value="%s" />' self.input = input_template % (self.preview.unused_name('stage'), "%d") - def tearDown(self): - settings.ROOT_URLCONF = self._old_root_urlconf - def test_unused_name(self): """ Verifies name mangling to get uniue field name. diff --git a/django/contrib/gis/db/backend/mysql/query.py b/django/contrib/gis/db/backend/mysql/query.py index 3f21478240..6522235135 100644 --- a/django/contrib/gis/db/backend/mysql/query.py +++ b/django/contrib/gis/db/backend/mysql/query.py @@ -38,12 +38,12 @@ MISC_TERMS = ['isnull'] # Assacceptable lookup types for Oracle spatial.
MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
MYSQL_GIS_TERMS += MISC_TERMS
-MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable
+MYSQL_GIS_TERMS = dict((term, None) for term in MYSQL_GIS_TERMS) # Making dictionary
-def get_geo_where_clause(lookup_type, table_prefix, field, value):
+def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
# Getting the quoted field as `geo_col`.
- geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
+ geo_col = '%s.%s' % (qn(table_alias), qn(name))
# See if a MySQL Geometry function matches the lookup type next
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
@@ -54,6 +54,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value): # TODO: Is this needed because MySQL cannot handle NULL
# geometries in its spatial indices.
if lookup_type == 'isnull':
- return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
+ return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
diff --git a/django/contrib/gis/db/backend/oracle/query.py b/django/contrib/gis/db/backend/oracle/query.py index 6f5c0dfe94..a073b7b45f 100644 --- a/django/contrib/gis/db/backend/oracle/query.py +++ b/django/contrib/gis/db/backend/oracle/query.py @@ -110,10 +110,10 @@ ORACLE_SPATIAL_TERMS += MISC_TERMS ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
#### The `get_geo_where_clause` function for Oracle ####
-def get_geo_where_clause(lookup_type, table_prefix, field, value):
+def get_geo_where_clause(table_alias, name, lookup_type, geo_annot):
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
# Getting the quoted table name as `geo_col`.
- geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
+ geo_col = '%s.%s' % (qn(table_alias), qn(name))
# See if a Oracle Geometry function matches the lookup type next
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
@@ -126,19 +126,19 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value): sdo_op, arg_type = lookup_info
# Ensuring that a tuple _value_ was passed in from the user
- if not isinstance(value, tuple):
+ if not isinstance(geo_annot.value, tuple):
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
- if len(value) != 2:
+ if len(geo_annot.value) != 2:
raise ValueError('2-element tuple required for %s lookup type.' % lookup_type)
# Ensuring the argument type matches what we expect.
- if not isinstance(value[1], arg_type):
- raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
+ if not isinstance(geo_annot.value[1], arg_type):
+ raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1])))
if lookup_type == 'relate':
# The SDORelate class handles construction for these queries,
# and verifies the mask argument.
- return sdo_op(value[1]).as_sql(geo_col)
+ return sdo_op(geo_annot.value[1]).as_sql(geo_col)
else:
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
return sdo_op.as_sql(geo_col)
@@ -149,6 +149,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value): return lookup_info.as_sql(geo_col)
elif lookup_type == 'isnull':
# Handling 'isnull' lookup type
- return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
+ return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or ''))
raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
diff --git a/django/contrib/gis/db/backend/postgis/query.py b/django/contrib/gis/db/backend/postgis/query.py index 90063e1887..8780780402 100644 --- a/django/contrib/gis/db/backend/postgis/query.py +++ b/django/contrib/gis/db/backend/postgis/query.py @@ -218,7 +218,7 @@ MISC_TERMS = ['isnull'] POSTGIS_TERMS = POSTGIS_OPERATORS.keys() # Getting the operators first POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Functions POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull') -POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable +POSTGIS_TERMS = dict((term, None) for term in POSTGIS_TERMS) # Making a dictionary for fast lookups # For checking tuple parameters -- not very pretty but gets job done. def exactly_two(val): return val == 2 @@ -228,10 +228,10 @@ def num_params(lookup_type, val): else: return exactly_two(val) #### The `get_geo_where_clause` function for PostGIS. #### -def get_geo_where_clause(lookup_type, table_prefix, field, value): +def get_geo_where_clause(table_alias, name, lookup_type, geo_annot): "Returns the SQL WHERE clause for use in PostGIS SQL construction." # Getting the quoted field as `geo_col`. - geo_col = '%s.%s' % (qn(table_prefix), qn(field.column)) + geo_col = '%s.%s' % (qn(table_alias), qn(name)) if lookup_type in POSTGIS_OPERATORS: # See if a PostGIS operator matches the lookup type. return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col) @@ -248,30 +248,31 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value): op, arg_type = tmp # Ensuring that a tuple _value_ was passed in from the user - if not isinstance(value, (tuple, list)): + if not isinstance(geo_annot.value, (tuple, list)): raise TypeError('Tuple required for `%s` lookup type.' % lookup_type) + # Number of valid tuple parameters depends on the lookup type. - nparams = len(value) + nparams = len(geo_annot.value) if not num_params(lookup_type, nparams): raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type) # Ensuring the argument type matches what we expect. - if not isinstance(value[1], arg_type): - raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) + if not isinstance(geo_annot.value[1], arg_type): + raise TypeError('Argument type should be %s, got %s instead.' % (arg_type, type(geo_annot.value[1]))) # For lookup type `relate`, the op instance is not yet created (has # to be instantiated here to check the pattern parameter). if lookup_type == 'relate': - op = op(value[1]) + op = op(geo_annot.value[1]) elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': - if field.geodetic: + if geo_annot.geodetic: # Geodetic distances are only availble from Points to PointFields. - if field._geom != 'POINT': + if geo_annot.geom_type != 'POINT': raise TypeError('PostGIS spherical operations are only valid on PointFields.') - if value[0].geom_typeid != 0: + if geo_annot.value[0].geom_typeid != 0: raise TypeError('PostGIS geometry distance parameter is required to be of type Point.') # Setting up the geodetic operation appropriately. - if nparams == 3 and value[2] == 'spheroid': op = op[2] + if nparams == 3 and geo_annot.value[2] == 'spheroid': op = op[2] else: op = op[1] else: op = op[0] @@ -281,6 +282,6 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value): return op.as_sql(geo_col) elif lookup_type == 'isnull': # Handling 'isnull' lookup type - return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or '')) + return "%s IS %sNULL" % (geo_col, (not geo_annot.value and 'NOT ' or '')) raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type)) diff --git a/django/contrib/gis/db/models/fields/__init__.py b/django/contrib/gis/db/models/fields/__init__.py index 0a0d144dcc..4bb294c52d 100644 --- a/django/contrib/gis/db/models/fields/__init__.py +++ b/django/contrib/gis/db/models/fields/__init__.py @@ -111,7 +111,7 @@ class GeometryField(SpatialBackend.Field): except SpatialBackend.GeometryException: raise ValueError('Could not create geometry from lookup value: %s' % str(value)) else: - raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value)) + raise TypeError('Cannot use parameter of `%s` type as a geometry lookup parameter.' % type(value)) # Assigning the SRID value. geom.srid = self.get_srid(geom) diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index d5ea54e116..20f695b26f 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -103,7 +103,8 @@ class GeoQuery(sql.Query): self._select_aliases = aliases return result - def get_default_columns(self, with_aliases=False, col_aliases=None): + def get_default_columns(self, with_aliases=False, col_aliases=None, + start_alias=None, opts=None, as_pairs=False): """ Computes the default columns for selecting every field in the base model. @@ -115,11 +116,14 @@ class GeoQuery(sql.Query): geometry columns. """ result = [] - table_alias = self.tables[0] + if opts is None: + opts = self.model._meta + if start_alias: + table_alias = start_alias + else: + table_alias = self.tables[0] root_pk = self.model._meta.pk.column seen = {None: table_alias} - qn = self.quote_name_unless_alias - qn2 = self.connection.ops.quote_name aliases = set() for field, model in self.model._meta.get_fields_with_model(): try: @@ -128,12 +132,13 @@ class GeoQuery(sql.Query): alias = self.join((table_alias, model._meta.db_table, root_pk, model._meta.pk.column)) seen[model] = alias - + if as_pairs: + result.append((alias, field.column)) + continue # This part of the function is customized for GeoQuery. We # see if there was any custom selection specified in the # dictionary, and set up the selection format appropriately. field_sel = self.get_field_select(field, alias) - if with_aliases and field.column in col_aliases: c_alias = 'Col%d' % len(col_aliases) result.append('%s AS %s' % (field_sel, c_alias)) @@ -145,6 +150,8 @@ class GeoQuery(sql.Query): aliases.add(r) if with_aliases: col_aliases.add(field.column) + if as_pairs: + return result, None return result, aliases def get_ordering(self): diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index d9c75eaec3..68f5e247a0 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -1,21 +1,78 @@ +import datetime +from django.db.models.fields import Field from django.db.models.sql.where import WhereNode from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend +class GeoAnnotation(object): + """ + The annotation used for GeometryFields; basically a placeholder + for metadata needed by the `get_geo_where_clause` of the spatial + backend. + """ + def __init__(self, field, value, where): + self.geodetic = field.geodetic + self.geom_type = field._geom + self.value = value + self.where = tuple(where) + class GeoWhereNode(WhereNode): """ - The GeoWhereNode calls the `get_geo_where_clause` from the appropriate - spatial backend in order to construct correct spatial SQL. + Used to represent the SQL where-clause for spatial databases -- + these are tied to the GeoQuery class that created it. """ + def add(self, data, connector): + """ + This is overridden from the regular WhereNode to handle the + peculiarties of GeometryFields, because they need a special + annotation object that contains the spatial metadata from the + field so that the correct spatial SQL is generated. + """ + if not isinstance(data, (list, tuple)): + super(WhereNode, self).add(data, connector) + return + + alias, col, field, lookup_type, value = data + # Do we have a geographic field? + geo_field = hasattr(field, '_geom') + if field: + if geo_field: + # `GeometryField.get_db_prep_lookup` returns a where clause + # substitution array in addition to the parameters. + where, params = field.get_db_prep_lookup(lookup_type, value) + else: + params = field.get_db_prep_lookup(lookup_type, value) + db_type = field.db_type() + else: + # This is possible when we add a comparison to NULL sometimes (we + # don't really need to waste time looking up the associated field + # object). + params = Field().get_db_prep_lookup(lookup_type, value) + db_type = None + + if geo_field: + # The annotation will be a `GeoAnnotation` object that + # will contain the necessary geometry field metadata for + # the `get_geo_where_clause` to construct the appropriate + # spatial SQL when `make_atom` is called. + annotation = GeoAnnotation(field, value, where) + elif isinstance(value, datetime.datetime): + annotation = datetime.datetime + else: + annotation = bool(value) + + super(WhereNode, self).add((alias, col, db_type, lookup_type, + annotation, params), connector) + def make_atom(self, child, qn): - table_alias, name, field, lookup_type, value = child - if hasattr(field, '_geom'): + table_alias, name, db_type, lookup_type, value_annot, params = child + + if isinstance(value_annot, GeoAnnotation): if lookup_type in SpatialBackend.gis_terms: # Getting the geographic where clause; substitution parameters # will be populated in the GeoFieldSQL object returned by the # GeometryField. - gwc = get_geo_where_clause(lookup_type, table_alias, field, value) - where, params = field.get_db_prep_lookup(lookup_type, value) - return gwc % tuple(where), params + gwc = get_geo_where_clause(table_alias, name, lookup_type, value_annot) + return gwc % value_annot.where, params else: raise TypeError('Invalid lookup type: %r' % lookup_type) else: diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index b17e3d741c..057e2f715e 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -88,14 +88,14 @@ class DistanceTest(unittest.TestCase): else: dist = dist[0] # Creating the query set. - qs = AustraliaCity.objects.filter(point__dwithin=(self.au_pnt, dist)).order_by('name') + qs = AustraliaCity.objects.order_by('name') if type_error: # A TypeError should be raised on PostGIS when trying to pass # Distance objects into a DWithin query using a geodetic field. - self.assertRaises(TypeError, qs.count) + self.assertRaises(TypeError, AustraliaCity.objects.filter, point__dwithin=(self.au_pnt, dist)) else: - self.assertEqual(au_cities, self.get_names(qs)) - + self.assertEqual(au_cities, self.get_names(qs.filter(point__dwithin=(self.au_pnt, dist)))) + def test03a_distance_method(self): "Testing the `distance` GeoQuerySet method on projected coordinate systems." # The point for La Grange, TX @@ -217,11 +217,11 @@ class DistanceTest(unittest.TestCase): AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100)))) # Too many params (4 in this case) should raise a ValueError. self.assertRaises(ValueError, - AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')).count) + AustraliaCity.objects.filter, point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')) # Not enough params should raise a ValueError. self.assertRaises(ValueError, - AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)',)).count) + AustraliaCity.objects.filter, point__distance_lte=('POINT(5 23)',)) # Getting all cities w/in 550 miles of Hobart. hobart = AustraliaCity.objects.get(name='Hobart') diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index 82dc5e9a9e..f3cbf9b1f6 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -198,6 +198,7 @@ class GeoModelTest(unittest.TestCase): @no_oracle def test06_make_line(self): "Testing the `make_line` GeoQuerySet method." + if DISABLE: return # Ensuring that a `TypeError` is raised on models without PointFields. self.assertRaises(TypeError, State.objects.make_line) self.assertRaises(TypeError, Country.objects.make_line) @@ -381,14 +382,13 @@ class GeoModelTest(unittest.TestCase): pnt1 = fromstr('POINT (649287.0363174 4177429.4494686)', srid=2847) pnt2 = fromstr('POINT(-98.4919715741052 29.4333344025053)', srid=4326) - # Testing bad argument tuples that should return a TypeError or - # a ValueError. - bad_args = [((pnt1, 0), TypeError), - ((pnt2, 'T*T***FF*', 0), ValueError), - ((23, 'foo'), ValueError), - ] - for args, e in bad_args: - qs = Country.objects.filter(mpoly__relate=args) + # Not passing in a geometry as first param shoud + # raise a type error when initializing the GeoQuerySet + self.assertRaises(TypeError, Country.objects.filter, mpoly__relate=(23, 'foo')) + # Making sure the right exception is raised for the given + # bad arguments. + for bad_args, e in [((pnt1, 0), TypeError), ((pnt2, 'T*T***FF*', 0), ValueError)]: + qs = Country.objects.filter(mpoly__relate=bad_args) self.assertRaises(e, qs.count) # Relate works differently for the different backends. @@ -471,6 +471,7 @@ class GeoModelTest(unittest.TestCase): def test19_centroid(self): "Testing the `centroid` GeoQuerySet method." + if DISABLE: return qs = State.objects.exclude(poly__isnull=True).centroid() if oracle: tol = 0.1 else: tol = 0.000000001 @@ -479,6 +480,7 @@ class GeoModelTest(unittest.TestCase): def test20_pointonsurface(self): "Testing the `point_on_surface` GeoQuerySet method." + if DISABLE: return # Reference values. if SpatialBackend.oracle: # SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) FROM GEOAPP_COUNTRY; @@ -497,6 +499,7 @@ class GeoModelTest(unittest.TestCase): @no_oracle def test21_scale(self): "Testing the `scale` GeoQuerySet method." + if DISABLE: return xfac, yfac = 2, 3 qs = Country.objects.scale(xfac, yfac, model_att='scaled') for c in qs: @@ -509,6 +512,7 @@ class GeoModelTest(unittest.TestCase): @no_oracle def test22_translate(self): "Testing the `translate` GeoQuerySet method." + if DISABLE: return xfac, yfac = 5, -23 qs = Country.objects.translate(xfac, yfac, model_att='translated') for c in qs: @@ -520,6 +524,7 @@ class GeoModelTest(unittest.TestCase): def test23_numgeom(self): "Testing the `num_geom` GeoQuerySet method." + if DISABLE: return # Both 'countries' only have two geometries. for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom) for c in City.objects.filter(point__isnull=False).num_geom(): @@ -530,6 +535,7 @@ class GeoModelTest(unittest.TestCase): def test24_numpoints(self): "Testing the `num_points` GeoQuerySet method." + if DISABLE: return for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points) if postgis: # Oracle cannot count vertices in Point geometries. @@ -538,6 +544,7 @@ class GeoModelTest(unittest.TestCase): @no_oracle def test25_geoset(self): "Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods." + if DISABLE: return geom = Point(5, 23) for c in Country.objects.all().intersection(geom).difference(geom).sym_difference(geom).union(geom): self.assertEqual(c.mpoly.difference(geom), c.difference) diff --git a/django/contrib/sitemaps/management/commands/ping_google.py b/django/contrib/sitemaps/management/commands/ping_google.py new file mode 100644 index 0000000000..afff04b39c --- /dev/null +++ b/django/contrib/sitemaps/management/commands/ping_google.py @@ -0,0 +1,14 @@ +from django.core.management.base import BaseCommand +from django.contrib.sitemaps import ping_google + + +class Command(BaseCommand): + help = "Ping google with an updated sitemap, pass optional url of sitemap" + + def execute(self, *args, **options): + if len(args) == 1: + sitemap_url = args[0] + else: + sitemap_url = None + ping_google(sitemap_url=sitemap_url) + diff --git a/django/core/files/__init__.py b/django/core/files/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/core/files/__init__.py diff --git a/django/core/files/locks.py b/django/core/files/locks.py new file mode 100644 index 0000000000..212b51a73d --- /dev/null +++ b/django/core/files/locks.py @@ -0,0 +1,66 @@ +""" +Portable file locking utilities. + +Based partially on example by Jonathan Feignberg <jdf@pobox.com> in the Python +Cookbook, licensed under the Python Software License. + + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65203 + +Example Usage:: + + >>> from django.core.files import locks + >>> f = open('./file', 'wb') + >>> locks.lock(f, locks.LOCK_EX) + >>> f.write('Django') + >>> f.close() +""" + +__all__ = ('LOCK_EX','LOCK_SH','LOCK_NB','lock','unlock') + +system_type = None + +try: + import win32con + import win32file + import pywintypes + LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK + LOCK_SH = 0 + LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY + __overlapped = pywintypes.OVERLAPPED() + system_type = 'nt' +except (ImportError, AttributeError): + pass + +try: + import fcntl + LOCK_EX = fcntl.LOCK_EX + LOCK_SH = fcntl.LOCK_SH + LOCK_NB = fcntl.LOCK_NB + system_type = 'posix' +except (ImportError, AttributeError): + pass + +if system_type == 'nt': + def lock(file, flags): + hfile = win32file._get_osfhandle(file.fileno()) + win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped) + + def unlock(file): + hfile = win32file._get_osfhandle(file.fileno()) + win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped) +elif system_type == 'posix': + def lock(file, flags): + fcntl.flock(file.fileno(), flags) + + def unlock(file): + fcntl.flock(file.fileno(), fcntl.LOCK_UN) +else: + # File locking is not supported. + LOCK_EX = LOCK_SH = LOCK_NB = None + + # Dummy functions that don't do anything. + def lock(file, flags): + pass + + def unlock(file): + pass diff --git a/django/core/files/move.py b/django/core/files/move.py new file mode 100644 index 0000000000..66873d450c --- /dev/null +++ b/django/core/files/move.py @@ -0,0 +1,59 @@ +""" +Move a file in the safest way possible:: + + >>> from django.core.files.move import file_move_save + >>> file_move_save("/tmp/old_file", "/tmp/new_file") +""" + +import os +from django.core.files import locks + +__all__ = ['file_move_safe'] + +try: + import shutil + file_move = shutil.move +except ImportError: + file_move = os.rename + +def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_overwrite=False): + """ + Moves a file from one location to another in the safest way possible. + + First, try using ``shutils.move``, which is OS-dependent but doesn't break + if moving across filesystems. Then, try ``os.rename``, which will break + across filesystems. Finally, streams manually from one file to another in + pure Python. + + If the destination file exists and ``allow_overwrite`` is ``False``, this + function will throw an ``IOError``. + """ + + # There's no reason to move if we don't have to. + if old_file_name == new_file_name: + return + + if not allow_overwrite and os.path.exists(new_file_name): + raise IOError("Cannot overwrite existing file '%s'." % new_file_name) + + try: + file_move(old_file_name, new_file_name) + return + except OSError: + # This will happen with os.rename if moving to another filesystem + pass + + # If the built-in didn't work, do it the hard way. + new_file = open(new_file_name, 'wb') + locks.lock(new_file, locks.LOCK_EX) + old_file = open(old_file_name, 'rb') + current_chunk = None + + while current_chunk != '': + current_chunk = old_file.read(chunk_size) + new_file.write(current_chunk) + + new_file.close() + old_file.close() + + os.remove(old_file_name) diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py new file mode 100644 index 0000000000..637609d085 --- /dev/null +++ b/django/core/files/uploadedfile.py @@ -0,0 +1,190 @@ +""" +Classes representing uploaded files. +""" + +import os +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +__all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile') + +class UploadedFile(object): + """ + A abstract uploaded file (``TemporaryUploadedFile`` and + ``InMemoryUploadedFile`` are the built-in concrete subclasses). + + An ``UploadedFile`` object behaves somewhat like a file object and + represents some file data that the user submitted with a form. + """ + DEFAULT_CHUNK_SIZE = 64 * 2**10 + + def __init__(self, file_name=None, content_type=None, file_size=None, charset=None): + self.file_name = file_name + self.file_size = file_size + self.content_type = content_type + self.charset = charset + + def __repr__(self): + return "<%s: %s (%s)>" % (self.__class__.__name__, self.file_name, self.content_type) + + def _set_file_name(self, name): + # Sanitize the file name so that it can't be dangerous. + if name is not None: + # Just use the basename of the file -- anything else is dangerous. + name = os.path.basename(name) + + # File names longer than 255 characters can cause problems on older OSes. + if len(name) > 255: + name, ext = os.path.splitext(name) + name = name[:255 - len(ext)] + ext + + self._file_name = name + + def _get_file_name(self): + return self._file_name + + file_name = property(_get_file_name, _set_file_name) + + def chunk(self, chunk_size=None): + """ + Read the file and yield chucks of ``chunk_size`` bytes (defaults to + ``UploadedFile.DEFAULT_CHUNK_SIZE``). + """ + if not chunk_size: + chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE + + if hasattr(self, 'seek'): + self.seek(0) + # Assume the pointer is at zero... + counter = self.file_size + + while counter > 0: + yield self.read(chunk_size) + counter -= chunk_size + + def multiple_chunks(self, chunk_size=None): + """ + Returns ``True`` if you can expect multiple chunks. + + NB: If a particular file representation is in memory, subclasses should + always return ``False`` -- there's no good reason to read from memory in + chunks. + """ + if not chunk_size: + chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE + return self.file_size < chunk_size + + # Abstract methods; subclasses *must* default read() and probably should + # define open/close. + def read(self, num_bytes=None): + raise NotImplementedError() + + def open(self): + pass + + def close(self): + pass + + # Backwards-compatible support for uploaded-files-as-dictionaries. + def __getitem__(self, key): + import warnings + warnings.warn( + message = "The dictionary access of uploaded file objects is deprecated. Use the new object interface instead.", + category = DeprecationWarning, + stacklevel = 2 + ) + backwards_translate = { + 'filename': 'file_name', + 'content-type': 'content_type', + } + + if key == 'content': + return self.read() + elif key == 'filename': + return self.file_name + elif key == 'content-type': + return self.content_type + else: + return getattr(self, key) + +class TemporaryUploadedFile(UploadedFile): + """ + A file uploaded to a temporary location (i.e. stream-to-disk). + """ + + def __init__(self, file, file_name, content_type, file_size, charset): + super(TemporaryUploadedFile, self).__init__(file_name, content_type, file_size, charset) + self.file = file + self.path = file.name + self.file.seek(0) + + def temporary_file_path(self): + """ + Returns the full path of this file. + """ + return self.path + + def read(self, *args, **kwargs): + return self.file.read(*args, **kwargs) + + def open(self): + self.seek(0) + + def seek(self, *args, **kwargs): + self.file.seek(*args, **kwargs) + +class InMemoryUploadedFile(UploadedFile): + """ + A file uploaded into memory (i.e. stream-to-memory). + """ + def __init__(self, file, field_name, file_name, content_type, file_size, charset): + super(InMemoryUploadedFile, self).__init__(file_name, content_type, file_size, charset) + self.file = file + self.field_name = field_name + self.file.seek(0) + + def seek(self, *args, **kwargs): + self.file.seek(*args, **kwargs) + + def open(self): + self.seek(0) + + def read(self, *args, **kwargs): + return self.file.read(*args, **kwargs) + + def chunk(self, chunk_size=None): + self.file.seek(0) + yield self.read() + + def multiple_chunks(self, chunk_size=None): + # Since it's in memory, we'll never have multiple chunks. + return False + +class SimpleUploadedFile(InMemoryUploadedFile): + """ + A simple representation of a file, which just has content, size, and a name. + """ + def __init__(self, name, content, content_type='text/plain'): + self.file = StringIO(content or '') + self.file_name = name + self.field_name = None + self.file_size = len(content or '') + self.content_type = content_type + self.charset = None + self.file.seek(0) + + def from_dict(cls, file_dict): + """ + Creates a SimpleUploadedFile object from + a dictionary object with the following keys: + - filename + - content-type + - content + """ + return cls(file_dict['filename'], + file_dict['content'], + file_dict.get('content-type', 'text/plain')) + + from_dict = classmethod(from_dict) diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py new file mode 100644 index 0000000000..ab587769f7 --- /dev/null +++ b/django/core/files/uploadhandler.py @@ -0,0 +1,245 @@ +""" +Base file upload handler classes, and the built-in concrete subclasses +""" +import os +import tempfile +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.files.uploadedfile import TemporaryUploadedFile, InMemoryUploadedFile + +__all__ = ['UploadFileException','StopUpload', 'SkipFile', 'FileUploadHandler', + 'TemporaryFileUploadHandler', 'MemoryFileUploadHandler', + 'load_handler'] + +class UploadFileException(Exception): + """ + Any error having to do with uploading files. + """ + pass + +class StopUpload(UploadFileException): + """ + This exception is raised when an upload must abort. + """ + def __init__(self, connection_reset=False): + """ + If ``connection_reset`` is ``True``, Django knows will halt the upload + without consuming the rest of the upload. This will cause the browser to + show a "connection reset" error. + """ + self.connection_reset = connection_reset + + def __unicode__(self): + if self.connection_reset: + return u'StopUpload: Halt current upload.' + else: + return u'StopUpload: Consume request data, then halt.' + +class SkipFile(UploadFileException): + """ + This exception is raised by an upload handler that wants to skip a given file. + """ + pass + +class StopFutureHandlers(UploadFileException): + """ + Upload handers that have handled a file and do not want future handlers to + run should raise this exception instead of returning None. + """ + pass + +class FileUploadHandler(object): + """ + Base class for streaming upload handlers. + """ + chunk_size = 64 * 2 ** 10 #: The default chunk size is 64 KB. + + def __init__(self, request=None): + self.file_name = None + self.content_type = None + self.content_length = None + self.charset = None + self.request = request + + def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None): + """ + Handle the raw input from the client. + + Parameters: + + :input_data: + An object that supports reading via .read(). + :META: + ``request.META``. + :content_length: + The (integer) value of the Content-Length header from the + client. + :boundary: The boundary from the Content-Type header. Be sure to + prepend two '--'. + """ + pass + + def new_file(self, field_name, file_name, content_type, content_length, charset=None): + """ + Signal that a new file has been started. + + Warning: As with any data from the client, you should not trust + content_length (and sometimes won't even get it). + """ + self.field_name = field_name + self.file_name = file_name + self.content_type = content_type + self.content_length = content_length + self.charset = charset + + def receive_data_chunk(self, raw_data, start): + """ + Receive data from the streamed upload parser. ``start`` is the position + in the file of the chunk. + """ + raise NotImplementedError() + + def file_complete(self, file_size): + """ + Signal that a file has completed. File size corresponds to the actual + size accumulated by all the chunks. + + Subclasses must should return a valid ``UploadedFile`` object. + """ + raise NotImplementedError() + + def upload_complete(self): + """ + Signal that the upload is complete. Subclasses should perform cleanup + that is necessary for this handler. + """ + pass + +class TemporaryFileUploadHandler(FileUploadHandler): + """ + Upload handler that streams data into a temporary file. + """ + def __init__(self, *args, **kwargs): + super(TemporaryFileUploadHandler, self).__init__(*args, **kwargs) + + def new_file(self, file_name, *args, **kwargs): + """ + Create the file object to append to as data is coming in. + """ + super(TemporaryFileUploadHandler, self).new_file(file_name, *args, **kwargs) + self.file = TemporaryFile(settings.FILE_UPLOAD_TEMP_DIR) + self.write = self.file.write + + def receive_data_chunk(self, raw_data, start): + self.write(raw_data) + + def file_complete(self, file_size): + self.file.seek(0) + return TemporaryUploadedFile( + file = self.file, + file_name = self.file_name, + content_type = self.content_type, + file_size = file_size, + charset = self.charset + ) + +class MemoryFileUploadHandler(FileUploadHandler): + """ + File upload handler to stream uploads into memory (used for small files). + """ + + def handle_raw_input(self, input_data, META, content_length, boundary, encoding=None): + """ + Use the content_length to signal whether or not this handler should be in use. + """ + # Check the content-length header to see if we should + # If the the post is too large, we cannot use the Memory handler. + if content_length > settings.FILE_UPLOAD_MAX_MEMORY_SIZE: + self.activated = False + else: + self.activated = True + + def new_file(self, *args, **kwargs): + super(MemoryFileUploadHandler, self).new_file(*args, **kwargs) + if self.activated: + self.file = StringIO() + raise StopFutureHandlers() + + def receive_data_chunk(self, raw_data, start): + """ + Add the data to the StringIO file. + """ + if self.activated: + self.file.write(raw_data) + else: + return raw_data + + def file_complete(self, file_size): + """ + Return a file object if we're activated. + """ + if not self.activated: + return + + return InMemoryUploadedFile( + file = self.file, + field_name = self.field_name, + file_name = self.file_name, + content_type = self.content_type, + file_size = file_size, + charset = self.charset + ) + +class TemporaryFile(object): + """ + A temporary file that tries to delete itself when garbage collected. + """ + def __init__(self, dir): + if not dir: + dir = tempfile.gettempdir() + try: + (fd, name) = tempfile.mkstemp(suffix='.upload', dir=dir) + self.file = os.fdopen(fd, 'w+b') + except (OSError, IOError): + raise OSError("Could not create temporary file for uploading, have you set settings.FILE_UPLOAD_TEMP_DIR correctly?") + self.name = name + + def __getattr__(self, name): + a = getattr(self.__dict__['file'], name) + if type(a) != type(0): + setattr(self, name, a) + return a + + def __del__(self): + try: + os.unlink(self.name) + except OSError: + pass + +def load_handler(path, *args, **kwargs): + """ + Given a path to a handler, return an instance of that handler. + + E.g.:: + >>> load_handler('django.core.files.uploadhandler.TemporaryFileUploadHandler', request) + <TemporaryFileUploadHandler object at 0x...> + + """ + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = __import__(module, {}, {}, [attr]) + except ImportError, e: + raise ImproperlyConfigured('Error importing upload handler module %s: "%s"' % (module, e)) + except ValueError, e: + raise ImproperlyConfigured('Error importing upload handler module. Is FILE_UPLOAD_HANDLERS a correctly defined list or tuple?') + try: + cls = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured('Module "%s" does not define a "%s" upload handler backend' % (module, attr)) + return cls(*args, **kwargs) diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index abab399009..332df6f54c 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -53,7 +53,8 @@ class ModPythonRequest(http.HttpRequest): def _load_post_and_files(self): "Populates self._post and self._files" if 'content-type' in self._req.headers_in and self._req.headers_in['content-type'].startswith('multipart'): - self._post, self._files = http.parse_file_upload(self._req.headers_in, self.raw_post_data) + self._raw_post_data = '' + self._post, self._files = self.parse_file_upload(self.META, self._req) else: self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict() diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index df2ba19b65..795f139042 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -112,9 +112,8 @@ class WSGIRequest(http.HttpRequest): # Populates self._post and self._files if self.method == 'POST': if self.environ.get('CONTENT_TYPE', '').startswith('multipart'): - header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')]) - header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '') - self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data) + self._raw_post_data = '' + self._post, self._files = self.parse_file_upload(self.META, self.environ['wsgi.input']) else: self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict() else: diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index c8df8d04f7..098bca793f 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -21,10 +21,10 @@ class Command(LabelCommand): for f in fields: field_output = [qn(f.name), f.db_type()] field_output.append("%sNULL" % (not f.null and "NOT " or "")) - if f.unique: - field_output.append("UNIQUE") if f.primary_key: field_output.append("PRIMARY KEY") + elif f.unique: + field_output.append("UNIQUE") if f.db_index: unique = f.unique and "UNIQUE " or "" index_output.append("CREATE %sINDEX %s_%s ON %s (%s);" % \ diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 688cd58e2c..0ccc38ba8d 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -162,3 +162,9 @@ class Command(BaseCommand): else: if verbosity > 0: print "Installed %d object(s) from %d fixture(s)" % (object_count, fixture_count) + + # Close the DB connection. This is required as a workaround for an + # edge case in MySQL: if the same connection is used to + # create tables, load data, and query, the query can return + # incorrect results. See Django #7572, MySQL #37735. + connection.close() diff --git a/django/core/management/sql.py b/django/core/management/sql.py index dfe97cdd0c..0b01edcfc9 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -268,11 +268,11 @@ def sql_model_create(model, style, known_models=set()): field_output = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)] field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or ''))) - if f.unique and (not f.primary_key or connection.features.allows_unique_and_pk): - field_output.append(style.SQL_KEYWORD('UNIQUE')) if f.primary_key: field_output.append(style.SQL_KEYWORD('PRIMARY KEY')) - if tablespace and connection.features.supports_tablespaces and (f.unique or f.primary_key) and connection.features.autoindexes_primary_keys: + elif f.unique: + field_output.append(style.SQL_KEYWORD('UNIQUE')) + if tablespace and connection.features.supports_tablespaces and f.unique: # We must specify the index tablespace inline, because we # won't be generating a CREATE INDEX statement for this field. field_output.append(connection.ops.tablespace_sql(tablespace, inline=True)) @@ -355,7 +355,7 @@ def many_to_many_sql_for_model(model, style): for f in opts.local_many_to_many: if not isinstance(f.rel, generic.GenericRel): tablespace = f.db_tablespace or opts.db_tablespace - if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys: + if tablespace and connection.features.supports_tablespaces: tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True) else: tablespace_sql = '' @@ -468,15 +468,14 @@ def sql_indexes_for_model(model, style): qn = connection.ops.quote_name for f in model._meta.local_fields: - if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys): - unique = f.unique and 'UNIQUE ' or '' + if f.db_index and not f.unique: tablespace = f.db_tablespace or model._meta.db_tablespace if tablespace and connection.features.supports_tablespaces: tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace) else: tablespace_sql = '' output.append( - style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \ + style.SQL_KEYWORD('CREATE INDEX') + ' ' + \ style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \ style.SQL_KEYWORD('ON') + ' ' + \ style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \ diff --git a/django/core/paginator.py b/django/core/paginator.py index 04cc4bf481..5fc6c80812 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -173,7 +173,10 @@ class ObjectPaginator(Paginator): if self._count is None: try: self._count = self.object_list.count() - except TypeError: + except (AttributeError, TypeError): + # AttributeError if object_list has no count() method. + # TypeError if object_list.count() requires arguments + # (i.e. is of type list). self._count = len(self.object_list) return self._count count = property(_get_count) diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 5bdf08b8ca..f88f8c70bb 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -551,6 +551,9 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): def __init__(self, *args, **kwargs): from django.conf import settings self.admin_media_prefix = settings.ADMIN_MEDIA_PREFIX + # We set self.path to avoid crashes in log_message() on unsupported + # requests (like "OPTIONS"). + self.path = '' BaseHTTPRequestHandler.__init__(self, *args, **kwargs) def get_environ(self): diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py index d7145e15ec..dc4c35b08d 100644 --- a/django/core/servers/fastcgi.py +++ b/django/core/servers/fastcgi.py @@ -40,6 +40,7 @@ Optional Fcgi settings: (setting=value) workdir=DIRECTORY change to this directory when daemonizing. outlog=FILE write stdout to this file. errlog=FILE write stderr to this file. + umask=UMASK umask to use when daemonizing (default 022). Examples: Run a "standard" fastcgi process on a file-descriptor @@ -73,6 +74,7 @@ FASTCGI_OPTIONS = { 'maxrequests': 0, 'outlog': None, 'errlog': None, + 'umask': None, } def fastcgi_help(message=None): @@ -159,6 +161,8 @@ def runfastcgi(argset=[], **kwargs): daemon_kwargs['out_log'] = options['outlog'] if options['errlog']: daemon_kwargs['err_log'] = options['errlog'] + if options['umask']: + daemon_kwargs['umask'] = int(options['umask']) if daemonize: from django.utils.daemonize import become_daemon diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 2ad63bfc7d..ff0bcbcfea 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -296,3 +296,8 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None): kwargs = kwargs or {} return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs)) +def clear_url_caches(): + global _resolver_cache + global _callable_cache + _resolver_cache.clear() + _callable_cache.clear() diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 7a4e46a7d7..36a2339638 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -41,8 +41,6 @@ class BaseDatabaseWrapper(local): class BaseDatabaseFeatures(object): allows_group_by_ordinal = True - allows_unique_and_pk = True - autoindexes_primary_keys = True inline_fk_references = True needs_datetime_string_cast = True supports_constraints = True diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 23735a023b..336ad89504 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -60,7 +60,6 @@ server_version_re = re.compile(r'(\d{1,2})\.(\d{1,2})\.(\d{1,2})') # TRADITIONAL will automatically cause most warnings to be treated as errors. class DatabaseFeatures(BaseDatabaseFeatures): - autoindexes_primary_keys = False inline_fk_references = False empty_fetchmany_value = () update_can_self_select = False @@ -136,7 +135,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): features = DatabaseFeatures() ops = DatabaseOperations() operators = { - 'exact': '= %s', + 'exact': '= BINARY %s', 'iexact': 'LIKE %s', 'contains': 'LIKE BINARY %s', 'icontains': 'LIKE %s', diff --git a/django/db/backends/mysql_old/base.py b/django/db/backends/mysql_old/base.py index e57aee6a04..48b6b6958a 100644 --- a/django/db/backends/mysql_old/base.py +++ b/django/db/backends/mysql_old/base.py @@ -64,7 +64,6 @@ class MysqlDebugWrapper: return getattr(self.cursor, attr) class DatabaseFeatures(BaseDatabaseFeatures): - autoindexes_primary_keys = False inline_fk_references = False empty_fetchmany_value = () update_can_self_select = False @@ -140,7 +139,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): features = DatabaseFeatures() ops = DatabaseOperations() operators = { - 'exact': '= %s', + 'exact': '= BINARY %s', 'iexact': 'LIKE %s', 'contains': 'LIKE BINARY %s', 'icontains': 'LIKE %s', diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 56ae051de8..0b408990fb 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -24,7 +24,6 @@ IntegrityError = Database.IntegrityError class DatabaseFeatures(BaseDatabaseFeatures): allows_group_by_ordinal = False - allows_unique_and_pk = False # Suppress UNIQUE/PK for Oracle (ORA-02259) empty_fetchmany_value = () needs_datetime_string_cast = False supports_tablespaces = True @@ -147,11 +146,11 @@ class DatabaseOperations(BaseDatabaseOperations): # Since we've just deleted all the rows, running our sequence # ALTER code will reset the sequence to 0. for sequence_info in sequences: - table_name = sequence_info['table'] - seq_name = get_sequence_name(table_name) + sequence_name = get_sequence_name(sequence_info['table']) + table_name = self.quote_name(sequence_info['table']) column_name = self.quote_name(sequence_info['column'] or 'id') - query = _get_sequence_reset_sql() % {'sequence': seq_name, - 'table': self.quote_name(table_name), + query = _get_sequence_reset_sql() % {'sequence': sequence_name, + 'table': table_name, 'column': column_name} sql.append(query) return sql @@ -163,19 +162,22 @@ class DatabaseOperations(BaseDatabaseOperations): output = [] query = _get_sequence_reset_sql() for model in model_list: - for f in model._meta.fields: + for f in model._meta.local_fields: if isinstance(f, models.AutoField): + table_name = self.quote_name(model._meta.db_table) sequence_name = get_sequence_name(model._meta.db_table) - column_name = self.quote_name(f.db_column or f.name) + column_name = self.quote_name(f.column) output.append(query % {'sequence': sequence_name, - 'table': model._meta.db_table, + 'table': table_name, 'column': column_name}) break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.many_to_many: + table_name = self.quote_name(f.m2m_db_table()) sequence_name = get_sequence_name(f.m2m_db_table()) + column_name = self.quote_name('id') output.append(query % {'sequence': sequence_name, - 'table': f.m2m_db_table(), - 'column': self.quote_name('id')}) + 'table': table_name, + 'column': column_name}) return output def start_transaction_sql(self): diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 14bc8faf86..a106df47a8 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -23,7 +23,7 @@ DATA_TYPES = { 'ImageField': 'NVARCHAR2(%(max_length)s)', 'IntegerField': 'NUMBER(11)', 'IPAddressField': 'VARCHAR2(15)', - 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(column)s IS NULL))', + 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))', 'OneToOneField': 'NUMBER(11)', 'PhoneNumberField': 'VARCHAR2(20)', 'PositiveIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)', diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index 7e27b23f72..ba6e3235c2 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -97,7 +97,7 @@ class DatabaseOperations(BaseDatabaseOperations): # Use `coalesce` to set the sequence for each model to the max pk value if there are records, # or 1 if there are none. Set the `is_called` property (the third argument to `setval`) to true # if there are records (as the max pk value is already in use), otherwise set it to false. - for f in model._meta.fields: + for f in model._meta.local_fields: if isinstance(f, models.AutoField): output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \ (style.SQL_KEYWORD('SELECT'), diff --git a/django/db/models/base.py b/django/db/models/base.py index 14a48044df..5669694a1b 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -19,6 +19,8 @@ from django.dispatch import dispatcher from django.utils.datastructures import SortedDict from django.utils.functional import curry from django.utils.encoding import smart_str, force_unicode, smart_unicode +from django.core.files.move import file_move_safe +from django.core.files import locks from django.conf import settings try: @@ -50,7 +52,15 @@ class ModelBase(type): meta = attr_meta base_meta = getattr(new_class, '_meta', None) - new_class.add_to_class('_meta', Options(meta)) + if getattr(meta, 'app_label', None) is None: + # Figure out the app_label by looking one level up. + # For 'django.contrib.sites.models', this would be 'sites'. + model_module = sys.modules[new_class.__module__] + kwargs = {"app_label": model_module.__name__.split('.')[-2]} + else: + kwargs = {} + + new_class.add_to_class('_meta', Options(meta, **kwargs)) if not abstract: new_class.add_to_class('DoesNotExist', subclass_exception('DoesNotExist', ObjectDoesNotExist, module)) @@ -71,11 +81,6 @@ class ModelBase(type): if new_class._default_manager.model._meta.abstract: old_default_mgr = new_class._default_manager new_class._default_manager = None - if getattr(new_class._meta, 'app_label', None) is None: - # Figure out the app_label by looking one level up. - # For 'django.contrib.sites.models', this would be 'sites'. - model_module = sys.modules[new_class.__module__] - new_class._meta.app_label = model_module.__name__.split('.')[-2] # Bail out early if we have already created this class. m = get_model(new_class._meta.app_label, name, False) @@ -392,6 +397,21 @@ class Model(object): for sub_obj in getattr(self, rel_opts_name).all(): sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) + # Handle any ancestors (for the model-inheritance case). We do this by + # traversing to the most remote parent classes -- those with no parents + # themselves -- and then adding those instances to the collection. That + # will include all the child instances down to "self". + parent_stack = self._meta.parents.values() + while parent_stack: + link = parent_stack.pop() + parent_obj = getattr(self, link.name) + if parent_obj._meta.parents: + parent_stack.extend(parent_obj._meta.parents.values()) + continue + # At this point, parent_obj is base class (no ancestor models). So + # delete it and all its descendents. + parent_obj._collect_sub_objects(seen_objs) + def delete(self): assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname) @@ -439,7 +459,7 @@ class Model(object): def _get_FIELD_filename(self, field): if getattr(self, field.attname): # value is not blank - return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)) + return os.path.normpath(os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname))) return '' def _get_FIELD_url(self, field): @@ -451,16 +471,51 @@ class Model(object): def _get_FIELD_size(self, field): return os.path.getsize(self._get_FIELD_filename(field)) - def _save_FIELD_file(self, field, filename, raw_contents, save=True): + def _save_FIELD_file(self, field, filename, raw_field, save=True): directory = field.get_directory_name() try: # Create the date-based directory if it doesn't exist. os.makedirs(os.path.join(settings.MEDIA_ROOT, directory)) except OSError: # Directory probably already exists. pass + + # + # Check for old-style usage (files-as-dictionaries). Warn here first + # since there are multiple locations where we need to support both new + # and old usage. + # + if isinstance(raw_field, dict): + import warnings + warnings.warn( + message = "Representing uploaded files as dictionaries is"\ + " deprected. Use django.core.files.SimpleUploadedFile"\ + " instead.", + category = DeprecationWarning, + stacklevel = 2 + ) + from django.core.files.uploadedfile import SimpleUploadedFile + raw_field = SimpleUploadedFile.from_dict(raw_field) + + elif isinstance(raw_field, basestring): + import warnings + warnings.warn( + message = "Representing uploaded files as strings is "\ + " deprecated. Use django.core.files.SimpleUploadedFile "\ + " instead.", + category = DeprecationWarning, + stacklevel = 2 + ) + from django.core.files.uploadedfile import SimpleUploadedFile + raw_field = SimpleUploadedFile(filename, raw_field) + + if filename is None: + filename = raw_field.file_name + filename = field.get_filename(filename) + # # If the filename already exists, keep adding an underscore to the name of # the file until the filename doesn't exist. + # while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)): try: dot_index = filename.rindex('.') @@ -468,14 +523,27 @@ class Model(object): filename += '_' else: filename = filename[:dot_index] + '_' + filename[dot_index:] + # + # Save the file name on the object and write the file to disk + # - # Write the file to disk. setattr(self, field.attname, filename) full_filename = self._get_FIELD_filename(field) - fp = open(full_filename, 'wb') - fp.write(raw_contents) - fp.close() + + if hasattr(raw_field, 'temporary_file_path'): + # This file has a file path that we can move. + raw_field.close() + file_move_safe(raw_field.temporary_file_path(), full_filename) + + else: + # This is a normal uploadedfile that we can stream. + fp = open(full_filename, 'wb') + locks.lock(fp, locks.LOCK_EX) + for chunk in raw_field.chunk(): + fp.write(chunk) + locks.unlock(fp) + fp.close() # Save the width and/or height, if applicable. if isinstance(field, ImageField) and (field.width_field or field.height_field): diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 33a941ff01..0a2549437b 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -91,7 +91,7 @@ class Field(object): self.name = name self.verbose_name = verbose_name self.primary_key = primary_key - self.max_length, self.unique = max_length, unique + self.max_length, self._unique = max_length, unique self.blank, self.null = blank, null # Oracle treats the empty string ('') as null, so coerce the null # option whenever '' is a possible value. @@ -168,6 +168,10 @@ class Field(object): except KeyError: return None + def unique(self): + return self._unique or self.primary_key + unique = property(unique) + def validate_full(self, field_data, all_data): """ Returns a list of errors for this field. This is the main interface, @@ -695,7 +699,7 @@ class DecimalField(Field): _("This value must be a decimal number.")) def _format(self, value): - if isinstance(value, basestring): + if isinstance(value, basestring) or value is None: return value else: return self.format_number(value) @@ -716,8 +720,7 @@ class DecimalField(Field): return u"%.*f" % (self.decimal_places, value) def get_db_prep_save(self, value): - if value is not None: - value = self._format(value) + value = self._format(value) return super(DecimalField, self).get_db_prep_save(value) def get_db_prep_lookup(self, lookup_type, value): @@ -812,7 +815,7 @@ class FileField(Field): setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self)) setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self)) setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self)) - setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents, save=True: instance._save_FIELD_file(self, filename, raw_contents, save)) + setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save)) dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls) def delete_file(self, instance): @@ -835,9 +838,19 @@ class FileField(Field): if new_data.get(upload_field_name, False): func = getattr(new_object, 'save_%s_file' % self.name) if rel: - func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"], save) + file = new_data[upload_field_name][0] else: - func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"], save) + file = new_data[upload_field_name] + + # Backwards-compatible support for files-as-dictionaries. + # We don't need to raise a warning because Model._save_FIELD_file will + # do so for us. + try: + file_name = file.file_name + except AttributeError: + file_name = file['filename'] + + func(file_name, file, save) def get_directory_name(self): return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to)))) @@ -850,7 +863,7 @@ class FileField(Field): def save_form_data(self, instance, data): from django.newforms.fields import UploadedFile if data and isinstance(data, UploadedFile): - getattr(instance, "save_%s_file" % self.name)(data.filename, data.content, save=False) + getattr(instance, "save_%s_file" % self.name)(data.filename, data.data, save=False) def formfield(self, **kwargs): defaults = {'form_class': forms.FileField} @@ -1166,12 +1179,3 @@ class XMLField(TextField): def get_manipulator_field_objs(self): return [curry(oldforms.XMLLargeTextField, schema_path=self.schema_path)] -class OrderingField(IntegerField): - empty_strings_allowed=False - def __init__(self, with_respect_to, **kwargs): - self.wrt = with_respect_to - kwargs['null'] = True - IntegerField.__init__(self, **kwargs ) - - def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True): - return [oldforms.HiddenField(name_prefix + self.name)] diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 9d5886d426..9ef46df575 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -185,11 +185,11 @@ class SingleRelatedObjectDescriptor(object): def __set__(self, instance, value): if instance is None: raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name - - # The similarity of the code below to the code in + + # The similarity of the code below to the code in # ReverseSingleRelatedObjectDescriptor is annoying, but there's a bunch # of small differences that would make a common base class convoluted. - + # If null=True, we can assign null here, but otherwise the value needs # to be an instance of the related class. if value is None and self.related.field.null == False: @@ -197,14 +197,14 @@ class SingleRelatedObjectDescriptor(object): (instance._meta.object_name, self.related.get_accessor_name())) elif value is not None and not isinstance(value, self.related.model): raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' % - (value, instance._meta.object_name, + (value, instance._meta.object_name, self.related.get_accessor_name(), self.related.opts.object_name)) - + # Set the value of the related field setattr(value, self.related.field.rel.get_related_field().attname, instance) # Since we already know what the related object is, seed the related - # object caches now, too. This avoids another db hit if you get the + # object caches now, too. This avoids another db hit if you get the # object you just set. setattr(instance, self.cache_name, value) setattr(value, self.related.field.get_cache_name(), instance) @@ -243,7 +243,7 @@ class ReverseSingleRelatedObjectDescriptor(object): def __set__(self, instance, value): if instance is None: raise AttributeError, "%s must be accessed via instance" % self._field.name - + # If null=True, we can assign null here, but otherwise the value needs # to be an instance of the related class. if value is None and self.field.null == False: @@ -251,9 +251,9 @@ class ReverseSingleRelatedObjectDescriptor(object): (instance._meta.object_name, self.field.name)) elif value is not None and not isinstance(value, self.field.rel.to): raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' % - (value, instance._meta.object_name, + (value, instance._meta.object_name, self.field.name, self.field.rel.to._meta.object_name)) - + # Set the value of the related field try: val = getattr(value, self.field.rel.get_related_field().attname) @@ -262,7 +262,7 @@ class ReverseSingleRelatedObjectDescriptor(object): setattr(instance, self.field.attname, val) # Since we already know what the related object is, seed the related - # object cache now, too. This avoids another db hit if you get the + # object cache now, too. This avoids another db hit if you get the # object you just set. setattr(instance, self.field.get_cache_name(), value) @@ -322,7 +322,9 @@ class ForeignRelatedObjectsDescriptor(object): clear.alters_data = True manager = RelatedManager() - manager.core_filters = {'%s__pk' % rel_field.name: getattr(instance, rel_field.rel.get_related_field().attname)} + attname = rel_field.rel.get_related_field().name + manager.core_filters = {'%s__%s' % (rel_field.name, attname): + getattr(instance, attname)} manager.model = self.related.model return manager @@ -692,6 +694,11 @@ class ForeignKey(RelatedField, Field): def contribute_to_class(self, cls, name): super(ForeignKey, self).contribute_to_class(cls, name) setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self)) + if isinstance(self.rel.to, basestring): + target = self.rel.to + else: + target = self.rel.to._meta.db_table + cls._meta.duplicate_targets[self.column] = (target, "o2m") def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) @@ -829,6 +836,12 @@ class ManyToManyField(RelatedField, Field): # Set up the accessor for the m2m table name for the relation self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta) + if isinstance(self.rel.to, basestring): + target = self.rel.to + else: + target = self.rel.to._meta.db_table + cls._meta.duplicate_targets[self.column] = (target, "m2m") + def contribute_to_related_class(self, cls, related): # m2m relations to self do not have a ManyRelatedObjectsDescriptor, # as it would be redundant - unless the field is non-symmetrical. diff --git a/django/db/models/options.py b/django/db/models/options.py index c2313f221f..a81a34d722 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -25,7 +25,7 @@ DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'abstract') class Options(object): - def __init__(self, meta): + def __init__(self, meta, app_label=None): self.local_fields, self.local_many_to_many = [], [] self.module_name, self.verbose_name = None, None self.verbose_name_plural = None @@ -33,7 +33,7 @@ class Options(object): self.ordering = [] self.unique_together = [] self.permissions = [] - self.object_name, self.app_label = None, None + self.object_name, self.app_label = None, app_label self.get_latest_by = None self.order_with_respect_to = None self.db_tablespace = settings.DEFAULT_TABLESPACE @@ -44,8 +44,12 @@ class Options(object): self.one_to_one_field = None self.abstract = False self.parents = SortedDict() + self.duplicate_targets = {} def contribute_to_class(self, cls, name): + from django.db import connection + from django.db.backends.util import truncate_name + cls._meta = self self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS # First, construct the default values for these options. @@ -87,9 +91,13 @@ class Options(object): self.verbose_name_plural = string_concat(self.verbose_name, 's') del self.meta + # If the db_table wasn't provided, use the app_label + module_name. + if not self.db_table: + self.db_table = "%s_%s" % (self.app_label, self.module_name) + self.db_table = truncate_name(self.db_table, connection.ops.max_name_length()) + + def _prepare(self, model): - from django.db import connection - from django.db.backends.util import truncate_name if self.order_with_respect_to: self.order_with_respect_to = self.get_field(self.order_with_respect_to) self.ordering = ('_order',) @@ -108,10 +116,23 @@ class Options(object): auto_created=True) model.add_to_class('id', auto) - # If the db_table wasn't provided, use the app_label + module_name. - if not self.db_table: - self.db_table = "%s_%s" % (self.app_label, self.module_name) - self.db_table = truncate_name(self.db_table, connection.ops.max_name_length()) + # Determine any sets of fields that are pointing to the same targets + # (e.g. two ForeignKeys to the same remote model). The query + # construction code needs to know this. At the end of this, + # self.duplicate_targets will map each duplicate field column to the + # columns it duplicates. + collections = {} + for column, target in self.duplicate_targets.iteritems(): + try: + collections[target].add(column) + except KeyError: + collections[target] = set([column]) + self.duplicate_targets = {} + for elt in collections.itervalues(): + if len(elt) == 1: + continue + for column in elt: + self.duplicate_targets[column] = elt.difference(set([column])) def add_field(self, field): # Insert the given field in the order in which it was created, using diff --git a/django/db/models/query.py b/django/db/models/query.py index 98caaf004c..e92f6c4275 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -3,7 +3,7 @@ import warnings from django.conf import settings from django.db import connection, transaction, IntegrityError from django.db.models.fields import DateField, FieldDoesNotExist -from django.db.models.query_utils import Q +from django.db.models.query_utils import Q, select_related_descend from django.db.models import signals, sql from django.dispatch import dispatcher from django.utils.datastructures import SortedDict @@ -761,8 +761,7 @@ def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0, index_end = index_start + len(klass._meta.fields) obj = klass(*row[index_start:index_end]) for f in klass._meta.fields: - if (not f.rel or (not restricted and f.null) or - (restricted and f.name not in requested) or f.rel.parent_link): + if not select_related_descend(f, restricted, requested): continue if restricted: next = requested[f.name] diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 0ce7900c74..8dbb1ec667 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -48,3 +48,20 @@ class Q(tree.Node): obj.negate() return obj +def select_related_descend(field, restricted, requested): + """ + Returns True if this field should be used to descend deeper for + select_related() purposes. Used by both the query construction code + (sql.query.fill_related_selections()) and the model instance creation code + (query.get_cached_row()). + """ + if not field.rel: + return False + if field.rel.parent_link: + return False + if restricted and field.name not in requested: + return False + if not restricted and field.null: + return False + return True + diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index e8d10bc55b..7944d0358f 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -7,6 +7,7 @@ databases). The abstraction barrier only works one way: this module has to know all about the internals of models in order to get the information it needs. """ +import datetime from copy import deepcopy from django.utils.tree import Node @@ -14,9 +15,10 @@ from django.utils.datastructures import SortedDict from django.dispatch import dispatcher from django.db import connection from django.db.models import signals +from django.db.models.fields import FieldDoesNotExist +from django.db.models.query_utils import select_related_descend from django.db.models.sql.where import WhereNode, EverythingNode, AND, OR from django.db.models.sql.datastructures import Count -from django.db.models.fields import FieldDoesNotExist from django.core.exceptions import FieldError from datastructures import EmptyResultSet, Empty, MultiJoin from constants import * @@ -56,6 +58,7 @@ class Query(object): self.start_meta = None self.select_fields = [] self.related_select_fields = [] + self.dupe_avoidance = {} # SQL-related attributes self.select = [] @@ -164,6 +167,7 @@ class Query(object): obj.start_meta = self.start_meta obj.select_fields = self.select_fields[:] obj.related_select_fields = self.related_select_fields[:] + obj.dupe_avoidance = self.dupe_avoidance.copy() obj.select = self.select[:] obj.tables = self.tables[:] obj.where = deepcopy(self.where) @@ -214,7 +218,7 @@ class Query(object): obj.select_related = False obj.related_select_cols = [] obj.related_select_fields = [] - if obj.distinct and len(obj.select) > 1: + if len(obj.select) > 1: obj = self.clone(CountQuery, _query=obj, where=self.where_class(), distinct=False) obj.select = [] @@ -362,10 +366,21 @@ class Query(object): item.relabel_aliases(change_map) self.select.append(item) self.select_fields = rhs.select_fields[:] - self.extra_select = rhs.extra_select.copy() - self.extra_tables = rhs.extra_tables - self.extra_where = rhs.extra_where - self.extra_params = rhs.extra_params + + if connector == OR: + # It would be nice to be able to handle this, but the queries don't + # really make sense (or return consistent value sets). Not worth + # the extra complexity when you can write a real query instead. + if self.extra_select and rhs.extra_select: + raise ValueError("When merging querysets using 'or', you " + "cannot have extra(select=...) on both sides.") + if self.extra_where and rhs.extra_where: + raise ValueError("When merging querysets using 'or', you " + "cannot have extra(where=...) on both sides.") + self.extra_select.update(rhs.extra_select) + self.extra_tables += rhs.extra_tables + self.extra_where += rhs.extra_where + self.extra_params += rhs.extra_params # Ordering uses the 'rhs' ordering, unless it has none, in which case # the current ordering is used. @@ -439,28 +454,39 @@ class Query(object): self._select_aliases = aliases return result - def get_default_columns(self, with_aliases=False, col_aliases=None): + def get_default_columns(self, with_aliases=False, col_aliases=None, + start_alias=None, opts=None, as_pairs=False): """ Computes the default columns for selecting every field in the base model. Returns a list of strings, quoted appropriately for use in SQL - directly, as well as a set of aliases used in the select statement. + directly, as well as a set of aliases used in the select statement (if + 'as_pairs' is True, returns a list of (alias, col_name) pairs instead + of strings as the first component and None as the second component). """ result = [] - table_alias = self.tables[0] - root_pk = self.model._meta.pk.column + if opts is None: + opts = self.model._meta + if start_alias: + table_alias = start_alias + else: + table_alias = self.tables[0] + root_pk = opts.pk.column seen = {None: table_alias} qn = self.quote_name_unless_alias qn2 = self.connection.ops.quote_name aliases = set() - for field, model in self.model._meta.get_fields_with_model(): + for field, model in opts.get_fields_with_model(): try: alias = seen[model] except KeyError: alias = self.join((table_alias, model._meta.db_table, root_pk, model._meta.pk.column)) seen[model] = alias + if as_pairs: + result.append((alias, field.column)) + continue if with_aliases and field.column in col_aliases: c_alias = 'Col%d' % len(col_aliases) result.append('%s.%s AS %s' % (qn(alias), @@ -473,6 +499,8 @@ class Query(object): aliases.add(r) if with_aliases: col_aliases.add(field.column) + if as_pairs: + return result, None return result, aliases def get_from_clause(self): @@ -609,6 +637,11 @@ class Query(object): alias, False) alias = joins[-1] col = target.column + if not field.rel: + # To avoid inadvertent trimming of a necessary alias, use the + # refcount to show that we are referencing a non-relation field on + # the model. + self.ref_alias(alias) # Must use left outer joins for nullable fields. for join in joins: @@ -829,8 +862,8 @@ class Query(object): if reuse and always_create and table in self.table_map: # Convert the 'reuse' to case to be "exclude everything but the - # reusable set for this table". - exclusions = set(self.table_map[table]).difference(reuse) + # reusable set, minus exclusions, for this table". + exclusions = set(self.table_map[table]).difference(reuse).union(set(exclusions)) always_create = False t_ident = (lhs_table, table, lhs_col, col) if not always_create: @@ -865,7 +898,8 @@ class Query(object): return alias def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1, - used=None, requested=None, restricted=None, nullable=None): + used=None, requested=None, restricted=None, nullable=None, + dupe_set=None): """ Fill in the information needed for a select_related query. The current depth is measured as the number of connections away from the root model @@ -875,6 +909,7 @@ class Query(object): if not restricted and self.max_depth and cur_depth > self.max_depth: # We've recursed far enough; bail out. return + if not opts: opts = self.get_meta() root_alias = self.get_initial_alias() @@ -882,6 +917,10 @@ class Query(object): self.related_select_fields = [] if not used: used = set() + if dupe_set is None: + dupe_set = set() + orig_dupe_set = dupe_set + orig_used = used # Setup for the case when only particular related fields should be # included in the related selection. @@ -893,9 +932,10 @@ class Query(object): restricted = False for f, model in opts.get_fields_with_model(): - if (not f.rel or (restricted and f.name not in requested) or - (not restricted and f.null) or f.rel.parent_link): + if not select_related_descend(f, restricted, requested): continue + dupe_set = orig_dupe_set.copy() + used = orig_used.copy() table = f.rel.to._meta.db_table if nullable or f.null: promote = True @@ -906,18 +946,32 @@ class Query(object): alias = root_alias for int_model in opts.get_base_chain(model): lhs_col = int_opts.parents[int_model].column + dedupe = lhs_col in opts.duplicate_targets + if dedupe: + used.update(self.dupe_avoidance.get(id(opts), lhs_col), + ()) + dupe_set.add((opts, lhs_col)) int_opts = int_model._meta alias = self.join((alias, int_opts.db_table, lhs_col, int_opts.pk.column), exclusions=used, promote=promote) + for (dupe_opts, dupe_col) in dupe_set: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) else: alias = root_alias + + dedupe = f.column in opts.duplicate_targets + if dupe_set or dedupe: + used.update(self.dupe_avoidance.get((id(opts), f.column), ())) + if dedupe: + dupe_set.add((opts, f.column)) + alias = self.join((alias, table, f.column, f.rel.get_related_field().column), exclusions=used, promote=promote) used.add(alias) - self.related_select_cols.extend([(alias, f2.column) - for f2 in f.rel.to._meta.fields]) + self.related_select_cols.extend(self.get_default_columns( + start_alias=alias, opts=f.rel.to._meta, as_pairs=True)[0]) self.related_select_fields.extend(f.rel.to._meta.fields) if restricted: next = requested.get(f.name, {}) @@ -927,8 +981,10 @@ class Query(object): new_nullable = f.null else: new_nullable = None + for dupe_opts, dupe_col in dupe_set: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1, - used, next, restricted, new_nullable) + used, next, restricted, new_nullable, dupe_set) def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, can_reuse=None): @@ -1058,15 +1114,17 @@ class Query(object): 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 = self.where_class() + entry.add((alias, j_col, None, 'isnull', True), AND) entry.negate() self.where.add(entry, AND) break - elif not (lookup_type == 'in' and not value): + elif not (lookup_type == 'in' and not value) and field.null: # 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 = self.where_class() + entry.add((alias, col, None, 'isnull', True), AND) entry.negate() self.where.add(entry, AND) @@ -1114,7 +1172,9 @@ class Query(object): (which gives the table we are joining to), 'alias' is the alias for the table we are joining to. If dupe_multis is True, any many-to-many or many-to-one joins will always create a new alias (necessary for - disjunctive filters). + disjunctive filters). If can_reuse is not None, it's a list of aliases + that can be reused in these joins (nothing else can be reused in this + case). Returns the final field involved in the join, the target database column (used for any 'where' constraint), the final 'opts' value and the @@ -1122,7 +1182,14 @@ class Query(object): """ joins = [alias] last = [0] + dupe_set = set() + exclusions = set() for pos, name in enumerate(names): + try: + exclusions.add(int_alias) + except NameError: + pass + exclusions.add(alias) last.append(len(joins)) if name == 'pk': name = opts.pk.name @@ -1141,6 +1208,7 @@ class Query(object): names = opts.get_all_field_names() raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (name, ", ".join(names))) + if not allow_many and (m2m or not direct): for alias in joins: self.unref_alias(alias) @@ -1150,12 +1218,27 @@ class Query(object): alias_list = [] for int_model in opts.get_base_chain(model): lhs_col = opts.parents[int_model].column + dedupe = lhs_col in opts.duplicate_targets + if dedupe: + exclusions.update(self.dupe_avoidance.get( + (id(opts), lhs_col), ())) + dupe_set.add((opts, lhs_col)) opts = int_model._meta alias = self.join((alias, opts.db_table, lhs_col, - opts.pk.column), exclusions=joins) + opts.pk.column), exclusions=exclusions) joins.append(alias) + exclusions.add(alias) + for (dupe_opts, dupe_col) in dupe_set: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) cached_data = opts._join_cache.get(name) orig_opts = opts + dupe_col = direct and field.column or field.field.column + dedupe = dupe_col in opts.duplicate_targets + if dupe_set or dedupe: + if dedupe: + dupe_set.add((opts, dupe_col)) + exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col), + ())) if direct: if m2m: @@ -1177,9 +1260,11 @@ class Query(object): target) int_alias = self.join((alias, table1, from_col1, to_col1), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) alias = self.join((int_alias, table2, from_col2, to_col2), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) joins.extend([int_alias, alias]) elif field.rel: # One-to-one or many-to-one field @@ -1195,7 +1280,7 @@ class Query(object): opts, target) alias = self.join((alias, table, from_col, to_col), - exclusions=joins, nullable=field.null) + exclusions=exclusions, nullable=field.null) joins.append(alias) else: # Non-relation fields. @@ -1223,9 +1308,11 @@ class Query(object): target) int_alias = self.join((alias, table1, from_col1, to_col1), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) alias = self.join((int_alias, table2, from_col2, to_col2), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) joins.extend([int_alias, alias]) else: # One-to-many field (ForeignKey defined on the target model) @@ -1243,14 +1330,34 @@ class Query(object): opts, target) alias = self.join((alias, table, from_col, to_col), - dupe_multis, joins, nullable=True, reuse=can_reuse) + dupe_multis, exclusions, nullable=True, + reuse=can_reuse) joins.append(alias) + for (dupe_opts, dupe_col) in dupe_set: + try: + self.update_dupe_avoidance(dupe_opts, dupe_col, int_alias) + except NameError: + self.update_dupe_avoidance(dupe_opts, dupe_col, alias) + if pos != len(names) - 1: raise FieldError("Join on field %r not permitted." % name) return field, target, opts, joins, last + def update_dupe_avoidance(self, opts, col, alias): + """ + For a column that is one of multiple pointing to the same table, update + the internal data structures to note that this alias shouldn't be used + for those other columns. + """ + ident = id(opts) + for name in opts.duplicate_targets[col]: + try: + self.dupe_avoidance[ident, name].add(alias) + except KeyError: + self.dupe_avoidance[ident, name] = set([alias]) + def split_exclude(self, filter_expr, prefix): """ When doing an exclude against any kind of N-to-many relation, we need diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 14e54487a3..18e4bf2f7e 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -21,12 +21,42 @@ class WhereNode(tree.Node): the correct SQL). The children in this tree are usually either Q-like objects or lists of - [table_alias, field_name, field_class, lookup_type, value]. However, a - child could also be any class with as_sql() and relabel_aliases() methods. + [table_alias, field_name, db_type, lookup_type, value_annotation, + params]. However, a child could also be any class with as_sql() and + relabel_aliases() methods. """ default = AND - def as_sql(self, node=None, qn=None): + def add(self, data, connector): + """ + Add a node to the where-tree. If the data is a list or tuple, it is + expected to be of the form (alias, col_name, field_obj, lookup_type, + value), which is then slightly munged before being stored (to avoid + storing any reference to field objects). Otherwise, the 'data' is + stored unchanged and can be anything with an 'as_sql()' method. + """ + if not isinstance(data, (list, tuple)): + super(WhereNode, self).add(data, connector) + return + + alias, col, field, lookup_type, value = data + if field: + params = field.get_db_prep_lookup(lookup_type, value) + db_type = field.db_type() + else: + # This is possible when we add a comparison to NULL sometimes (we + # don't really need to waste time looking up the associated field + # object). + params = Field().get_db_prep_lookup(lookup_type, value) + db_type = None + if isinstance(value, datetime.datetime): + annotation = datetime.datetime + else: + annotation = bool(value) + super(WhereNode, self).add((alias, col, db_type, lookup_type, + annotation, params), connector) + + def as_sql(self, qn=None): """ Returns the SQL version of the where clause and the value to be substituted in. Returns None, None if this node is empty. @@ -35,82 +65,73 @@ class WhereNode(tree.Node): (generally not needed except by the internal implementation for recursion). """ - if node is None: - node = self if not qn: qn = connection.ops.quote_name - if not node.children: + if not self.children: return None, [] result = [] result_params = [] empty = True - for child in node.children: + for child in self.children: try: if hasattr(child, 'as_sql'): sql, params = child.as_sql(qn=qn) - format = '(%s)' - elif isinstance(child, tree.Node): - sql, params = self.as_sql(child, qn) - if child.negated: - format = 'NOT (%s)' - elif len(child.children) == 1: - format = '%s' - else: - format = '(%s)' else: + # A leaf node in the tree. sql, params = self.make_atom(child, qn) - format = '%s' except EmptyResultSet: - if node.connector == AND and not node.negated: + if self.connector == AND and not self.negated: # We can bail out early in this particular case (only). raise - elif node.negated: + elif self.negated: empty = False continue except FullResultSet: if self.connector == OR: - if node.negated: + if self.negated: empty = True break # We match everything. No need for any constraints. return '', [] - if node.negated: + if self.negated: empty = True continue empty = False if sql: - result.append(format % sql) + result.append(sql) result_params.extend(params) if empty: raise EmptyResultSet - conn = ' %s ' % node.connector - return conn.join(result), result_params + + conn = ' %s ' % self.connector + sql_string = conn.join(result) + if sql_string: + if self.negated: + sql_string = 'NOT (%s)' % sql_string + elif len(self.children) != 1: + sql_string = '(%s)' % sql_string + return sql_string, result_params def make_atom(self, child, qn): """ - Turn a tuple (table_alias, field_name, field_class, lookup_type, value) - into valid SQL. + Turn a tuple (table_alias, column_name, db_type, lookup_type, + value_annot, params) into valid SQL. Returns the string for the SQL fragment and the parameters to use for it. """ - table_alias, name, field, lookup_type, value = child + table_alias, name, db_type, lookup_type, value_annot, params = child if table_alias: lhs = '%s.%s' % (qn(table_alias), qn(name)) else: lhs = qn(name) - db_type = field and field.db_type() or None field_sql = connection.ops.field_cast_sql(db_type) % lhs - if isinstance(value, datetime.datetime): + if value_annot is datetime.datetime: cast_sql = connection.ops.datetime_cast_sql() else: cast_sql = '%s' - if field: - params = field.get_db_prep_lookup(lookup_type, value) - else: - params = Field().get_db_prep_lookup(lookup_type, value) if isinstance(params, QueryWrapper): extra, params = params.data else: @@ -123,11 +144,11 @@ class WhereNode(tree.Node): connection.operators[lookup_type] % cast_sql), params) if lookup_type == 'in': - if not value: + if not value_annot: raise EmptyResultSet if extra: return ('%s IN %s' % (field_sql, extra), params) - return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(value))), + return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(params))), params) elif lookup_type in ('range', 'year'): return ('%s BETWEEN %%s and %%s' % field_sql, params) @@ -135,8 +156,8 @@ class WhereNode(tree.Node): return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type, field_sql), params) elif lookup_type == 'isnull': - return ('%s IS %sNULL' % (field_sql, (not value and 'NOT ' or '')), - params) + return ('%s IS %sNULL' % (field_sql, + (not value_annot and 'NOT ' or '')), ()) elif lookup_type == 'search': return (connection.ops.fulltext_search_sql(field_sql), params) elif lookup_type in ('regex', 'iregex'): diff --git a/django/db/transaction.py b/django/db/transaction.py index c0c3779f6a..cd27cf6044 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -196,7 +196,10 @@ def commit_on_success(func): managed(True) try: res = func(*args, **kw) - except Exception, e: + except (Exception, KeyboardInterrupt, SystemExit): + # (We handle KeyboardInterrupt and SystemExit specially, since + # they don't inherit from Exception in Python 2.5, but we + # should treat them uniformly here.) if is_dirty(): rollback() raise diff --git a/django/http/__init__.py b/django/http/__init__.py index 7faa3c875e..ef15479983 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -9,14 +9,15 @@ try: except ImportError: from cgi import parse_qsl -from django.utils.datastructures import MultiValueDict, FileDict +from django.utils.datastructures import MultiValueDict, ImmutableList from django.utils.encoding import smart_str, iri_to_uri, force_unicode - +from django.http.multipartparser import MultiPartParser +from django.conf import settings +from django.core.files import uploadhandler from utils import * RESERVED_CHARS="!*'();:@&=+$,/?%#[]" - class Http404(Exception): pass @@ -25,6 +26,7 @@ class HttpRequest(object): # The encoding used in GET/POST dicts. None means use default setting. _encoding = None + _upload_handlers = [] def __init__(self): self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} @@ -102,39 +104,31 @@ class HttpRequest(object): encoding = property(_get_encoding, _set_encoding) -def parse_file_upload(header_dict, post_data): - """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" - import email, email.Message - from cgi import parse_header - raw_message = '\r\n'.join(['%s:%s' % pair for pair in header_dict.items()]) - raw_message += '\r\n\r\n' + post_data - msg = email.message_from_string(raw_message) - POST = QueryDict('', mutable=True) - FILES = MultiValueDict() - for submessage in msg.get_payload(): - if submessage and isinstance(submessage, email.Message.Message): - name_dict = parse_header(submessage['Content-Disposition'])[1] - # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads - # or {'name': 'blah'} for POST fields - # We assume all uploaded files have a 'filename' set. - if 'filename' in name_dict: - assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported" - if not name_dict['filename'].strip(): - continue - # IE submits the full path, so trim everything but the basename. - # (We can't use os.path.basename because that uses the server's - # directory separator, which may not be the same as the - # client's one.) - filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:] - FILES.appendlist(name_dict['name'], FileDict({ - 'filename': filename, - 'content-type': 'Content-Type' in submessage and submessage['Content-Type'] or None, - 'content': submessage.get_payload(), - })) - else: - POST.appendlist(name_dict['name'], submessage.get_payload()) - return POST, FILES + 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): + if not self._upload_handlers: + # If thre 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) + 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." + ) + parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) + return parser.parse() class QueryDict(MultiValueDict): """ diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py new file mode 100644 index 0000000000..8bed5681cf --- /dev/null +++ b/django/http/multipartparser.py @@ -0,0 +1,658 @@ +""" +Multi-part parsing for file uploads. + +Exposes one class, ``MultiPartParser``, which feeds chunks of uploaded data to +file upload handlers for processing. +""" +import cgi +from django.conf import settings +from django.core.exceptions import SuspiciousOperation +from django.utils.datastructures import MultiValueDict +from django.utils.encoding import force_unicode +from django.utils.text import unescape_entities +from django.core.files.uploadhandler import StopUpload, SkipFile, StopFutureHandlers + +__all__ = ('MultiPartParser','MultiPartParserError','InputStreamExhausted') + +class MultiPartParserError(Exception): + pass + +class InputStreamExhausted(Exception): + """ + No more reads are allowed from this device. + """ + pass + +RAW = "raw" +FILE = "file" +FIELD = "field" + +class MultiPartParser(object): + """ + A rfc2388 multipart/form-data parser. + + ``MultiValueDict.parse()`` reads the input stream in ``chunk_size`` chunks + and returns a tuple of ``(MultiValueDict(POST), MultiValueDict(FILES))``. If + ``file_upload_dir`` is defined files will be streamed to temporary files in + that directory. + """ + def __init__(self, META, input_data, upload_handlers, encoding=None): + """ + Initialize the MultiPartParser object. + + :META: + The standard ``META`` dictionary in Django request objects. + :input_data: + The raw post data, as a bytestring. + :upload_handler: + An UploadHandler instance that performs operations on the uploaded + data. + :encoding: + The encoding with which to treat the incoming data. + """ + + # + # Content-Type should containt multipart and the boundary information. + # + + content_type = META.get('HTTP_CONTENT_TYPE', META.get('CONTENT_TYPE', '')) + if not content_type.startswith('multipart/'): + raise MultiPartParserError('Invalid Content-Type: %s' % content_type) + + # Parse the header to get the boundary to split the parts. + ctypes, opts = parse_header(content_type) + boundary = opts.get('boundary') + if not boundary or not cgi.valid_boundary(boundary): + raise MultiPartParserError('Invalid boundary in multipart: %s' % boundary) + + + # + # Content-Length should contain the length of the body we are about + # to receive. + # + try: + content_length = int(META.get('HTTP_CONTENT_LENGTH', META.get('CONTENT_LENGTH',0))) + except (ValueError, TypeError): + # For now set it to 0; we'll try again later on down. + content_length = 0 + + if content_length <= 0: + # This means we shouldn't continue...raise an error. + raise MultiPartParserError("Invalid content length: %r" % content_length) + + self._boundary = boundary + self._input_data = input_data + + # For compatibility with low-level network APIs (with 32-bit integers), + # the chunk size should be < 2^31, but still divisible by 4. + self._chunk_size = min(2**31-4, *[x.chunk_size for x in upload_handlers if x.chunk_size]) + + self._meta = META + self._encoding = encoding or settings.DEFAULT_CHARSET + self._content_length = content_length + self._upload_handlers = upload_handlers + + def parse(self): + """ + Parse the POST data and break it into a FILES MultiValueDict and a POST + MultiValueDict. + + Returns a tuple containing the POST and FILES dictionary, respectively. + """ + # We have to import QueryDict down here to avoid a circular import. + from django.http import QueryDict + + encoding = self._encoding + handlers = self._upload_handlers + + limited_input_data = LimitBytes(self._input_data, self._content_length) + + # See if the handler will want to take care of the parsing. + # This allows overriding everything if somebody wants it. + for handler in handlers: + result = handler.handle_raw_input(limited_input_data, + self._meta, + self._content_length, + self._boundary, + encoding) + if result is not None: + return result[0], result[1] + + # Create the data structures to be used later. + self._post = QueryDict('', mutable=True) + self._files = MultiValueDict() + + # Instantiate the parser and stream: + stream = LazyStream(ChunkIter(limited_input_data, self._chunk_size)) + + # Whether or not to signal a file-completion at the beginning of the loop. + old_field_name = None + counters = [0] * len(handlers) + + try: + for item_type, meta_data, field_stream in Parser(stream, self._boundary): + if old_field_name: + # We run this at the beginning of the next loop + # since we cannot be sure a file is complete until + # we hit the next boundary/part of the multipart content. + self.handle_file_complete(old_field_name, counters) + + try: + disposition = meta_data['content-disposition'][1] + field_name = disposition['name'].strip() + except (KeyError, IndexError, AttributeError): + continue + + transfer_encoding = meta_data.get('content-transfer-encoding') + field_name = force_unicode(field_name, encoding, errors='replace') + + if item_type == FIELD: + # This is a post field, we can just set it in the post + if transfer_encoding == 'base64': + raw_data = field_stream.read() + try: + data = str(raw_data).decode('base64') + except: + data = raw_data + else: + data = field_stream.read() + + self._post.appendlist(field_name, + force_unicode(data, encoding, errors='replace')) + elif item_type == FILE: + # This is a file, use the handler... + file_successful = True + file_name = disposition.get('filename') + if not file_name: + continue + file_name = force_unicode(file_name, encoding, errors='replace') + file_name = self.IE_sanitize(unescape_entities(file_name)) + + content_type = meta_data.get('content-type', ('',))[0].strip() + try: + charset = meta_data.get('content-type', (0,{}))[1].get('charset', None) + except: + charset = None + + try: + content_length = int(meta_data.get('content-length')[0]) + except (IndexError, TypeError, ValueError): + content_length = None + + counters = [0] * len(handlers) + try: + for handler in handlers: + try: + handler.new_file(field_name, file_name, + content_type, content_length, + charset) + except StopFutureHandlers: + break + + for chunk in field_stream: + if transfer_encoding == 'base64': + # We only special-case base64 transfer encoding + try: + chunk = str(chunk).decode('base64') + except Exception, e: + # Since this is only a chunk, any error is an unfixable error. + raise MultiPartParserError("Could not decode base64 data: %r" % e) + + for i, handler in enumerate(handlers): + chunk_length = len(chunk) + chunk = handler.receive_data_chunk(chunk, + counters[i]) + counters[i] += chunk_length + if chunk is None: + # If the chunk received by the handler is None, then don't continue. + break + + except SkipFile, e: + file_successful = False + # Just use up the rest of this file... + exhaust(field_stream) + else: + # Handle file upload completions on next iteration. + old_field_name = field_name + else: + # If this is neither a FIELD or a FILE, just exhaust the stream. + exhaust(stream) + except StopUpload, e: + if not e.connection_reset: + exhaust(limited_input_data) + else: + # Make sure that the request data is all fed + exhaust(limited_input_data) + + # Signal that the upload has completed. + for handler in handlers: + retval = handler.upload_complete() + if retval: + break + + return self._post, self._files + + def handle_file_complete(self, old_field_name, counters): + """ + Handle all the signalling that takes place when a file is complete. + """ + for i, handler in enumerate(self._upload_handlers): + file_obj = handler.file_complete(counters[i]) + if file_obj: + # If it returns a file object, then set the files dict. + self._files.appendlist(force_unicode(old_field_name, + self._encoding, + errors='replace'), + file_obj) + break + + def IE_sanitize(self, filename): + """Cleanup filename from Internet Explorer full paths.""" + return filename and filename[filename.rfind("\\")+1:].strip() + +class LazyStream(object): + """ + The LazyStream wrapper allows one to get and "unget" bytes from a stream. + + Given a producer object (an iterator that yields bytestrings), the + LazyStream object will support iteration, reading, and keeping a "look-back" + variable in case you need to "unget" some bytes. + """ + def __init__(self, producer, length=None): + """ + Every LazyStream must have a producer when instantiated. + + A producer is an iterable that returns a string each time it + is called. + """ + self._producer = producer + self._empty = False + self._leftover = '' + self.length = length + self._position = 0 + self._remaining = length + + # These fields are to do sanity checking to make sure we don't + # have infinite loops getting/ungetting from the stream. The + # purpose overall is to raise an exception if we perform lots + # of stream get/unget gymnastics without getting + # anywhere. Naturally this is not sound, but most probably + # would indicate a bug if the exception is raised. + + # largest position tell us how far this lazystream has ever + # been advanced + self._largest_position = 0 + + # "modifications since" will start at zero and increment every + # time the position is modified but a new largest position is + # not achieved. + self._modifications_since = 0 + + def tell(self): + return self.position + + def read(self, size=None): + def parts(): + remaining = (size is not None and [size] or [self._remaining])[0] + # do the whole thing in one shot if no limit was provided. + if remaining is None: + yield ''.join(self) + return + + # otherwise do some bookkeeping to return exactly enough + # of the stream and stashing any extra content we get from + # the producer + while remaining != 0: + assert remaining > 0, 'remaining bytes to read should never go negative' + + chunk = self.next() + + emitting = chunk[:remaining] + self.unget(chunk[remaining:]) + remaining -= len(emitting) + yield emitting + + out = ''.join(parts()) + return out + + def next(self): + """ + Used when the exact number of bytes to read is unimportant. + + This procedure just returns whatever is chunk is conveniently returned + from the iterator instead. Useful to avoid unnecessary bookkeeping if + performance is an issue. + """ + if self._leftover: + output = self._leftover + self._leftover = '' + else: + output = self._producer.next() + self.position += len(output) + return output + + def close(self): + """ + Used to invalidate/disable this lazy stream. + + Replaces the producer with an empty list. Any leftover bytes that have + already been read will still be reported upon read() and/or next(). + """ + self._producer = [] + + def __iter__(self): + return self + + def unget(self, bytes): + """ + Places bytes back onto the front of the lazy stream. + + Future calls to read() will return those bytes first. The + stream position and thus tell() will be rewound. + """ + self.position -= len(bytes) + self._leftover = ''.join([bytes, self._leftover]) + + def _set_position(self, value): + if value > self._largest_position: + self._modifications_since = 0 + self._largest_position = value + else: + self._modifications_since += 1 + if self._modifications_since > 500: + raise SuspiciousOperation( + "The multipart parser got stuck, which shouldn't happen with" + " normal uploaded files. Check for malicious upload activity;" + " if there is none, report this to the Django developers." + ) + + self._position = value + + position = property(lambda self: self._position, _set_position) + +class ChunkIter(object): + """ + An iterable that will yield chunks of data. Given a file-like object as the + constructor, this object will yield chunks of read operations from that + object. + """ + def __init__(self, flo, chunk_size=64 * 1024): + self.flo = flo + self.chunk_size = chunk_size + + def next(self): + try: + data = self.flo.read(self.chunk_size) + except InputStreamExhausted: + raise StopIteration() + if data: + return data + else: + raise StopIteration() + + def __iter__(self): + return self + +class LimitBytes(object): + """ Limit bytes for a file object. """ + def __init__(self, fileobject, length): + self._file = fileobject + self.remaining = length + + def read(self, num_bytes=None): + """ + Read data from the underlying file. + If you ask for too much or there isn't anything left, + this will raise an InputStreamExhausted error. + """ + if self.remaining <= 0: + raise InputStreamExhausted() + if num_bytes is None: + num_bytes = self.remaining + else: + num_bytes = min(num_bytes, self.remaining) + self.remaining -= num_bytes + return self._file.read(num_bytes) + +class InterBoundaryIter(object): + """ + A Producer that will iterate over boundaries. + """ + def __init__(self, stream, boundary): + self._stream = stream + self._boundary = boundary + + def __iter__(self): + return self + + def next(self): + try: + return LazyStream(BoundaryIter(self._stream, self._boundary)) + except InputStreamExhausted: + raise StopIteration() + +class BoundaryIter(object): + """ + A Producer that is sensitive to boundaries. + + Will happily yield bytes until a boundary is found. Will yield the bytes + before the boundary, throw away the boundary bytes themselves, and push the + post-boundary bytes back on the stream. + + The future calls to .next() after locating the boundary will raise a + StopIteration exception. + """ + + def __init__(self, stream, boundary): + self._stream = stream + self._boundary = boundary + self._done = False + # rollback an additional six bytes because the format is like + # this: CRLF<boundary>[--CRLF] + self._rollback = len(boundary) + 6 + + # Try to use mx fast string search if available. Otherwise + # use Python find. Wrap the latter for consistency. + unused_char = self._stream.read(1) + if not unused_char: + raise InputStreamExhausted() + self._stream.unget(unused_char) + try: + from mx.TextTools import FS + self._fs = FS(boundary).find + except ImportError: + self._fs = lambda data: data.find(boundary) + + def __iter__(self): + return self + + def next(self): + if self._done: + raise StopIteration() + + stream = self._stream + rollback = self._rollback + + bytes_read = 0 + chunks = [] + for bytes in stream: + bytes_read += len(bytes) + chunks.append(bytes) + if bytes_read > rollback: + break + if not bytes: + break + else: + self._done = True + + if not chunks: + raise StopIteration() + + chunk = ''.join(chunks) + boundary = self._find_boundary(chunk, len(chunk) < self._rollback) + + if boundary: + end, next = boundary + stream.unget(chunk[next:]) + self._done = True + return chunk[:end] + else: + # make sure we dont treat a partial boundary (and + # its separators) as data + if not chunk[:-rollback]:# and len(chunk) >= (len(self._boundary) + 6): + # There's nothing left, we should just return and mark as done. + self._done = True + return chunk + else: + stream.unget(chunk[-rollback:]) + return chunk[:-rollback] + + def _find_boundary(self, data, eof = False): + """ + Finds a multipart boundary in data. + + Should no boundry exist in the data None is returned instead. Otherwise + a tuple containing the indices of the following are returned: + + * the end of current encapsulation + * the start of the next encapsulation + """ + index = self._fs(data) + if index < 0: + return None + else: + end = index + next = index + len(self._boundary) + data_len = len(data) - 1 + # backup over CRLF + if data[max(0,end-1)] == '\n': + end -= 1 + if data[max(0,end-1)] == '\r': + end -= 1 + # skip over --CRLF + #if data[min(data_len,next)] == '-': + # next += 1 + #if data[min(data_len,next)] == '-': + # next += 1 + #if data[min(data_len,next)] == '\r': + # next += 1 + #if data[min(data_len,next)] == '\n': + # next += 1 + return end, next + +def exhaust(stream_or_iterable): + """ + Completely exhausts an iterator or stream. + + Raise a MultiPartParserError if the argument is not a stream or an iterable. + """ + iterator = None + try: + iterator = iter(stream_or_iterable) + except TypeError: + iterator = ChunkIter(stream_or_iterable, 16384) + + if iterator is None: + raise MultiPartParserError('multipartparser.exhaust() was passed a non-iterable or stream parameter') + + for __ in iterator: + pass + +def parse_boundary_stream(stream, max_header_size): + """ + Parses one and exactly one stream that encapsulates a boundary. + """ + # Stream at beginning of header, look for end of header + # and parse it if found. The header must fit within one + # chunk. + chunk = stream.read(max_header_size) + + # 'find' returns the top of these four bytes, so we'll + # need to munch them later to prevent them from polluting + # the payload. + header_end = chunk.find('\r\n\r\n') + + def _parse_header(line): + main_value_pair, params = parse_header(line) + try: + name, value = main_value_pair.split(':', 1) + except: + raise ValueError("Invalid header: %r" % line) + return name, (value, params) + + if header_end == -1: + # we find no header, so we just mark this fact and pass on + # the stream verbatim + stream.unget(chunk) + return (RAW, {}, stream) + + header = chunk[:header_end] + + # here we place any excess chunk back onto the stream, as + # well as throwing away the CRLFCRLF bytes from above. + stream.unget(chunk[header_end + 4:]) + + TYPE = RAW + outdict = {} + + # Eliminate blank lines + for line in header.split('\r\n'): + # This terminology ("main value" and "dictionary of + # parameters") is from the Python docs. + try: + name, (value, params) = _parse_header(line) + except: + continue + + if name == 'content-disposition': + TYPE = FIELD + if params.get('filename'): + TYPE = FILE + + outdict[name] = value, params + + if TYPE == RAW: + stream.unget(chunk) + + return (TYPE, outdict, stream) + +class Parser(object): + def __init__(self, stream, boundary): + self._stream = stream + self._separator = '--' + boundary + + def __iter__(self): + boundarystream = InterBoundaryIter(self._stream, self._separator) + for sub_stream in boundarystream: + # Iterate over each part + yield parse_boundary_stream(sub_stream, 1024) + +def parse_header(line): + """ Parse the header into a key-value. """ + plist = _parse_header_params(';' + line) + key = plist.pop(0).lower() + pdict = {} + for p in plist: + i = p.find('=') + if i >= 0: + name = p[:i].strip().lower() + value = p[i+1:].strip() + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + value = value.replace('\\\\', '\\').replace('\\"', '"') + pdict[name] = value + return key, pdict + +def _parse_header_params(s): + plist = [] + while s[:1] == ';': + s = s[1:] + end = s.find(';') + while end > 0 and s.count('"', 0, end) % 2: + end = s.find(';', end + 1) + if end < 0: + end = len(s) + f = s[:end] + plist.append(f.strip()) + s = s[end:] + return plist diff --git a/django/middleware/http.py b/django/middleware/http.py index 2ef46c6b61..53b65c1034 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -19,14 +19,14 @@ class ConditionalGetMiddleware(object): # Setting the status is enough here. The response handling path # automatically removes content for this status code (in # http.conditional_content_removal()). - response.status = 304 + response.status_code = 304 if response.has_header('Last-Modified'): if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) if if_modified_since == response['Last-Modified']: # Setting the status code is enough here (same reasons as # above). - response.status = 304 + response.status_code = 304 return response diff --git a/django/newforms/fields.py b/django/newforms/fields.py index dfe46a2e3e..1feef31ee0 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -7,6 +7,11 @@ import datetime import os import re import time +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + # Python 2.3 fallbacks try: from decimal import Decimal, DecimalException @@ -416,9 +421,9 @@ except ImportError: class UploadedFile(StrAndUnicode): "A wrapper for files uploaded in a FileField" - def __init__(self, filename, content): + def __init__(self, filename, data): self.filename = filename - self.content = content + self.data = data def __unicode__(self): """ @@ -444,15 +449,34 @@ class FileField(Field): return None elif not data and initial: return initial + + if isinstance(data, dict): + # We warn once, then support both ways below. + import warnings + warnings.warn( + message = "Representing uploaded files as dictionaries is"\ + " deprecated. Use django.core.files.SimpleUploadedFile "\ + " instead.", + category = DeprecationWarning, + stacklevel = 2 + ) + try: - f = UploadedFile(data['filename'], data['content']) - except TypeError: + file_name = data.file_name + file_size = data.file_size + except AttributeError: + try: + file_name = data.get('filename') + file_size = bool(data['content']) + except (AttributeError, KeyError): + raise ValidationError(self.error_messages['invalid']) + + if not file_name: raise ValidationError(self.error_messages['invalid']) - except KeyError: - raise ValidationError(self.error_messages['missing']) - if not f.content: + if not file_size: raise ValidationError(self.error_messages['empty']) - return f + + return UploadedFile(file_name, data) class ImageField(FileField): default_error_messages = { @@ -470,15 +494,31 @@ class ImageField(FileField): elif not data and initial: return initial from PIL import Image - from cStringIO import StringIO + + # We need to get a file object for PIL. We might have a path or we might + # have to read the data into memory. + if hasattr(data, 'temporary_file_path'): + file = data.temporary_file_path() + else: + if hasattr(data, 'read'): + file = StringIO(data.read()) + else: + file = StringIO(data['content']) + try: # load() is the only method that can spot a truncated JPEG, # but it cannot be called sanely after verify() - trial_image = Image.open(StringIO(f.content)) + trial_image = Image.open(file) trial_image.load() + + # Since we're about to use the file again we have to reset the + # file object if possible. + if hasattr(file, 'reset'): + file.reset() + # verify() is the only method that can spot a corrupt PNG, # but it must be called immediately after the constructor - trial_image = Image.open(StringIO(f.content)) + trial_image = Image.open(file) trial_image.verify() except Exception: # Python Imaging Library doesn't recognize it as an image raise ValidationError(self.error_messages['invalid_image']) @@ -535,13 +575,17 @@ class BooleanField(Field): def clean(self, value): """Returns a Python boolean object.""" - super(BooleanField, self).clean(value) # Explicitly check for the string 'False', which is what a hidden field # will submit for False. Because bool("True") == True, we don't need to # handle that explicitly. if value == 'False': - return False - return bool(value) + value = False + else: + value = bool(value) + super(BooleanField, self).clean(value) + if not value and self.required: + raise ValidationError(self.error_messages['required']) + return value class NullBooleanField(BooleanField): """ diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py index fc8727185f..ee838d234a 100644 --- a/django/oldforms/__init__.py +++ b/django/oldforms/__init__.py @@ -680,18 +680,27 @@ class FileUploadField(FormField): self.field_name, self.is_required = field_name, is_required self.validator_list = [self.isNonEmptyFile] + validator_list - def isNonEmptyFile(self, field_data, all_data): + def isNonEmptyFile(self, new_data, all_data): + if hasattr(new_data, 'upload_errors'): + upload_errors = new_data.upload_errors() + if upload_errors: + raise validators.CriticalValidationError, upload_errors try: - content = field_data['content'] - except TypeError: - raise validators.CriticalValidationError, ugettext("No file was submitted. Check the encoding type on the form.") - if not content: + file_size = new_data.file_size + except AttributeError: + file_size = len(new_data['content']) + if not file_size: raise validators.CriticalValidationError, ugettext("The submitted file is empty.") def render(self, data): return mark_safe(u'<input type="file" id="%s" class="v%s" name="%s" />' % \ (self.get_id(), self.__class__.__name__, self.field_name)) + def prepare(self, new_data): + if hasattr(new_data, 'upload_errors'): + upload_errors = new_data.upload_errors() + new_data[self.field_name] = { '_file_upload_error': upload_errors } + def html2python(data): if data is None: raise EmptyValue diff --git a/django/test/client.py b/django/test/client.py index a15876e6f9..87731043a7 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -1,7 +1,11 @@ import urllib import sys import os -from cStringIO import StringIO +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + from django.conf import settings from django.contrib.auth import authenticate, login from django.core.handlers.base import BaseHandler @@ -19,6 +23,27 @@ from django.utils.itercompat import is_iterable BOUNDARY = 'BoUnDaRyStRiNg' MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY + +class FakePayload(object): + """ + A wrapper around StringIO that restricts what can be read since data from + the network can't be seeked and cannot be read outside of its content + length. This makes sure that views can't do anything under the test client + that wouldn't work in Real Life. + """ + def __init__(self, content): + self.__content = StringIO(content) + self.__len = len(content) + + def read(self, num_bytes=None): + if num_bytes is None: + num_bytes = self.__len or 1 + assert self.__len >= num_bytes, "Cannot read more than the available bytes from the HTTP incoming data." + content = self.__content.read(num_bytes) + self.__len -= num_bytes + return content + + class ClientHandler(BaseHandler): """ A HTTP Handler that can be used for testing purposes. @@ -39,7 +64,7 @@ class ClientHandler(BaseHandler): request = WSGIRequest(environ) response = self.get_response(request) - # Apply response middleware + # Apply response middleware. for middleware_method in self._response_middleware: response = middleware_method(request, response) response = self.apply_response_fixes(request, response) @@ -49,14 +74,15 @@ class ClientHandler(BaseHandler): return response def store_rendered_templates(store, signal, sender, template, context): - "A utility function for storing templates and contexts that are rendered" + """ + Stores templates and contexts that are rendered. + """ store.setdefault('template',[]).append(template) store.setdefault('context',[]).append(context) def encode_multipart(boundary, data): """ - A simple method for encoding multipart POST data from a dictionary of - form values. + Encodes multipart POST data from a dictionary of form values. The key will be used as the form data name; the value will be transmitted as content. If the value is a file, the contents of the file will be sent @@ -68,7 +94,8 @@ def encode_multipart(boundary, data): if isinstance(value, file): lines.extend([ '--' + boundary, - 'Content-Disposition: form-data; name="%s"; filename="%s"' % (to_str(key), to_str(os.path.basename(value.name))), + 'Content-Disposition: form-data; name="%s"; filename="%s"' \ + % (to_str(key), to_str(os.path.basename(value.name))), 'Content-Type: application/octet-stream', '', value.read() @@ -122,13 +149,14 @@ class Client: def store_exc_info(self, *args, **kwargs): """ - Utility method that can be used to store exceptions when they are - generated by a view. + Stores exceptions when they are generated by a view. """ self.exc_info = sys.exc_info() def _session(self): - "Obtain the current session variables" + """ + Obtains the current session variables. + """ if 'django.contrib.sessions' in settings.INSTALLED_APPS: engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None) @@ -144,7 +172,6 @@ class Client: Assumes defaults for the query environment, which can be overridden using the arguments to the request. """ - environ = { 'HTTP_COOKIE': self.cookies, 'PATH_INFO': '/', @@ -158,13 +185,13 @@ class Client: environ.update(self.defaults) environ.update(request) - # Curry a data dictionary into an instance of - # the template renderer callback function + # Curry a data dictionary into an instance of the template renderer + # callback function. data = {} on_template_render = curry(store_rendered_templates, data) dispatcher.connect(on_template_render, signal=signals.template_rendered) - # Capture exceptions created by the handler + # Capture exceptions created by the handler. dispatcher.connect(self.store_exc_info, signal=got_request_exception) try: @@ -187,14 +214,14 @@ class Client: exc_info = self.exc_info self.exc_info = None raise exc_info[1], None, exc_info[2] - - # Save the client and request that stimulated the response + + # Save the client and request that stimulated the response. response.client = self response.request = request - # Add any rendered template detail to the response + # Add any rendered template detail to the response. # If there was only one template rendered (the most likely case), - # flatten the list to a single element + # flatten the list to a single element. for detail in ('template', 'context'): if data.get(detail): if len(data[detail]) == 1: @@ -204,14 +231,16 @@ class Client: else: setattr(response, detail, None) - # Update persistent cookie data + # Update persistent cookie data. if response.cookies: self.cookies.update(response.cookies) return response def get(self, path, data={}, **extra): - "Request a response from the server using GET." + """ + Requests a response from the server using GET. + """ r = { 'CONTENT_LENGTH': None, 'CONTENT_TYPE': 'text/html; charset=utf-8', @@ -224,8 +253,9 @@ class Client: return self.request(**r) def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra): - "Request a response from the server using POST." - + """ + Requests a response from the server using POST. + """ if content_type is MULTIPART_CONTENT: post_data = encode_multipart(BOUNDARY, data) else: @@ -236,37 +266,43 @@ class Client: 'CONTENT_TYPE': content_type, 'PATH_INFO': urllib.unquote(path), 'REQUEST_METHOD': 'POST', - 'wsgi.input': StringIO(post_data), + 'wsgi.input': FakePayload(post_data), } r.update(extra) return self.request(**r) def login(self, **credentials): - """Set the Client to appear as if it has sucessfully logged into a site. + """ + Sets the Client to appear as if it has successfully logged into a site. Returns True if login is possible; False if the provided credentials are incorrect, or the user is inactive, or if the sessions framework is not available. """ user = authenticate(**credentials) - if user and user.is_active and 'django.contrib.sessions' in settings.INSTALLED_APPS: + if user and user.is_active \ + and 'django.contrib.sessions' in settings.INSTALLED_APPS: engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) - # Create a fake request to store login details + # Create a fake request to store login details. request = HttpRequest() request.session = engine.SessionStore() login(request, user) - # Set the cookie to represent the session - self.cookies[settings.SESSION_COOKIE_NAME] = request.session.session_key - self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None - self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/' - self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN - self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None - self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None + # Set the cookie to represent the session. + session_cookie = settings.SESSION_COOKIE_NAME + self.cookies[session_cookie] = request.session.session_key + cookie_data = { + 'max-age': None, + 'path': '/', + 'domain': settings.SESSION_COOKIE_DOMAIN, + 'secure': settings.SESSION_COOKIE_SECURE or None, + 'expires': None, + } + self.cookies[session_cookie].update(cookie_data) - # Save the session values + # Save the session values. request.session.save() return True @@ -274,7 +310,8 @@ class Client: return False def logout(self): - """Removes the authenticated user's cookies. + """ + Removes the authenticated user's cookies. Causes the authenticated user to be logged out. """ diff --git a/django/test/testcases.py b/django/test/testcases.py index ee83b960a6..3bad3995bb 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -4,10 +4,12 @@ from urlparse import urlsplit, urlunsplit from django.http import QueryDict from django.db import transaction +from django.conf import settings from django.core import mail from django.core.management import call_command from django.test import _doctest as doctest from django.test.client import Client +from django.core.urlresolvers import clear_url_caches normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s) @@ -54,6 +56,8 @@ class TestCase(unittest.TestCase): * Flushing the database. * If the Test Case class has a 'fixtures' member, installing the named fixtures. + * If the Test Case class has a 'urls' member, replace the + ROOT_URLCONF with it. * Clearing the mail test outbox. """ call_command('flush', verbosity=0, interactive=False) @@ -61,6 +65,10 @@ class TestCase(unittest.TestCase): # We have to use this slightly awkward syntax due to the fact # that we're using *args and **kwargs together. call_command('loaddata', *self.fixtures, **{'verbosity': 0}) + if hasattr(self, 'urls'): + self._old_root_urlconf = settings.ROOT_URLCONF + settings.ROOT_URLCONF = self.urls + clear_url_caches() mail.outbox = [] def __call__(self, result=None): @@ -79,6 +87,23 @@ class TestCase(unittest.TestCase): result.addError(self, sys.exc_info()) return super(TestCase, self).__call__(result) + try: + self._post_teardown() + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + import sys + result.addError(self, sys.exc_info()) + return + + def _post_teardown(self): + """ Performs any post-test things. This includes: + + * Putting back the original ROOT_URLCONF if it was changed. + """ + if hasattr(self, '_old_root_urlconf'): + settings.ROOT_URLCONF = self._old_root_urlconf + clear_url_caches() def assertRedirects(self, response, expected_url, status_code=302, target_status_code=200, host=None): diff --git a/django/utils/daemonize.py b/django/utils/daemonize.py index f0c2f684bd..68e5392d89 100644 --- a/django/utils/daemonize.py +++ b/django/utils/daemonize.py @@ -2,7 +2,8 @@ import os import sys if os.name == 'posix': - def become_daemon(our_home_dir='.', out_log='/dev/null', err_log='/dev/null'): + def become_daemon(our_home_dir='.', out_log='/dev/null', + err_log='/dev/null', umask=022): "Robustly turn into a UNIX daemon, running in our_home_dir." # First fork try: @@ -13,7 +14,7 @@ if os.name == 'posix': sys.exit(1) os.setsid() os.chdir(our_home_dir) - os.umask(0) + os.umask(umask) # Second fork try: @@ -32,13 +33,13 @@ if os.name == 'posix': # Set custom file descriptors so that they get proper buffering. sys.stdout, sys.stderr = so, se else: - def become_daemon(our_home_dir='.', out_log=None, err_log=None): + def become_daemon(our_home_dir='.', out_log=None, err_log=None, umask=022): """ If we're not running under a POSIX system, just simulate the daemon mode by doing redirections and directory changing. """ os.chdir(our_home_dir) - os.umask(0) + os.umask(umask) sys.stdin.close() sys.stdout.close() sys.stderr.close() diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 21a72f2d1e..f27bc1cfff 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -332,17 +332,49 @@ class DotExpandedDict(dict): except TypeError: # Special-case if current isn't a dict. current = {bits[-1]: v} -class FileDict(dict): +class ImmutableList(tuple): """ - A dictionary used to hold uploaded file contents. The only special feature - here is that repr() of this object won't dump the entire contents of the - file to the output. A handy safeguard for a large file upload. + A tuple-like object that raises useful errors when it is asked to mutate. + + Example:: + + >>> a = ImmutableList(range(5), warning="You cannot mutate this.") + >>> a[3] = '4' + Traceback (most recent call last): + ... + AttributeError: You cannot mutate this. """ - def __repr__(self): - if 'content' in self: - d = dict(self, content='<omitted>') - return dict.__repr__(d) - return dict.__repr__(self) + + def __new__(cls, *args, **kwargs): + if 'warning' in kwargs: + warning = kwargs['warning'] + del kwargs['warning'] + else: + warning = 'ImmutableList object is immutable.' + self = tuple.__new__(cls, *args, **kwargs) + self.warning = warning + return self + + def complain(self, *wargs, **kwargs): + if isinstance(self.warning, Exception): + raise self.warning + else: + raise AttributeError, self.warning + + # All list mutation functions complain. + __delitem__ = complain + __delslice__ = complain + __iadd__ = complain + __imul__ = complain + __setitem__ = complain + __setslice__ = complain + append = complain + extend = complain + insert = complain + pop = complain + remove = complain + sort = complain + reverse = complain class DictWrapper(dict): """ diff --git a/django/utils/text.py b/django/utils/text.py index aa190c8c4f..3686a454a8 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -3,6 +3,7 @@ from django.conf import settings from django.utils.encoding import force_unicode from django.utils.functional import allow_lazy from django.utils.translation import ugettext_lazy +from htmlentitydefs import name2codepoint # Capitalizes the first letter of a string. capfirst = lambda x: x and force_unicode(x)[0].upper() + force_unicode(x)[1:] @@ -222,3 +223,26 @@ def smart_split(text): yield bit smart_split = allow_lazy(smart_split, unicode) +def _replace_entity(match): + text = match.group(1) + if text[0] == u'#': + text = text[1:] + try: + if text[0] in u'xX': + c = int(text[1:], 16) + else: + c = int(text) + return unichr(c) + except ValueError: + return match.group(0) + else: + try: + return unichr(name2codepoint[text]) + except (ValueError, KeyError): + return match.group(0) + +_entity_re = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));") + +def unescape_entities(text): + return _entity_re.sub(_replace_entity, text) +unescape_entities = allow_lazy(unescape_entities, unicode) diff --git a/django/utils/tree.py b/django/utils/tree.py index a62a4ae6c3..5dd9f56799 100644 --- a/django/utils/tree.py +++ b/django/utils/tree.py @@ -29,6 +29,22 @@ class Node(object): self.subtree_parents = [] self.negated = negated + # We need this because of django.db.models.query_utils.Q. Q. __init__() is + # problematic, but it is a natural Node subclass in all other respects. + def _new_instance(cls, children=None, connector=None, negated=False): + """ + This is called to create a new instance of this class when we need new + Nodes (or subclasses) in the internal code in this class. Normally, it + just shadows __init__(). However, subclasses with an __init__ signature + that is not an extension of Node.__init__ might need to implement this + method to allow a Node to create a new instance of them (if they have + any extra setting up to do). + """ + obj = Node(children, connector, negated) + obj.__class__ = cls + return obj + _new_instance = classmethod(_new_instance) + def __str__(self): if self.negated: return '(NOT (%s: %s))' % (self.connector, ', '.join([str(c) for c @@ -82,7 +98,8 @@ class Node(object): else: self.children.append(node) else: - obj = Node(self.children, self.connector, self.negated) + obj = self._new_instance(self.children, self.connector, + self.negated) self.connector = conn_type self.children = [obj, node] @@ -96,7 +113,8 @@ class Node(object): Interpreting the meaning of this negate is up to client code. This method is useful for implementing "not" arrangements. """ - self.children = [Node(self.children, self.connector, not self.negated)] + self.children = [self._new_instance(self.children, self.connector, + not self.negated)] self.connector = self.default def start_subtree(self, conn_type): @@ -108,12 +126,13 @@ class Node(object): if len(self.children) == 1: self.connector = conn_type elif self.connector != conn_type: - self.children = [Node(self.children, self.connector, self.negated)] + self.children = [self._new_instance(self.children, self.connector, + self.negated)] self.connector = conn_type self.negated = False - self.subtree_parents.append(Node(self.children, self.connector, - self.negated)) + self.subtree_parents.append(self.__class__(self.children, + self.connector, self.negated)) self.connector = self.default self.negated = False self.children = [] @@ -126,7 +145,7 @@ class Node(object): the current instances state to be the parent. """ obj = self.subtree_parents.pop() - node = Node(self.children, self.connector) + node = self.__class__(self.children, self.connector) self.connector = obj.connector self.negated = obj.negated self.children = obj.children diff --git a/docs/apache_auth.txt b/docs/apache_auth.txt index 859050e716..62fd191896 100644 --- a/docs/apache_auth.txt +++ b/docs/apache_auth.txt @@ -39,9 +39,10 @@ with the standard ``Auth*`` and ``Require`` directives:: example at the bottom of this note). You'll also need to insert configuration directives that prevent Apache - from trying to use other authentication modules. Depending on which other - authentication modules you have loaded, you might need one or more of - the following directives:: + from trying to use other authentication modules, as well as specifying + the ``AuthUserFile`` directive and pointing it to ``/dev/null``. Depending + on which other authentication modules you have loaded, you might need one + or more of the following directives:: AuthBasicAuthoritative Off AuthDefaultAuthoritative Off @@ -65,6 +66,7 @@ with the standard ``Auth*`` and ``Require`` directives:: <Location /example/> AuthType Basic AuthName "example.com" + **AuthUserFile /dev/null** **AuthBasicAuthoritative Off** Require valid-user diff --git a/docs/db-api.txt b/docs/db-api.txt index f80d63797a..9a604bf320 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -443,6 +443,31 @@ This is roughly equivalent to:: Note, however, that the first of these will raise ``IndexError`` while the second will raise ``DoesNotExist`` if no objects match the given criteria. +Combining QuerySets +------------------- + +If you have two ``QuerySet`` instances that act on the same model, you can +combine them using ``&`` and ``|`` to get the items that are in both result +sets or in either results set, respectively. For example:: + + Entry.objects.filter(pubdate__gte=date1) & \ + Entry.objects.filter(headline__startswith="What") + +will combine the two queries into a single SQL query. Of course, in this case +you could have achieved the same result using multiple filters on the same +``QuerySet``, but sometimes the ability to combine individual ``QuerySet`` +instance is useful. + +Be careful, if you are using ``extra()`` to add custom handling to your +``QuerySet`` however. All the ``extra()`` components are merged and the result +may or may not make sense. If you are using custom SQL fragments in your +``extra()`` calls, Django will not inspect these fragments to see if they need +to be rewritten because of changes in the merged query. So test the effects +carefully. Also realise that if you are combining two ``QuerySets`` with +``|``, you cannot use ``extra(select=...)`` or ``extra(where=...)`` on *both* +``QuerySets``. You can only use those calls on one or the other (Django will +raise a ``ValueError`` if you try to use this incorrectly). + QuerySet methods that return new QuerySets ------------------------------------------ diff --git a/docs/flatpages.txt b/docs/flatpages.txt index a538217642..f1a354c653 100644 --- a/docs/flatpages.txt +++ b/docs/flatpages.txt @@ -14,9 +14,14 @@ custom Django application. A flatpage can use a custom template or a default, systemwide flatpage template. It can be associated with one, or multiple, sites. +**New in Django development version** + +The content field may optionally be left blank if you prefer to put your +content in a custom template. + Here are some examples of flatpages on Django-powered sites: - * http://www.chicagocrime.org/about/ + * http://www.everyblock.com/about/ * http://www.lawrence.com/about/contact/ Installation diff --git a/docs/model-api.txt b/docs/model-api.txt index 1bed4ee818..4e7b5c3096 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -2155,7 +2155,7 @@ still only creating one database table per child model at the database level. When an abstract base class is created, Django makes any ``Meta`` inner class you declared on the base class available as an attribute. If a child class -does not declared its own ``Meta`` class, it will inherit the parent's +does not declare its own ``Meta`` class, it will inherit the parent's ``Meta``. If the child wants to extend the parent's ``Meta`` class, it can subclass it. For example:: diff --git a/docs/newforms.txt b/docs/newforms.txt index 04e4c1aaa0..296fc04c85 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -805,12 +805,12 @@ ContactForm to include an ``ImageField`` called ``mugshot``, we need to bind the file data containing the mugshot image:: # Bound form with an image field + >>> from django.core.files.uploadedfile import SimpleUploadedFile >>> data = {'subject': 'hello', ... 'message': 'Hi there', ... 'sender': 'foo@example.com', ... 'cc_myself': True} - >>> file_data = {'mugshot': {'filename':'face.jpg' - ... 'content': <file data>}} + >>> file_data = {'mugshot': SimpleUploadedFile('face.jpg', <file data>)} >>> f = ContactFormWithMugshot(data, file_data) In practice, you will usually specify ``request.FILES`` as the source diff --git a/docs/request_response.txt b/docs/request_response.txt index 866a697e31..54fc24df9e 100644 --- a/docs/request_response.txt +++ b/docs/request_response.txt @@ -80,19 +80,36 @@ All attributes except ``session`` should be considered read-only. strings. ``FILES`` + + .. admonition:: Changed in Django development version + + In previous versions of Django, ``request.FILES`` contained + simple ``dict`` objects representing uploaded files. This is + no longer true -- files are represented by ``UploadedFile`` + objects as described below. + + These ``UploadedFile`` objects will emulate the old-style ``dict`` + interface, but this is deprecated and will be removed in the next + release of Django. + A dictionary-like object containing all uploaded files. Each key in ``FILES`` is the ``name`` from the ``<input type="file" name="" />``. Each - value in ``FILES`` is a standard Python dictionary with the following three - keys: + value in ``FILES`` is an ``UploadedFile`` object containing the following + attributes: - * ``filename`` -- The name of the uploaded file, as a Python string. - * ``content-type`` -- The content type of the uploaded file. - * ``content`` -- The raw content of the uploaded file. + * ``read(num_bytes=None)`` -- Read a number of bytes from the file. + * ``file_name`` -- The name of the uploaded file. + * ``file_size`` -- The size, in bytes, of the uploaded file. + * ``chunk()`` -- A generator that yields sequential chunks of data. + See `File Uploads`_ for more information. + Note that ``FILES`` will only contain data if the request method was POST and the ``<form>`` that posted to the request had ``enctype="multipart/form-data"``. Otherwise, ``FILES`` will be a blank dictionary-like object. + + .. _File Uploads: ../upload_handling/ ``META`` A standard Python dictionary containing all available HTTP headers. diff --git a/docs/settings.txt b/docs/settings.txt index 3fe999d0d2..a68d2ff92f 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -279,7 +279,7 @@ Default: ``''`` (Empty string) The database backend to use. The build-in database backends are ``'postgresql_psycopg2'``, ``'postgresql'``, ``'mysql'``, ``'mysql_old'``, -``'sqlite3'`` and ``'oracle'``. +``'sqlite3'``, ``'oracle'``, and ``'oracle'``. In the Django development version, you can use a database backend that doesn't ship with Django by setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e. @@ -530,6 +530,43 @@ Default: ``'utf-8'`` The character encoding used to decode any files read from disk. This includes template files and initial SQL data files. +FILE_UPLOAD_HANDLERS +-------------------- + +**New in Django development version** + +Default:: + + ("django.core.files.fileuploadhandler.MemoryFileUploadHandler", + "django.core.files.fileuploadhandler.TemporaryFileUploadHandler",) + +A tuple of handlers to use for uploading. See `file uploads`_ for details. + +.. _file uploads: ../upload_handling/ + +FILE_UPLOAD_MAX_MEMORY_SIZE +--------------------------- + +**New in Django development version** + +Default: ``2621440`` (i.e. 2.5 MB). + +The maximum size (in bytes) that an upload will be before it gets streamed to +the file system. See `file uploads`_ for details. + +FILE_UPLOAD_TEMP_DIR +-------------------- + +**New in Django development version** + +Default: ``None`` + +The directory to store data temporarily while uploading files. If ``None``, +Django will use the standard temporary directory for the operating system. For +example, this will default to '/tmp' on *nix-style operating systems. + +See `file uploads`_ for details. + FIXTURE_DIRS ------------- diff --git a/docs/sitemaps.txt b/docs/sitemaps.txt index eb749dda2f..6a16e61879 100644 --- a/docs/sitemaps.txt +++ b/docs/sitemaps.txt @@ -317,3 +317,14 @@ A more efficient solution, however, would be to call ``ping_google()`` from a cron script, or some other scheduled task. The function makes an HTTP request to Google's servers, so you may not want to introduce that network overhead each time you call ``save()``. + +Pinging Google via `manage.py` +------------------------------ + +**New in Django development version** + +Once the sitemaps application is added to your project, you may also +ping the Google server's through the command line manage.py interface:: + + python manage.py ping_google [/sitemap.xml] + diff --git a/docs/templates.txt b/docs/templates.txt index 3a557c1476..04a46580c5 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -477,11 +477,11 @@ escaped when the template is written. This means you would write :: - {{ data|default:"3 > 2" }} + {{ data|default:"3 < 2" }} ...rather than :: - {{ data|default:"3 > 2" }} <-- Bad! Don't do this. + {{ data|default:"3 < 2" }} <-- Bad! Don't do this. This doesn't affect what happens to data coming from the variable itself. The variable's contents are still automatically escaped, if necessary, because diff --git a/docs/testing.txt b/docs/testing.txt index befa6979af..0b18545efb 100644 --- a/docs/testing.txt +++ b/docs/testing.txt @@ -797,6 +797,37 @@ another test, or by the order of test execution. .. _dumpdata documentation: ../django-admin/#dumpdata-appname-appname .. _loaddata documentation: ../django-admin/#loaddata-fixture-fixture +URLconf configuration +~~~~~~~~~~~~~~~~~~~~~ + +**New in Django development version** + +If your application provides views, you may want to include tests that +use the test client to exercise those views. However, an end user is free +to deploy the views in your application at any URL of their choosing. +This means that your tests can't rely upon the fact that your views will +be available at a particular URL. + +In order to provide a reliable URL space for your test, +``django.test.TestCase`` provides the ability to customize the URLconf +configuration for the duration of the execution of a test suite. +If your ``TestCase`` instance defines an ``urls`` attribute, the +``TestCase`` will use the value of that attribute as the ``ROOT_URLCONF`` +for the duration of that test. + +For example:: + + from django.test import TestCase + + class TestMyViews(TestCase): + urls = 'myapp.test_urls' + + def testIndexPageView(self): + # Here you'd test your view using ``Client``. + +This test case will use the contents of ``myapp.test_urls`` as the +URLconf for the duration of the test case. + Emptying the test outbox ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/upload_handling.txt b/docs/upload_handling.txt new file mode 100644 index 0000000000..34cd085ac9 --- /dev/null +++ b/docs/upload_handling.txt @@ -0,0 +1,346 @@ +============ +File Uploads +============ + +**New in Django development version** + +Most Web sites wouldn't be complete without a way to upload files. When Django +handles a file upload, the file data ends up placed in ``request.FILES`` (for +more on the ``request`` object see the documentation for `request and response +objects`_). This document explains how files are stored on disk and in memory, +and how to customize the default behavior. + +.. _request and response objects: ../request_response/#attributes + +Basic file uploads +================== + +Consider a simple form containing a ``FileField``:: + + from django import newforms as forms + + class UploadFileForm(forms.Form): + title = forms.CharField(max_length=50) + file = forms.FileField() + +A view handling this form will receive the file data in ``request.FILES``, which +is a dictionary containing a key for each ``FileField`` (or ``ImageField``, or +other ``FileField`` subclass) in the form. So the data from the above form would +be accessible as ``request.FILES['file']``. + +Most of the time, you'll simply pass the file data from ``request`` into the +form as described in `binding uploaded files to a form`_. This would look +something like:: + + from django.http import HttpResponseRedirect + from django.shortcuts import render_to_response + + # Imaginary function to handle an uploaded file. + from somewhere import handle_uploaded_file + + def upload_file(request): + if request.method == 'POST': + form = UploadFileForm(request.POST, request.FILES) + if form.is_valid(): + handle_uploaded_file(request.FILES['file']) + return HttpResponseRedirect('/success/url/') + else: + form = UploadFileForm() + return render_to_response('upload.html', {'form': form}) + +.. _binding uploaded files to a form: ../newforms/#binding-uploaded-files-to-a- form + +Notice that we have to pass ``request.FILES`` into the form's constructor; this +is how file data gets bound into a form. + +Handling uploaded files +----------------------- + +The final piece of the puzzle is handling the actual file data from +``request.FILES``. Each entry in this dictionary is an ``UploadedFile`` object +-- a simple wrapper around an uploaded file. You'll usually use one of these +methods to access the uploaded content: + + ``UploadedFile.read()`` + Read the entire uploaded data from the file. Be careful with this + method: if the uploaded file is huge it can overwhelm your system if you + try to read it into memory. You'll probably want to use ``chunk()`` + instead; see below. + + ``UploadedFile.multiple_chunks()`` + Returns ``True`` if the uploaded file is big enough to require + reading in multiple chunks. By default this will be any file + larger than 2.5 megabytes, but that's configurable; see below. + + ``UploadedFile.chunk()`` + A generator returning chunks of the file. If ``multiple_chunks()`` is + ``True``, you should use this method in a loop instead of ``read()``. + + In practice, it's often easiest simply to use ``chunks()`` all the time; + see the example below. + + ``UploadedFile.file_name`` + The name of the uploaded file (e.g. ``my_file.txt``). + + ``UploadedFile.file_size`` + The size, in bytes, of the uploaded file. + +There are a few other methods and attributes available on ``UploadedFile`` +objects; see `UploadedFile objects`_ for a complete reference. + +Putting it all together, here's a common way you might handle an uploaded file:: + + def handle_uploaded_file(f): + destination = open('some/file/name.txt', 'wb') + for chunk in f.chunks(): + destination.write(chunk) + +Looping over ``UploadedFile.chunks()`` instead of using ``read()`` ensures that +large files don't overwhelm your system's memory. + +Where uploaded data is stored +----------------------------- + +Before you save uploaded files, the data needs to be stored somewhere. + +By default, if an uploaded file is smaller than 2.5 megabytes, Django will hold +the entire contents of the upload in memory. This means that saving the file +involves only a read from memory and a write to disk and thus is very fast. + +However, if an uploaded file is too large, Django will write the uploaded file +to a temporary file stored in your system's temporary directory. On a Unix-like +platform this means you can expect Django to generate a file called something +like ``/tmp/tmpzfp6I6.upload``. If an upload is large enough, you can watch this +file grow in size as Django streams the data onto disk. + +These specifics -- 2.5 megabytes; ``/tmp``; etc. -- are simply "reasonable +defaults". Read on for details on how you can customize or completely replace +upload behavior. + +Changing upload handler behavior +-------------------------------- + +Three `settings`_ control Django's file upload behavior: + + ``FILE_UPLOAD_MAX_MEMORY_SIZE`` + The maximum size, in bytes, for files that will be uploaded + into memory. Files larger than ``FILE_UPLOAD_MAX_MEMORY_SIZE`` + will be streamed to disk. + + Defaults to 2.5 megabytes. + + ``FILE_UPLOAD_TEMP_DIR`` + The directory where uploaded files larger than ``FILE_UPLOAD_TEMP_DIR`` + will be stored. + + Defaults to your system's standard temporary directory (i.e. ``/tmp`` on + most Unix-like systems). + + ``FILE_UPLOAD_HANDLERS`` + The actual handlers for uploaded files. Changing this setting + allows complete customization -- even replacement -- of + Django's upload process. See `upload handlers`_, below, + for details. + + Defaults to:: + + ("django.core.files.uploadhandler.MemoryFileUploadHandler", + "django.core.files.uploadhandler.TemporaryFileUploadHandler",) + + Which means "try to upload to memory first, then fall back to temporary + files." + +.. _settings: ../settings/ + +``UploadedFile`` objects +======================== + +All ``UploadedFile`` objects define the following methods/attributes: + + ``UploadedFile.read(self, num_bytes=None)`` + Returns a byte string of length ``num_bytes``, or the complete file if + ``num_bytes`` is ``None``. + + ``UploadedFile.chunk(self, chunk_size=None)`` + A generator yielding small chunks from the file. If ``chunk_size`` isn't + given, chunks will be 64 kb. + + ``UploadedFile.multiple_chunks(self, chunk_size=None)`` + Returns ``True`` if you can expect more than one chunk when calling + ``UploadedFile.chunk(self, chunk_size)``. + + ``UploadedFile.file_size`` + The size, in bytes, of the uploaded file. + + ``UploadedFile.file_name`` + The name of the uploaded file as provided by the user. + + ``UploadedFile.content_type`` + The content-type header uploaded with the file (e.g. ``text/plain`` or + ``application/pdf``). Like any data supplied by the user, you shouldn't + trust that the uploaded file is actually this type. You'll still need to + validate that the file contains the content that the content-type header + claims -- "trust but verify." + + ``UploadedFile.charset`` + For ``text/*`` content-types, the character set (i.e. ``utf8``) supplied + by the browser. Again, "trust but verify" is the best policy here. + + ``UploadedFile.temporary_file_path()`` + Only files uploaded onto disk will have this method; it returns the full + path to the temporary uploaded file. + +Upload Handlers +=============== + +When a user uploads a file, Django passes off the file data to an *upload +handler* -- a small class that handles file data as it gets uploaded. Upload +handlers are initially defined in the ``FILE_UPLOAD_HANDLERS`` setting, which +defaults to:: + + ("django.core.files.uploadhandler.MemoryFileUploadHandler", + "django.core.files.uploadhandler.TemporaryFileUploadHandler",) + +Together the ``MemoryFileUploadHandler`` and ``TemporaryFileUploadHandler`` +provide Django's default file upload behavior of reading small files into memory +and large ones onto disk. + +You can write custom handlers that customize how Django handles files. You +could, for example, use custom handlers to enforce user-level quotas, compress +data on the fly, render progress bars, and even send data to another storage +location directly without storing it locally. + +Modifying upload handlers on the fly +------------------------------------ + +Sometimes particular views require different upload behavior. In these cases, +you can override upload handlers on a per-request basis by modifying +``request.upload_handlers``. By default, this list will contain the upload +handlers given by ``FILE_UPLOAD_HANDLERS``, but you can modify the list as you +would any other list. + +For instance, suppose you've written a ``ProgressBarUploadHandler`` that +provides feedback on upload progress to some sort of AJAX widget. You'd add this +handler to your upload handers like this:: + + request.upload_handlers.insert(0, ProgressBarUploadHandler()) + +You'd probably want to use ``list.insert()`` in this case (instead of +``append()``) because a progress bar handler would need to run *before* any +other handlers. Remember, the upload handlers are processed in order. + +If you want to replace the upload handlers completely, you can just assign a new +list:: + + request.upload_handlers = [ProgressBarUploadHandler()] + +.. note:: + + You can only modify upload handlers *before* accessing ``request.FILES`` -- + it doesn't make sense to change upload handlers after upload handling has + already started. If you try to modify ``request.upload_handlers`` after + reading from ``request.FILES`` Django will throw an error. + + Thus, you should always modify uploading handlers as early in your view as + possible. + +Writing custom upload handlers +------------------------------ + +All file upload handlers should be subclasses of +``django.core.files.uploadhandler.FileUploadHandler``. You can define upload +handlers wherever you wish. + +Required methods +~~~~~~~~~~~~~~~~ + +Custom file upload handlers **must** define the following methods: + + ``FileUploadHandler.receive_data_chunk(self, raw_data, start)`` + Receives a "chunk" of data from the file upload. + + ``raw_data`` is a byte string containing the uploaded data. + + ``start`` is the position in the file where this ``raw_data`` chunk + begins. + + The data you return will get fed into the subsequent upload handlers' + ``receive_data_chunk`` methods. In this way, one handler can be a + "filter" for other handlers. + + Return ``None`` from ``receive_data_chunk`` to sort-circuit remaining + upload handlers from getting this chunk.. This is useful if you're + storing the uploaded data yourself and don't want future handlers to + store a copy of the data. + + If you raise a ``StopUpload`` or a ``SkipFile`` exception, the upload + will abort or the file will be completely skipped. + + ``FileUploadHandler.file_complete(self, file_size)`` + Called when a file has finished uploading. + + The handler should return an ``UploadedFile`` object that will be stored + in ``request.FILES``. Handlers may also return ``None`` to indicate that + the ``UploadedFile`` object should come from subsequent upload handlers. + +Optional methods +~~~~~~~~~~~~~~~~ + +Custom upload handlers may also define any of the following optional methods or +attributes: + + ``FileUploadHandler.chunk_size`` + Size, in bytes, of the "chunks" Django should store into memory and feed + into the handler. That is, this attribute controls the size of chunks + fed into ``FileUploadHandler.receive_data_chunk``. + + For maximum performance the chunk sizes should be divisible by ``4`` and + should not exceed 2 GB (2\ :sup:`31` bytes) in size. When there are + multiple chunk sizes provided by multiple handlers, Django will use the + smallest chunk size defined by any handler. + + The default is 64*2\ :sup:`10` bytes, or 64 KB. + + ``FileUploadHandler.new_file(self, field_name, file_name, content_type, content_length, charset)`` + Callback signaling that a new file upload is starting. This is called + before any data has been fed to any upload handlers. + + ``field_name`` is a string name of the file ``<input>`` field. + + ``file_name`` is the unicode filename that was provided by the browser. + + ``content_type`` is the MIME type provided by the browser -- E.g. + ``'image/jpeg'``. + + ``content_length`` is the length of the image given by the browser. + Sometimes this won't be provided and will be ``None``., ``None`` + otherwise. + + ``charset`` is the character set (i.e. ``utf8``) given by the browser. + Like ``content_length``, this sometimes won't be provided. + + This method may raise a ``StopFutureHandlers`` exception to prevent + future handlers from handling this file. + + ``FileUploadHandler.upload_complete(self)`` + Callback signaling that the entire upload (all files) has completed. + + ``FileUploadHandler.handle_raw_input(self, input_data, META, content_length, boundary, encoding)`` + Allows the handler to completely override the parsing of the raw + HTTP input. + + ``input_data`` is a file-like object that supports ``read()``-ing. + + ``META`` is the same object as ``request.META``. + + ``content_length`` is the length of the data in ``input_data``. Don't + read more than ``content_length`` bytes from ``input_data``. + + ``boundary`` is the MIME boundary for this request. + + ``encoding`` is the encoding of the request. + + Return ``None`` if you want upload handling to continue, or a tuple of + ``(POST, FILES)`` if you want to return the new data structures suitable + for the request directly. + diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 470312f5ca..c856720a74 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -67,7 +67,13 @@ class TextFile(models.Model): class ImageFile(models.Model): description = models.CharField(max_length=20) - image = models.FileField(upload_to=tempfile.gettempdir()) + try: + # If PIL is available, try testing PIL. + # Otherwise, it's equivalent to TextFile above. + import Image + image = models.ImageField(upload_to=tempfile.gettempdir()) + except ImportError: + image = models.FileField(upload_to=tempfile.gettempdir()) def __unicode__(self): return self.description @@ -75,6 +81,7 @@ class ImageFile(models.Model): __test__ = {'API_TESTS': """ >>> from django import newforms as forms >>> from django.newforms.models import ModelForm +>>> from django.core.files.uploadedfile import SimpleUploadedFile The bare bones, absolutely nothing custom, basic case. @@ -792,7 +799,18 @@ False # Upload a file and ensure it all works as expected. ->>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test1.txt', 'content': 'hello world'}}) +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')}) +>>> f.is_valid() +True +>>> type(f.cleaned_data['file']) +<class 'django.newforms.fields.UploadedFile'> +>>> instance = f.save() +>>> instance.file +u'...test1.txt' + +>>> os.unlink(instance.get_file_filename()) + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')}) >>> f.is_valid() True >>> type(f.cleaned_data['file']) @@ -814,18 +832,30 @@ u'...test1.txt' u'...test1.txt' # Delete the current file since this is not done by Django. - >>> os.unlink(instance.get_file_filename()) # Override the file by uploading a new one. ->>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test2.txt', 'content': 'hello world'}}, instance=instance) +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')}, instance=instance) >>> f.is_valid() True >>> instance = f.save() >>> instance.file u'...test2.txt' +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_file_filename()) + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +u'...test2.txt' + +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_file_filename()) + >>> instance.delete() # Test the non-required FileField @@ -838,12 +868,26 @@ True >>> instance.file '' ->>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': {'filename': 'test3.txt', 'content': 'hello world'}}, instance=instance) +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance) >>> f.is_valid() True >>> instance = f.save() >>> instance.file u'...test3.txt' + +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_file_filename()) +>>> instance.delete() + +>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.file +u'...test3.txt' + +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_file_filename()) >>> instance.delete() # ImageField ################################################################### @@ -858,7 +902,19 @@ u'...test3.txt' >>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png")).read() ->>> f = ImageFileForm(data={'description': u'An image'}, files={'image': {'filename': 'test.png', 'content': image_data}}) +>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)}) +>>> f.is_valid() +True +>>> type(f.cleaned_data['image']) +<class 'django.newforms.fields.UploadedFile'> +>>> instance = f.save() +>>> instance.image +u'...test.png' + +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_image_filename()) + +>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)}) >>> f.is_valid() True >>> type(f.cleaned_data['image']) @@ -885,13 +941,26 @@ u'...test.png' # Override the file by uploading a new one. ->>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': {'filename': 'test2.png', 'content': image_data}}, instance=instance) +>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data)}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +u'...test2.png' + +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_image_filename()) +>>> instance.delete() + +>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data)}) >>> f.is_valid() True >>> instance = f.save() >>> instance.image u'...test2.png' +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_image_filename()) >>> instance.delete() # Test the non-required ImageField @@ -904,7 +973,18 @@ True >>> instance.image '' ->>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': {'filename': 'test3.png', 'content': image_data}}, instance=instance) +>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}, instance=instance) +>>> f.is_valid() +True +>>> instance = f.save() +>>> instance.image +u'...test3.png' + +# Delete the current file since this is not done by Django. +>>> os.unlink(instance.get_image_filename()) +>>> instance.delete() + +>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}) >>> f.is_valid() True >>> instance = f.save() diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index 277c5961e3..1f08a32903 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -200,6 +200,29 @@ InvalidPage: ... >>> paginator.page_range [1] +# ObjectPaginator can be passed lists too. +>>> paginator = ObjectPaginator([1, 2, 3], 5) +>>> paginator.hits +3 +>>> paginator.pages +1 +>>> paginator.page_range +[1] + + +# ObjectPaginator can be passed other objects with a count() method. +>>> class Container: +... def __len__(self): +... return 42 +>>> paginator = ObjectPaginator(Container(), 10) +>>> paginator.hits +42 +>>> paginator.pages +5 +>>> paginator.page_range +[1, 2, 3, 4, 5] + + ################## # Orphan support # ################## diff --git a/tests/regressiontests/bug639/tests.py b/tests/regressiontests/bug639/tests.py index f9596d06cb..2726dec897 100644 --- a/tests/regressiontests/bug639/tests.py +++ b/tests/regressiontests/bug639/tests.py @@ -9,6 +9,7 @@ import unittest from regressiontests.bug639.models import Photo from django.http import QueryDict from django.utils.datastructures import MultiValueDict +from django.core.files.uploadedfile import SimpleUploadedFile class Bug639Test(unittest.TestCase): @@ -21,12 +22,8 @@ class Bug639Test(unittest.TestCase): # Fake a request query dict with the file qd = QueryDict("title=Testing&image=", mutable=True) - qd["image_file"] = { - "filename" : "test.jpg", - "content-type" : "image/jpeg", - "content" : img - } - + qd["image_file"] = SimpleUploadedFile('test.jpg', img, 'image/jpeg') + manip = Photo.AddManipulator() manip.do_html2python(qd) p = manip.save(qd) @@ -39,4 +36,4 @@ class Bug639Test(unittest.TestCase): Make sure to delete the "uploaded" file to avoid clogging /tmp. """ p = Photo.objects.get() - os.unlink(p.get_image_filename())
\ No newline at end of file + os.unlink(p.get_image_filename()) diff --git a/tests/regressiontests/datastructures/tests.py b/tests/regressiontests/datastructures/tests.py index d6141b09ce..62c57bc019 100644 --- a/tests/regressiontests/datastructures/tests.py +++ b/tests/regressiontests/datastructures/tests.py @@ -117,14 +117,25 @@ Init from sequence of tuples >>> d['person']['2']['firstname'] ['Adrian'] -### FileDict ################################################################ - ->>> d = FileDict({'content': 'once upon a time...'}) ->>> repr(d) -"{'content': '<omitted>'}" ->>> d = FileDict({'other-key': 'once upon a time...'}) +### ImmutableList ################################################################ +>>> d = ImmutableList(range(10)) +>>> d.sort() +Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "/var/lib/python-support/python2.5/django/utils/datastructures.py", line 359, in complain + raise AttributeError, self.warning +AttributeError: ImmutableList object is immutable. >>> repr(d) -"{'other-key': 'once upon a time...'}" +'(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)' +>>> d = ImmutableList(range(10), warning="Object is immutable!") +>>> d[1] +1 +>>> d[1] = 'test' +Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "/var/lib/python-support/python2.5/django/utils/datastructures.py", line 359, in complain + raise AttributeError, self.warning +AttributeError: Object is immutable! ### DictWrapper ############################################################# diff --git a/tests/regressiontests/extra_regress/__init__.py b/tests/regressiontests/extra_regress/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/extra_regress/__init__.py diff --git a/tests/regressiontests/extra_regress/models.py b/tests/regressiontests/extra_regress/models.py new file mode 100644 index 0000000000..e6665222bf --- /dev/null +++ b/tests/regressiontests/extra_regress/models.py @@ -0,0 +1,55 @@ +import copy + +from django.db import models +from django.db.models.query import Q + + +class RevisionableModel(models.Model): + base = models.ForeignKey('self', null=True) + title = models.CharField(blank=True, max_length=255) + + def __unicode__(self): + return u"%s (%s, %s)" % (self.title, self.id, self.base.id) + + def save(self): + super(RevisionableModel, self).save() + if not self.base: + self.base = self + super(RevisionableModel, self).save() + + def new_revision(self): + new_revision = copy.copy(self) + new_revision.pk = None + return new_revision + +__test__ = {"API_TESTS": """ +### Regression tests for #7314 and #7372 + +>>> rm = RevisionableModel.objects.create(title='First Revision') +>>> rm.pk, rm.base.pk +(1, 1) + +>>> rm2 = rm.new_revision() +>>> rm2.title = "Second Revision" +>>> rm2.save() +>>> print u"%s of %s" % (rm2.title, rm2.base.title) +Second Revision of First Revision + +>>> rm2.pk, rm2.base.pk +(2, 1) + +Queryset to match most recent revision: +>>> qs = RevisionableModel.objects.extra(where=["%(table)s.id IN (SELECT MAX(rev.id) FROM %(table)s rev GROUP BY rev.base_id)" % {'table': RevisionableModel._meta.db_table,}],) +>>> qs +[<RevisionableModel: Second Revision (2, 1)>] + +Queryset to search for string in title: +>>> qs2 = RevisionableModel.objects.filter(title__contains="Revision") +>>> qs2 +[<RevisionableModel: First Revision (1, 1)>, <RevisionableModel: Second Revision (2, 1)>] + +Following queryset should return the most recent revision: +>>> qs & qs2 +[<RevisionableModel: Second Revision (2, 1)>] + +"""} diff --git a/tests/regressiontests/file_uploads/__init__.py b/tests/regressiontests/file_uploads/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/file_uploads/__init__.py diff --git a/tests/regressiontests/file_uploads/models.py b/tests/regressiontests/file_uploads/models.py new file mode 100644 index 0000000000..2d5607b2a7 --- /dev/null +++ b/tests/regressiontests/file_uploads/models.py @@ -0,0 +1,2 @@ +# This file unintentionally left blank. +# Oops.
\ No newline at end of file diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py new file mode 100644 index 0000000000..8992298470 --- /dev/null +++ b/tests/regressiontests/file_uploads/tests.py @@ -0,0 +1,158 @@ +import os +import sha +import tempfile +from django.test import TestCase, client +from django.utils import simplejson + +class FileUploadTests(TestCase): + def test_simple_upload(self): + post_data = { + 'name': 'Ringo', + 'file_field': open(__file__), + } + response = self.client.post('/file_uploads/upload/', post_data) + self.assertEqual(response.status_code, 200) + + def test_large_upload(self): + tdir = tempfile.gettempdir() + + file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir) + file1.write('a' * (2 ** 21)) + file1.seek(0) + + file2 = tempfile.NamedTemporaryFile(suffix=".file2", dir=tdir) + file2.write('a' * (10 * 2 ** 20)) + file2.seek(0) + + # This file contains chinese symbols for a name. + file3 = open(os.path.join(tdir, u'test_中文_Orl\u00e9ans.jpg'), 'w+b') + file3.write('b' * (2 ** 10)) + file3.seek(0) + + post_data = { + 'name': 'Ringo', + 'file_field1': open(file1.name), + 'file_field2': open(file2.name), + 'file_unicode': file3, + } + + for key in post_data.keys(): + try: + post_data[key + '_hash'] = sha.new(post_data[key].read()).hexdigest() + post_data[key].seek(0) + except AttributeError: + post_data[key + '_hash'] = sha.new(post_data[key]).hexdigest() + + response = self.client.post('/file_uploads/verify/', post_data) + + try: + os.unlink(file3.name) + except: + pass + + self.assertEqual(response.status_code, 200) + + def test_dangerous_file_names(self): + """Uploaded file names should be sanitized before ever reaching the view.""" + # This test simulates possible directory traversal attacks by a + # malicious uploader We have to do some monkeybusiness here to construct + # a malicious payload with an invalid file name (containing os.sep or + # os.pardir). This similar to what an attacker would need to do when + # trying such an attack. + scary_file_names = [ + "/tmp/hax0rd.txt", # Absolute path, *nix-style. + "C:\\Windows\\hax0rd.txt", # Absolute path, win-syle. + "C:/Windows/hax0rd.txt", # Absolute path, broken-style. + "\\tmp\\hax0rd.txt", # Absolute path, broken in a different way. + "/tmp\\hax0rd.txt", # Absolute path, broken by mixing. + "subdir/hax0rd.txt", # Descendant path, *nix-style. + "subdir\\hax0rd.txt", # Descendant path, win-style. + "sub/dir\\hax0rd.txt", # Descendant path, mixed. + "../../hax0rd.txt", # Relative path, *nix-style. + "..\\..\\hax0rd.txt", # Relative path, win-style. + "../..\\hax0rd.txt" # Relative path, mixed. + ] + + payload = [] + for i, name in enumerate(scary_file_names): + payload.extend([ + '--' + client.BOUNDARY, + 'Content-Disposition: form-data; name="file%s"; filename="%s"' % (i, name), + 'Content-Type: application/octet-stream', + '', + 'You got pwnd.' + ]) + payload.extend([ + '--' + client.BOUNDARY + '--', + '', + ]) + + payload = "\r\n".join(payload) + r = { + 'CONTENT_LENGTH': len(payload), + 'CONTENT_TYPE': client.MULTIPART_CONTENT, + 'PATH_INFO': "/file_uploads/echo/", + 'REQUEST_METHOD': 'POST', + 'wsgi.input': client.FakePayload(payload), + } + response = self.client.request(**r) + + # The filenames should have been sanitized by the time it got to the view. + recieved = simplejson.loads(response.content) + for i, name in enumerate(scary_file_names): + got = recieved["file%s" % i] + self.assertEqual(got, "hax0rd.txt") + + def test_filename_overflow(self): + """File names over 256 characters (dangerous on some platforms) get fixed up.""" + name = "%s.txt" % ("f"*500) + payload = "\r\n".join([ + '--' + client.BOUNDARY, + 'Content-Disposition: form-data; name="file"; filename="%s"' % name, + 'Content-Type: application/octet-stream', + '', + 'Oops.' + '--' + client.BOUNDARY + '--', + '', + ]) + r = { + 'CONTENT_LENGTH': len(payload), + 'CONTENT_TYPE': client.MULTIPART_CONTENT, + 'PATH_INFO': "/file_uploads/echo/", + 'REQUEST_METHOD': 'POST', + 'wsgi.input': client.FakePayload(payload), + } + got = simplejson.loads(self.client.request(**r).content) + self.assert_(len(got['file']) < 256, "Got a long file name (%s characters)." % len(got['file'])) + + def test_custom_upload_handler(self): + # A small file (under the 5M quota) + smallfile = tempfile.NamedTemporaryFile() + smallfile.write('a' * (2 ** 21)) + + # A big file (over the quota) + bigfile = tempfile.NamedTemporaryFile() + bigfile.write('a' * (10 * 2 ** 20)) + + # Small file posting should work. + response = self.client.post('/file_uploads/quota/', {'f': open(smallfile.name)}) + got = simplejson.loads(response.content) + self.assert_('f' in got) + + # Large files don't go through. + response = self.client.post("/file_uploads/quota/", {'f': open(bigfile.name)}) + got = simplejson.loads(response.content) + self.assert_('f' not in got) + + def test_broken_custom_upload_handler(self): + f = tempfile.NamedTemporaryFile() + f.write('a' * (2 ** 21)) + + # AttributeError: You cannot alter upload handlers after the upload has been processed. + self.assertRaises( + AttributeError, + self.client.post, + '/file_uploads/quota/broken/', + {'f': open(f.name)} + ) +
\ No newline at end of file diff --git a/tests/regressiontests/file_uploads/uploadhandler.py b/tests/regressiontests/file_uploads/uploadhandler.py new file mode 100644 index 0000000000..54f82f626c --- /dev/null +++ b/tests/regressiontests/file_uploads/uploadhandler.py @@ -0,0 +1,26 @@ +""" +Upload handlers to test the upload API. +""" + +from django.core.files.uploadhandler import FileUploadHandler, StopUpload + +class QuotaUploadHandler(FileUploadHandler): + """ + This test upload handler terminates the connection if more than a quota + (5MB) is uploaded. + """ + + QUOTA = 5 * 2**20 # 5 MB + + def __init__(self, request=None): + super(QuotaUploadHandler, self).__init__(request) + self.total_upload = 0 + + def receive_data_chunk(self, raw_data, start): + self.total_upload += len(raw_data) + if self.total_upload >= self.QUOTA: + raise StopUpload(connection_reset=True) + return raw_data + + def file_complete(self, file_size): + return None
\ No newline at end of file diff --git a/tests/regressiontests/file_uploads/urls.py b/tests/regressiontests/file_uploads/urls.py new file mode 100644 index 0000000000..529bee312d --- /dev/null +++ b/tests/regressiontests/file_uploads/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import * +import views + +urlpatterns = patterns('', + (r'^upload/$', views.file_upload_view), + (r'^verify/$', views.file_upload_view_verify), + (r'^echo/$', views.file_upload_echo), + (r'^quota/$', views.file_upload_quota), + (r'^quota/broken/$', views.file_upload_quota_broken), +) diff --git a/tests/regressiontests/file_uploads/views.py b/tests/regressiontests/file_uploads/views.py new file mode 100644 index 0000000000..833cf90531 --- /dev/null +++ b/tests/regressiontests/file_uploads/views.py @@ -0,0 +1,70 @@ +import os +import sha +from django.core.files.uploadedfile import UploadedFile +from django.http import HttpResponse, HttpResponseServerError +from django.utils import simplejson +from uploadhandler import QuotaUploadHandler + +def file_upload_view(request): + """ + Check that a file upload can be updated into the POST dictionary without + going pear-shaped. + """ + form_data = request.POST.copy() + form_data.update(request.FILES) + if isinstance(form_data.get('file_field'), UploadedFile) and isinstance(form_data['name'], unicode): + # If a file is posted, the dummy client should only post the file name, + # not the full path. + if os.path.dirname(form_data['file_field'].file_name) != '': + return HttpResponseServerError() + return HttpResponse('') + else: + return HttpResponseServerError() + +def file_upload_view_verify(request): + """ + Use the sha digest hash to verify the uploaded contents. + """ + form_data = request.POST.copy() + form_data.update(request.FILES) + + # Check to see if unicode names worked out. + if not request.FILES['file_unicode'].file_name.endswith(u'test_\u4e2d\u6587_Orl\xe9ans.jpg'): + return HttpResponseServerError() + + for key, value in form_data.items(): + if key.endswith('_hash'): + continue + if key + '_hash' not in form_data: + continue + submitted_hash = form_data[key + '_hash'] + if isinstance(value, UploadedFile): + new_hash = sha.new(value.read()).hexdigest() + else: + new_hash = sha.new(value).hexdigest() + if new_hash != submitted_hash: + return HttpResponseServerError() + + return HttpResponse('') + +def file_upload_echo(request): + """ + Simple view to echo back info about uploaded files for tests. + """ + r = dict([(k, f.file_name) for k, f in request.FILES.items()]) + return HttpResponse(simplejson.dumps(r)) + +def file_upload_quota(request): + """ + Dynamically add in an upload handler. + """ + request.upload_handlers.insert(0, QuotaUploadHandler()) + return file_upload_echo(request) + +def file_upload_quota_broken(request): + """ + You can't change handlers after reading FILES; this view shouldn't work. + """ + response = file_upload_echo(request) + request.upload_handlers.insert(0, QuotaUploadHandler()) + return response
\ No newline at end of file diff --git a/tests/regressiontests/fixtures_regress/fixtures/big-fixture.json b/tests/regressiontests/fixtures_regress/fixtures/big-fixture.json new file mode 100644 index 0000000000..e655fbbf3f --- /dev/null +++ b/tests/regressiontests/fixtures_regress/fixtures/big-fixture.json @@ -0,0 +1,83 @@ +[ + { + "pk": 6, + "model": "fixtures_regress.channel", + "fields": { + "name": "Business" + } + }, + + { + "pk": 1, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 1", + "channels": [6] + } + }, + { + "pk": 2, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 2", + "channels": [6] + } + }, + { + "pk": 3, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 3", + "channels": [6] + } + }, + { + "pk": 4, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 4", + "channels": [6] + } + }, + + { + "pk": 5, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 5", + "channels": [6] + } + }, + { + "pk": 6, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 6", + "channels": [6] + } + }, + { + "pk": 7, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 7", + "channels": [6] + } + }, + { + "pk": 8, + "model": "fixtures_regress.article", + "fields": { + "title": "Article Title 8", + "channels": [6] + } + }, + { + "pk": 9, + "model": "fixtures_regress.article", + "fields": { + "title": "Yet Another Article", + "channels": [6] + } + } +]
\ No newline at end of file diff --git a/tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json b/tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json new file mode 100644 index 0000000000..00c482b3dd --- /dev/null +++ b/tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json @@ -0,0 +1,4 @@ +[ + {"pk": 1, "model": "fixtures_regress.parent", "fields": {"name": "fred"}}, + {"pk": 1, "model": "fixtures_regress.child", "fields": {"data": "apple"}} +] diff --git a/tests/regressiontests/fixtures_regress/models.py b/tests/regressiontests/fixtures_regress/models.py index 59fc167d50..2c048dc0b8 100644 --- a/tests/regressiontests/fixtures_regress/models.py +++ b/tests/regressiontests/fixtures_regress/models.py @@ -20,7 +20,7 @@ class Plant(models.Model): class Stuff(models.Model): name = models.CharField(max_length=20, null=True) owner = models.ForeignKey(User, null=True) - + def __unicode__(self): # Oracle doesn't distinguish between None and the empty string. # This hack makes the test case pass using Oracle. @@ -38,13 +38,29 @@ class Absolute(models.Model): super(Absolute, self).__init__(*args, **kwargs) Absolute.load_count += 1 +class Parent(models.Model): + name = models.CharField(max_length=10) + +class Child(Parent): + data = models.CharField(max_length=10) + +# Models to regresison check #7572 +class Channel(models.Model): + name = models.CharField(max_length=255) + +class Article(models.Model): + title = models.CharField(max_length=255) + channels = models.ManyToManyField(Channel) + + class Meta: + ordering = ('id',) __test__ = {'API_TESTS':""" >>> from django.core import management # Load a fixture that uses PK=1 >>> management.call_command('loaddata', 'sequence', verbosity=0) - + # Create a new animal. Without a sequence reset, this new object # will take a PK of 1 (on Postgres), and the save will fail. # This is a regression test for ticket #3790. @@ -61,9 +77,9 @@ __test__ = {'API_TESTS':""" [<Stuff: None is owned by None>] ############################################### -# Regression test for ticket #6436 -- +# Regression test for ticket #6436 -- # os.path.join will throw away the initial parts of a path if it encounters -# an absolute path. This means that if a fixture is specified as an absolute path, +# an absolute path. This means that if a fixture is specified as an absolute path, # we need to make sure we don't discover the absolute path in every fixture directory. >>> load_absolute_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'absolute.json') @@ -94,4 +110,28 @@ No fixture data found for 'bad_fixture2'. (File format may be invalid.) >>> sys.stderr = savestderr +############################################### +# Test for ticket #7565 -- PostgreSQL sequence resetting checks shouldn't +# ascend to parent models when inheritance is used (since they are treated +# individually). + +>>> management.call_command('loaddata', 'model-inheritance.json', verbosity=0) + +############################################### +# Test for ticket #7572 -- MySQL has a problem if the same connection is +# used to create tables, load data, and then query over that data. +# To compensate, we close the connection after running loaddata. +# This ensures that a new connection is opened when test queries are issued. + +>>> management.call_command('loaddata', 'big-fixture.json', verbosity=0) + +>>> articles = Article.objects.exclude(id=9) +>>> articles.values_list('id', flat=True) +[1, 2, 3, 4, 5, 6, 7, 8] + +# Just for good measure, run the same query again. Under the influence of +# ticket #7572, this will give a different result to the previous call. +>>> articles.values_list('id', flat=True) +[1, 2, 3, 4, 5, 6, 7, 8] + """} diff --git a/tests/regressiontests/forms/error_messages.py b/tests/regressiontests/forms/error_messages.py index 9f972f5b90..580326f894 100644 --- a/tests/regressiontests/forms/error_messages.py +++ b/tests/regressiontests/forms/error_messages.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" >>> from django.newforms import * +>>> from django.core.files.uploadedfile import SimpleUploadedFile # CharField ################################################################### @@ -214,11 +215,11 @@ ValidationError: [u'REQUIRED'] Traceback (most recent call last): ... ValidationError: [u'INVALID'] ->>> f.clean({}) +>>> f.clean(SimpleUploadedFile('name', None)) Traceback (most recent call last): ... -ValidationError: [u'MISSING'] ->>> f.clean({'filename': 'name', 'content':''}) +ValidationError: [u'EMPTY FILE'] +>>> f.clean(SimpleUploadedFile('name', '')) Traceback (most recent call last): ... ValidationError: [u'EMPTY FILE'] @@ -237,7 +238,7 @@ ValidationError: [u'REQUIRED'] Traceback (most recent call last): ... ValidationError: [u'INVALID'] ->>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') +>>> f.clean('http://www.broken.djangoproject.com') Traceback (most recent call last): ... ValidationError: [u'INVALID LINK'] diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py index f3b6a96a1e..4725c3ecf3 100644 --- a/tests/regressiontests/forms/fields.py +++ b/tests/regressiontests/forms/fields.py @@ -2,6 +2,7 @@ tests = r""" >>> from django.newforms import * >>> from django.newforms.widgets import RadioFieldRenderer +>>> from django.core.files.uploadedfile import SimpleUploadedFile >>> import datetime >>> import time >>> import re @@ -770,17 +771,17 @@ ValidationError: [u'This field is required.'] >>> f.clean(None, 'files/test2.pdf') 'files/test2.pdf' ->>> f.clean({}) +>>> f.clean(SimpleUploadedFile('', '')) Traceback (most recent call last): ... -ValidationError: [u'No file was submitted.'] +ValidationError: [u'No file was submitted. Check the encoding type on the form.'] ->>> f.clean({}, '') +>>> f.clean(SimpleUploadedFile('', ''), '') Traceback (most recent call last): ... -ValidationError: [u'No file was submitted.'] +ValidationError: [u'No file was submitted. Check the encoding type on the form.'] ->>> f.clean({}, 'files/test3.pdf') +>>> f.clean(None, 'files/test3.pdf') 'files/test3.pdf' >>> f.clean('some content that is not a file') @@ -788,20 +789,20 @@ Traceback (most recent call last): ... ValidationError: [u'No file was submitted. Check the encoding type on the form.'] ->>> f.clean({'filename': 'name', 'content': None}) +>>> f.clean(SimpleUploadedFile('name', None)) Traceback (most recent call last): ... ValidationError: [u'The submitted file is empty.'] ->>> f.clean({'filename': 'name', 'content': ''}) +>>> f.clean(SimpleUploadedFile('name', '')) Traceback (most recent call last): ... ValidationError: [u'The submitted file is empty.'] ->>> type(f.clean({'filename': 'name', 'content': 'Some File Content'})) +>>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'))) <class 'django.newforms.fields.UploadedFile'> ->>> type(f.clean({'filename': 'name', 'content': 'Some File Content'}, 'files/test4.pdf')) +>>> type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf')) <class 'django.newforms.fields.UploadedFile'> # URLField ################################################################## @@ -887,7 +888,7 @@ u'http://www.google.com' Traceback (most recent call last): ... ValidationError: [u'Enter a valid URL.'] ->>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') # bad domain +>>> f.clean('http://www.broken.djangoproject.com') # bad domain Traceback (most recent call last): ... ValidationError: [u'This URL appears to be a broken link.'] @@ -937,18 +938,24 @@ ValidationError: [u'This field is required.'] >>> f.clean(True) True >>> f.clean(False) -False +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] >>> f.clean(1) True >>> f.clean(0) -False +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] >>> f.clean('Django rocks') True >>> f.clean('True') True >>> f.clean('False') -False +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] >>> f = BooleanField(required=False) >>> f.clean('') diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index 7fc206de4c..041fa4054c 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" >>> from django.newforms import * +>>> from django.core.files.uploadedfile import SimpleUploadedFile >>> import datetime >>> import time >>> import re @@ -1465,7 +1466,7 @@ not request.POST. >>> print f <tr><th>File1:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="file" name="file1" /></td></tr> ->>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':''}}, auto_id=False) +>>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', '')}, auto_id=False) >>> print f <tr><th>File1:</th><td><ul class="errorlist"><li>The submitted file is empty.</li></ul><input type="file" name="file1" /></td></tr> @@ -1473,7 +1474,7 @@ not request.POST. >>> print f <tr><th>File1:</th><td><ul class="errorlist"><li>No file was submitted. Check the encoding type on the form.</li></ul><input type="file" name="file1" /></td></tr> ->>> f = FileForm(data={}, files={'file1': {'filename': 'name', 'content':'some content'}}, auto_id=False) +>>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', 'some content')}, auto_id=False) >>> print f <tr><th>File1:</th><td><input type="file" name="file1" /></td></tr> >>> f.is_valid() diff --git a/tests/regressiontests/many_to_one_regress/models.py b/tests/regressiontests/many_to_one_regress/models.py index 4e49df1555..429bdd7558 100644 --- a/tests/regressiontests/many_to_one_regress/models.py +++ b/tests/regressiontests/many_to_one_regress/models.py @@ -28,6 +28,24 @@ class Child(models.Model): parent = models.ForeignKey(Parent) +# Multiple paths to the same model (#7110, #7125) +class Category(models.Model): + name = models.CharField(max_length=20) + + def __unicode__(self): + return self.name + +class Record(models.Model): + category = models.ForeignKey(Category) + +class Relation(models.Model): + left = models.ForeignKey(Record, related_name='left_set') + right = models.ForeignKey(Record, related_name='right_set') + + def __unicode__(self): + return u"%s - %s" % (self.left.category.name, self.right.category.name) + + __test__ = {'API_TESTS':""" >>> Third.objects.create(id='3', name='An example') <Third: Third object> @@ -73,4 +91,26 @@ Traceback (most recent call last): ... ValueError: Cannot assign "<First: First object>": "Child.parent" must be a "Parent" instance. +# Test of multiple ForeignKeys to the same model (bug #7125) + +>>> c1 = Category.objects.create(name='First') +>>> c2 = Category.objects.create(name='Second') +>>> c3 = Category.objects.create(name='Third') +>>> r1 = Record.objects.create(category=c1) +>>> r2 = Record.objects.create(category=c1) +>>> r3 = Record.objects.create(category=c2) +>>> r4 = Record.objects.create(category=c2) +>>> r5 = Record.objects.create(category=c3) +>>> r = Relation.objects.create(left=r1, right=r2) +>>> r = Relation.objects.create(left=r3, right=r4) +>>> r = Relation.objects.create(left=r1, right=r3) +>>> r = Relation.objects.create(left=r5, right=r2) +>>> r = Relation.objects.create(left=r3, right=r2) + +>>> Relation.objects.filter(left__category__name__in=['First'], right__category__name__in=['Second']) +[<Relation: First - Second>] + +>>> Category.objects.filter(record__left_set__right__category__name='Second').order_by('name') +[<Category: First>, <Category: Second>] + """} diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py index e279a0669f..c2ba9ee008 100644 --- a/tests/regressiontests/model_fields/tests.py +++ b/tests/regressiontests/model_fields/tests.py @@ -15,4 +15,21 @@ Decimal("3.14") Traceback (most recent call last): ... ValidationError: [u'This value must be a decimal number.'] + +>>> f = DecimalField(max_digits=5, decimal_places=1) +>>> x = f.to_python(2) +>>> y = f.to_python('2.6') + +>>> f.get_db_prep_save(x) +u'2.0' +>>> f.get_db_prep_save(y) +u'2.6' +>>> f.get_db_prep_save(None) +>>> f.get_db_prep_lookup('exact', x) +[u'2.0'] +>>> f.get_db_prep_lookup('exact', y) +[u'2.6'] +>>> f.get_db_prep_lookup('exact', None) +[None] + """ diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py index 33e2e0e4f6..24d6186150 100644 --- a/tests/regressiontests/model_inheritance_regress/models.py +++ b/tests/regressiontests/model_inheritance_regress/models.py @@ -131,4 +131,26 @@ __test__ = {'API_TESTS':""" >>> Child.objects.dates('created', 'month') [datetime.datetime(2008, 6, 1, 0, 0)] +# Regression test for #7276: calling delete() on a model with multi-table +# inheritance should delete the associated rows from any ancestor tables, as +# well as any descendent objects. + +>>> ident = ItalianRestaurant.objects.all()[0].id +>>> Place.objects.get(pk=ident) +<Place: Guido's All New House of Pasta the place> +>>> xx = Restaurant.objects.create(name='a', address='xx', serves_hot_dogs=True, serves_pizza=False) + +# This should delete both Restuarants, plus the related places, plus the ItalianRestaurant. +>>> Restaurant.objects.all().delete() + +>>> Place.objects.get(pk=ident) +Traceback (most recent call last): +... +DoesNotExist: Place matching query does not exist. + +>>> ItalianRestaurant.objects.get(pk=ident) +Traceback (most recent call last): +... +DoesNotExist: ItalianRestaurant matching query does not exist. + """} diff --git a/tests/regressiontests/model_inheritance_select_related/__init__.py b/tests/regressiontests/model_inheritance_select_related/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/model_inheritance_select_related/__init__.py diff --git a/tests/regressiontests/model_inheritance_select_related/models.py b/tests/regressiontests/model_inheritance_select_related/models.py new file mode 100644 index 0000000000..a97ecaadea --- /dev/null +++ b/tests/regressiontests/model_inheritance_select_related/models.py @@ -0,0 +1,47 @@ +""" +Regression tests for the interaction between model inheritance and +select_related(). +""" + +from django.db import models + +class Place(models.Model): + name = models.CharField(max_length=50) + + class Meta: + ordering = ('name',) + + def __unicode__(self): + return u"%s the place" % self.name + +class Restaurant(Place): + serves_sushi = models.BooleanField() + serves_steak = models.BooleanField() + + def __unicode__(self): + return u"%s the restaurant" % self.name + +class Person(models.Model): + name = models.CharField(max_length=50) + favorite_restaurant = models.ForeignKey(Restaurant) + + def __unicode__(self): + return self.name + +__test__ = {'API_TESTS':""" +Regression test for #7246 + +>>> r1 = Restaurant.objects.create(name="Nobu", serves_sushi=True, serves_steak=False) +>>> r2 = Restaurant.objects.create(name="Craft", serves_sushi=False, serves_steak=True) +>>> p1 = Person.objects.create(name="John", favorite_restaurant=r1) +>>> p2 = Person.objects.create(name="Jane", favorite_restaurant=r2) + +>>> Person.objects.order_by('name').select_related() +[<Person: Jane>, <Person: John>] + +>>> jane = Person.objects.order_by('name').select_related('favorite_restaurant')[0] +>>> jane.favorite_restaurant.name +u'Craft' + +"""} + diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index c02ad73998..566411e513 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -3,13 +3,15 @@ Various complex queries that have been problematic in the past. """ import datetime +import pickle from django.db import models from django.db.models.query import Q class Tag(models.Model): name = models.CharField(max_length=10) - parent = models.ForeignKey('self', blank=True, null=True) + parent = models.ForeignKey('self', blank=True, null=True, + related_name='children') def __unicode__(self): return self.name @@ -24,6 +26,14 @@ class Note(models.Model): def __unicode__(self): return self.note +class Annotation(models.Model): + name = models.CharField(max_length=10) + tag = models.ForeignKey(Tag) + notes = models.ManyToManyField(Note) + + def __unicode__(self): + return self.name + class ExtraInfo(models.Model): info = models.CharField(max_length=100) note = models.ForeignKey(Note) @@ -162,85 +172,67 @@ class Child(models.Model): person = models.OneToOneField(Member, primary_key=True) parent = models.ForeignKey(Member, related_name="children") +# Custom primary keys interfered with ordering in the past. +class CustomPk(models.Model): + name = models.CharField(max_length=10, primary_key=True) + extra = models.CharField(max_length=10) + + class Meta: + ordering = ['name', 'extra'] + +class Related(models.Model): + custom = models.ForeignKey(CustomPk) + __test__ = {'API_TESTS':""" ->>> t1 = Tag(name='t1') ->>> t1.save() ->>> t2 = Tag(name='t2', parent=t1) ->>> t2.save() ->>> t3 = Tag(name='t3', parent=t1) ->>> t3.save() ->>> t4 = Tag(name='t4', parent=t3) ->>> t4.save() ->>> t5 = Tag(name='t5', parent=t3) ->>> t5.save() +>>> t1 = Tag.objects.create(name='t1') +>>> t2 = Tag.objects.create(name='t2', parent=t1) +>>> t3 = Tag.objects.create(name='t3', parent=t1) +>>> t4 = Tag.objects.create(name='t4', parent=t3) +>>> t5 = Tag.objects.create(name='t5', parent=t3) ->>> n1 = Note(note='n1', misc='foo') ->>> n1.save() ->>> n2 = Note(note='n2', misc='bar') ->>> n2.save() ->>> n3 = Note(note='n3', misc='foo') ->>> n3.save() +>>> n1 = Note.objects.create(note='n1', misc='foo') +>>> n2 = Note.objects.create(note='n2', misc='bar') +>>> n3 = Note.objects.create(note='n3', misc='foo') Create these out of order so that sorting by 'id' will be different to sorting by 'info'. Helps detect some problems later. ->>> e2 = ExtraInfo(info='e2', note=n2) ->>> e2.save() ->>> e1 = ExtraInfo(info='e1', note=n1) ->>> e1.save() +>>> e2 = ExtraInfo.objects.create(info='e2', note=n2) +>>> e1 = ExtraInfo.objects.create(info='e1', note=n1) ->>> a1 = Author(name='a1', num=1001, extra=e1) ->>> a1.save() ->>> a2 = Author(name='a2', num=2002, extra=e1) ->>> a2.save() ->>> a3 = Author(name='a3', num=3003, extra=e2) ->>> a3.save() ->>> a4 = Author(name='a4', num=4004, extra=e2) ->>> a4.save() +>>> a1 = Author.objects.create(name='a1', num=1001, extra=e1) +>>> a2 = Author.objects.create(name='a2', num=2002, extra=e1) +>>> a3 = Author.objects.create(name='a3', num=3003, extra=e2) +>>> a4 = Author.objects.create(name='a4', num=4004, extra=e2) >>> time1 = datetime.datetime(2007, 12, 19, 22, 25, 0) >>> 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, modified=time1, creator=a1, note=n3) ->>> i1.save() +>>> i1 = Item.objects.create(name='one', created=time1, modified=time1, creator=a1, note=n3) >>> i1.tags = [t1, t2] ->>> i2 = Item(name='two', created=time2, creator=a2, note=n2) ->>> i2.save() +>>> i2 = Item.objects.create(name='two', created=time2, creator=a2, note=n2) >>> i2.tags = [t1, t3] ->>> i3 = Item(name='three', created=time3, creator=a2, note=n3) ->>> i3.save() ->>> i4 = Item(name='four', created=time4, creator=a4, note=n3) ->>> i4.save() +>>> i3 = Item.objects.create(name='three', created=time3, creator=a2, note=n3) +>>> i4 = Item.objects.create(name='four', created=time4, creator=a4, note=n3) >>> i4.tags = [t4] ->>> r1 = Report(name='r1', creator=a1) ->>> r1.save() ->>> r2 = Report(name='r2', creator=a3) ->>> r2.save() ->>> r3 = Report(name='r3') ->>> r3.save() +>>> r1 = Report.objects.create(name='r1', creator=a1) +>>> r2 = Report.objects.create(name='r2', creator=a3) +>>> r3 = Report.objects.create(name='r3') Ordering by 'rank' gives us rank2, rank1, rank3. Ordering by the Meta.ordering will be rank3, rank2, rank1. ->>> rank1 = Ranking(rank=2, author=a2) ->>> rank1.save() ->>> rank2 = Ranking(rank=1, author=a3) ->>> rank2.save() ->>> rank3 = Ranking(rank=3, author=a1) ->>> rank3.save() +>>> rank1 = Ranking.objects.create(rank=2, author=a2) +>>> rank2 = Ranking.objects.create(rank=1, author=a3) +>>> rank3 = Ranking.objects.create(rank=3, author=a1) ->>> c1 = Cover(title="first", item=i4) ->>> c1.save() ->>> c2 = Cover(title="second", item=i2) ->>> c2.save() +>>> c1 = Cover.objects.create(title="first", item=i4) +>>> c2 = Cover.objects.create(title="second", item=i2) ->>> n1 = Number(num=4) ->>> n1.save() ->>> n2 = Number(num=8) ->>> n2.save() ->>> n3 = Number(num=12) ->>> n3.save() +>>> num1 = Number.objects.create(num=4) +>>> num2 = Number.objects.create(num=8) +>>> num3 = Number.objects.create(num=12) Bug #1050 >>> Item.objects.filter(tags__isnull=True) @@ -346,6 +338,10 @@ Bug #1878, #2939 4 >>> xx.delete() +Bug #7323 +>>> Item.objects.values('creator', 'name').count() +4 + Bug #2253 >>> q1 = Item.objects.order_by('name') >>> q2 = Item.objects.filter(id=i1.id) @@ -387,6 +383,10 @@ Bug #4510 >>> Author.objects.filter(report__name='r1') [<Author: a1>] +Bug #7378 +>>> a1.report_set.all() +[<Report: r1>] + Bug #5324, #6704 >>> Item.objects.filter(tags__name='t4') [<Item: four>] @@ -791,5 +791,19 @@ Empty querysets can be merged with others. >>> Note.objects.all() & Note.objects.none() [] +Bug #7204, #7506 -- make sure querysets with related fields can be pickled. If +this doesn't crash, it's a Good Thing. +>>> out = pickle.dumps(Item.objects.all()) + +Bug #7277 +>>> ann1 = Annotation.objects.create(name='a1', tag=t1) +>>> ann1.notes.add(n1) +>>> n1.annotation_set.filter(Q(tag=t5) | Q(tag__children=t5) | Q(tag__children__children=t5)) +[<Annotation: a1>] + +Bug #7371 +>>> Related.objects.order_by('custom') +[] + """} diff --git a/tests/regressiontests/select_related_regress/__init__.py b/tests/regressiontests/select_related_regress/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/select_related_regress/__init__.py diff --git a/tests/regressiontests/select_related_regress/models.py b/tests/regressiontests/select_related_regress/models.py new file mode 100644 index 0000000000..05b851829d --- /dev/null +++ b/tests/regressiontests/select_related_regress/models.py @@ -0,0 +1,60 @@ +from django.db import models + +class Building(models.Model): + name = models.CharField(max_length=10) + + def __unicode__(self): + return u"Building: %s" % self.name + +class Device(models.Model): + building = models.ForeignKey('Building') + name = models.CharField(max_length=10) + + def __unicode__(self): + return u"device '%s' in building %s" % (self.name, self.building) + +class Port(models.Model): + device = models.ForeignKey('Device') + port_number = models.CharField(max_length=10) + + def __unicode__(self): + return u"%s/%s" % (self.device.name, self.port_number) + +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): + return u"%s to %s" % (self.start, self.end) + +__test__ = {'API_TESTS': """ +Regression test for bug #7110. When using select_related(), we must query the +Device and Building tables using two different aliases (each) in order to +differentiate the start and end Connection fields. The net result is that both +the "connections = ..." queries here should give the same results. + +>>> b=Building.objects.create(name='101') +>>> dev1=Device.objects.create(name="router", building=b) +>>> dev2=Device.objects.create(name="switch", building=b) +>>> dev3=Device.objects.create(name="server", building=b) +>>> port1=Port.objects.create(port_number='4',device=dev1) +>>> port2=Port.objects.create(port_number='7',device=dev2) +>>> port3=Port.objects.create(port_number='1',device=dev3) +>>> c1=Connection.objects.create(start=port1, end=port2) +>>> c2=Connection.objects.create(start=port2, end=port3) + +>>> connections=Connection.objects.filter(start__device__building=b, end__device__building=b).order_by('id') +>>> [(c.id, unicode(c.start), unicode(c.end)) for c in connections] +[(1, u'router/4', u'switch/7'), (2, u'switch/7', u'server/1')] + +>>> connections=Connection.objects.filter(start__device__building=b, end__device__building=b).select_related().order_by('id') +>>> [(c.id, unicode(c.start), unicode(c.end)) for c in connections] +[(1, u'router/4', u'switch/7'), (2, u'switch/7', u'server/1')] + +# This final query should only join seven tables (port, device and building +# twice each, plus connection once). +>>> connections.query.count_active_tables() +7 + +"""} diff --git a/tests/regressiontests/string_lookup/models.py b/tests/regressiontests/string_lookup/models.py index 1bdb2d4452..39e7955592 100644 --- a/tests/regressiontests/string_lookup/models.py +++ b/tests/regressiontests/string_lookup/models.py @@ -97,6 +97,12 @@ __test__ = {'API_TESTS': ur""" >>> Article.objects.get(text__exact='The quick brown fox jumps over the lazy dog.') <Article: Article Test> +# Regression tests for #2170: test case sensitiveness +>>> Article.objects.filter(text__exact='tHe qUick bRown fOx jUmps over tHe lazy dog.') +[] +>>> Article.objects.filter(text__iexact='tHe qUick bRown fOx jUmps over tHe lazy dog.') +[<Article: Article Test>] + >>> Article.objects.get(text__contains='quick brown fox') <Article: Article Test> diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py index a204ec3e72..1eb55e312e 100644 --- a/tests/regressiontests/test_client_regress/models.py +++ b/tests/regressiontests/test_client_regress/models.py @@ -6,6 +6,7 @@ from django.test import Client, TestCase from django.core.urlresolvers import reverse from django.core.exceptions import SuspiciousOperation import os +import sha class AssertContainsTests(TestCase): def test_contains(self): @@ -240,16 +241,6 @@ class AssertFormErrorTests(TestCase): except AssertionError, e: self.assertEqual(str(e), "The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )") -class FileUploadTests(TestCase): - def test_simple_upload(self): - fd = open(os.path.join(os.path.dirname(__file__), "views.py")) - post_data = { - 'name': 'Ringo', - 'file_field': fd, - } - response = self.client.post('/test_client_regress/file_upload/', post_data) - self.assertEqual(response.status_code, 200) - class LoginTests(TestCase): fixtures = ['testdata'] @@ -269,7 +260,6 @@ class LoginTests(TestCase): # default client. self.assertRedirects(response, "http://testserver/test_client_regress/get_view/") - class URLEscapingTests(TestCase): def test_simple_argument_get(self): "Get a view that has a simple string argument" @@ -318,3 +308,22 @@ class ExceptionTests(TestCase): self.client.get("/test_client_regress/staff_only/") except SuspiciousOperation: self.fail("Staff should be able to visit this page") + +# We need two different tests to check URLconf subsitution - one to check +# it was changed, and another one (without self.urls) to check it was reverted on +# teardown. This pair of tests relies upon the alphabetical ordering of test execution. +class UrlconfSubstitutionTests(TestCase): + urls = 'regressiontests.test_client_regress.urls' + + def test_urlconf_was_changed(self): + "TestCase can enforce a custom URLConf on a per-test basis" + url = reverse('arg_view', args=['somename']) + self.assertEquals(url, '/arg_view/somename/') + +# This test needs to run *after* UrlconfSubstitutionTests; the zz prefix in the +# name is to ensure alphabetical ordering. +class zzUrlconfSubstitutionTests(TestCase): + def test_urlconf_was_reverted(self): + "URLconf is reverted to original value after modification in a TestCase" + url = reverse('arg_view', args=['somename']) + self.assertEquals(url, '/test_client_regress/arg_view/somename/') diff --git a/tests/regressiontests/test_client_regress/urls.py b/tests/regressiontests/test_client_regress/urls.py index dc26d1260a..12f6afacf3 100644 --- a/tests/regressiontests/test_client_regress/urls.py +++ b/tests/regressiontests/test_client_regress/urls.py @@ -3,7 +3,6 @@ import views urlpatterns = patterns('', (r'^no_template_view/$', views.no_template_view), - (r'^file_upload/$', views.file_upload_view), (r'^staff_only/$', views.staff_only_view), (r'^get_view/$', views.get_view), url(r'^arg_view/(?P<name>.+)/$', views.view_with_argument, name='arg_view'), diff --git a/tests/regressiontests/test_client_regress/views.py b/tests/regressiontests/test_client_regress/views.py index 9632c17284..d703c82124 100644 --- a/tests/regressiontests/test_client_regress/views.py +++ b/tests/regressiontests/test_client_regress/views.py @@ -1,36 +1,18 @@ -import os - from django.contrib.auth.decorators import login_required -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseServerError +from django.http import HttpResponse, HttpResponseRedirect from django.core.exceptions import SuspiciousOperation def no_template_view(request): "A simple view that expects a GET request, and returns a rendered template" return HttpResponse("No template used. Sample content: twice once twice. Content ends.") -def file_upload_view(request): - """ - Check that a file upload can be updated into the POST dictionary without - going pear-shaped. - """ - form_data = request.POST.copy() - form_data.update(request.FILES) - if isinstance(form_data['file_field'], dict) and isinstance(form_data['name'], unicode): - # If a file is posted, the dummy client should only post the file name, - # not the full path. - if os.path.dirname(form_data['file_field']['filename']) != '': - return HttpResponseServerError() - return HttpResponse('') - else: - return HttpResponseServerError() - def staff_only_view(request): "A view that can only be visited by staff. Non staff members get an exception" if request.user.is_staff: return HttpResponse('') else: raise SuspiciousOperation() - + def get_view(request): "A simple login protected view" return HttpResponse("Hello world") @@ -51,4 +33,4 @@ def view_with_argument(request, name): def login_protected_redirect_view(request): "A view that redirects all requests to the GET view" return HttpResponseRedirect('/test_client_regress/get_view/') -login_protected_redirect_view = login_required(login_protected_redirect_view)
\ No newline at end of file +login_protected_redirect_view = login_required(login_protected_redirect_view) diff --git a/tests/urls.py b/tests/urls.py index dbdf9a8064..cea453ef37 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -5,6 +5,9 @@ urlpatterns = patterns('', (r'^test_client/', include('modeltests.test_client.urls')), (r'^test_client_regress/', include('regressiontests.test_client_regress.urls')), + # File upload test views + (r'^file_uploads/', include('regressiontests.file_uploads.urls')), + # Always provide the auth system login and logout views (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}), (r'^accounts/logout/$', 'django.contrib.auth.views.logout'), |
