diff --git a/AUTHORS b/AUTHORS index c7aba07430..767dd2c55b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,7 +1,6 @@ Django was originally created in late 2003 at World Online, the Web division of the Lawrence Journal-World newspaper in Lawrence, Kansas. - The PRIMARY AUTHORS are (and/or have been): Adrian Holovaty , who originally created Django with @@ -45,6 +44,7 @@ answer newbie questions, and generally made Django that much better: adurdin@gmail.com akaihola Andreas + andy@jadedplanet.net ant9000@netwise.it David Ascher Arthur @@ -53,10 +53,12 @@ answer newbie questions, and generally made Django that much better: Shannon -jj Behrens Esdras Beleza James Bennett + Ben Paul Bissex Simon Blanchard Andrew Brehaut - andy@jadedplanet.net + brut.alll@gmail.com + Jonathan Buchanan Antonio Cavedoni C8E Chris Chamberlin @@ -66,17 +68,19 @@ answer newbie questions, and generally made Django that much better: crankycoder@gmail.com Matt Croydon dackze+django@gmail.com + Dirk Datzert Jonathan Daugherty (cygnus) + dave@thebarproject.com Jason Davies (Esaj) Alex Dedul deric@monowerks.com dne@mayonnaise.net Maximillian Dornseif - dummy@habmalnefrage.de Jeremy Dunck Andy Dustman Clint Ecker Enrico + Marc Fargas favo@exoweb.net Eric Floehr gandalf@owca.info @@ -84,16 +88,18 @@ answer newbie questions, and generally made Django that much better: martin.glueck@gmail.com Simon Greenhill Espen Grindhaug + Brian Harring Brant Harris Hawkeye - heckj@mac.com + Joe Heck Joel Heenan hipertracker@gmail.com Ian Holsman Kieran Holland Robert Rock Howard Jason Huggins - Baurzhan Ismagulov + Tom Insam + Baurzhan Ismagulov jcrasta@gmail.com Michael Josephson jpellerin@gmail.com @@ -112,6 +118,7 @@ answer newbie questions, and generally made Django that much better: Jeong-Min Lee Christopher Lenz lerouxb@gmail.com + Waylan Limberg limodou mattmcc Martin Maney @@ -162,8 +169,8 @@ answer newbie questions, and generally made Django that much better: Aaron Swartz Tyson Tate Tom Tobin - Tom Insam Joe Topjian + torne-django@wolfpuppy.org.uk Karen Tracey Makoto Tsuyuki Amit Upadhyay diff --git a/MANIFEST.in b/MANIFEST.in index b5fbb3cb90..11b60bcc14 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,10 @@ include AUTHORS include INSTALL include LICENSE +recursive-include docs * +recursive-include scripts * recursive-include django/conf/locale * -recursive-include django/contrib/admin/templates -recursive-include django/contrib/admin/media -recursive-include django/contrib/comments/templates -recursive-include django/contrib/sitemaps/templates +recursive-include django/contrib/admin/templates * +recursive-include django/contrib/admin/media * +recursive-include django/contrib/comments/templates * +recursive-include django/contrib/sitemaps/templates * diff --git a/django/conf/__init__.py b/django/conf/__init__.py index daf5ad766a..021ecc8131 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -7,6 +7,7 @@ a list of all possible variables. """ import os +import time # Needed for Windows from django.conf import global_settings ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE" @@ -105,8 +106,10 @@ class Settings(object): new_installed_apps.append(app) self.INSTALLED_APPS = new_installed_apps - # move the time zone info into os.environ - os.environ['TZ'] = self.TIME_ZONE + if hasattr(time, 'tzset'): + # Move the time zone info into os.environ. See ticket #2315 for why + # we don't do this unconditionally (breaks Windows). + os.environ['TZ'] = self.TIME_ZONE def get_all_members(self): return dir(self) diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index a44bc172f0..4fc03c809b 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -18,6 +18,8 @@ DATABASE_PORT = '' # Set to empty string for default. Not used with # Local time zone for this installation. All choices can be found here: # http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +# If running in a Windows environment this must be set to the same as your +# system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 7700ec7d7a..023f9b43be 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -4,6 +4,7 @@ from django.contrib.sites.models import Site from django.template import Context, loader from django.core import validators from django import oldforms +from django.utils.translation import gettext as _ class UserCreationForm(oldforms.Manipulator): "A form that creates a user, with no privileges, from the given username and password." diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py index 119615a0b9..cdb4e8170f 100644 --- a/django/contrib/syndication/feeds.py +++ b/django/contrib/syndication/feeds.py @@ -78,6 +78,7 @@ class Feed(object): author_link = self.__get_dynamic_attr('author_link', obj), author_email = self.__get_dynamic_attr('author_email', obj), categories = self.__get_dynamic_attr('categories', obj), + feed_copyright = self.__get_dynamic_attr('feed_copyright', obj), ) try: @@ -116,5 +117,6 @@ class Feed(object): author_email = author_email, author_link = author_link, categories = self.__get_dynamic_attr('item_categories', item), + item_copyright = self.__get_dynamic_attr('item_copyright', item), ) return feed diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 964f84ba86..01da0003fd 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -50,4 +50,9 @@ class LazyDate(object): return (datetime.datetime.now() + self.delta).date() def __getattr__(self, attr): + if attr == 'delta': + # To fix ticket #3377. Note that normal accesses to LazyDate.delta + # (after construction) will still work, because they don't go + # through __getattr__). This is mainly needed for unpickling. + raise AttributeError return getattr(self.__get_value__(), attr) diff --git a/django/db/models/query.py b/django/db/models/query.py index 51fa334e63..8a784be7c8 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -167,17 +167,16 @@ class QuerySet(object): def iterator(self): "Performs the SELECT database lookup of this QuerySet." + try: + select, sql, params = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + # self._select is a dictionary, and dictionaries' key order is # undefined, so we convert it to a list of tuples. extra_select = self._select.items() cursor = connection.cursor() - - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - raise StopIteration - cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) fill_cache = self._select_related index_end = len(self.model._meta.fields) @@ -198,9 +197,12 @@ class QuerySet(object): "Performs a SELECT COUNT() and returns the number of records as an integer." counter = self._clone() counter._order_by = () + counter._select_related = False + + offset = counter._offset + limit = counter._limit counter._offset = None counter._limit = None - counter._select_related = False try: select, sql, params = counter._get_sql_clause() @@ -214,7 +216,16 @@ class QuerySet(object): cursor.execute("SELECT COUNT(DISTINCT(%s))" % id_col + sql, params) else: cursor.execute("SELECT COUNT(*)" + sql, params) - return cursor.fetchone()[0] + count = cursor.fetchone()[0] + + # Apply any offset and limit constraints manually, since using LIMIT or + # OFFSET in SQL doesn't change the output of COUNT. + if offset: + count = max(0, count - offset) + if limit: + count = min(limit, count) + + return count def get(self, *args, **kwargs): "Performs the SELECT and returns a single object matching the given keyword arguments." @@ -523,11 +534,18 @@ class QuerySet(object): return select, " ".join(sql), params class ValuesQuerySet(QuerySet): - def iterator(self): + def __init__(self, *args, **kwargs): + super(ValuesQuerySet, self).__init__(*args, **kwargs) # select_related and select aren't supported in values(). self._select_related = False self._select = {} + def iterator(self): + try: + select, sql, params = self._get_sql_clause() + except EmptyResultSet: + raise StopIteration + # self._fields is a list of field names to fetch. if self._fields: columns = [self.model._meta.get_field(f, many_to_many=False).column for f in self._fields] @@ -535,15 +553,9 @@ class ValuesQuerySet(QuerySet): else: # Default to all fields. columns = [f.column for f in self.model._meta.fields] field_names = [f.attname for f in self.model._meta.fields] - - cursor = connection.cursor() - - try: - select, sql, params = self._get_sql_clause() - except EmptyResultSet: - raise StopIteration select = ['%s.%s' % (backend.quote_name(self.model._meta.db_table), backend.quote_name(c)) for c in columns] + cursor = connection.cursor() cursor.execute("SELECT " + (self._distinct and "DISTINCT " or "") + ",".join(select) + sql, params) while 1: rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE) @@ -592,9 +604,6 @@ class EmptyQuerySet(QuerySet): super(EmptyQuerySet, self).__init__(model) self._result_cache = [] - def iterator(self): - raise StopIteration - def count(self): return 0 @@ -606,6 +615,9 @@ class EmptyQuerySet(QuerySet): c._result_cache = [] return c + def _get_sql_clause(self): + raise EmptyResultSet + class QOperator(object): "Base class for QAnd and QOr" def __init__(self, *args): @@ -881,8 +893,14 @@ def lookup_inner(path, lookup_type, value, opts, table, column): new_opts = field.rel.to._meta new_column = new_opts.pk.column join_column = field.column - - raise FieldFound + raise FieldFound + elif path: + # For regular fields, if there are still items on the path, + # an error has been made. We munge "name" so that the error + # properly identifies the cause of the problem. + name += LOOKUP_SEPARATOR + path[0] + else: + raise FieldFound except FieldFound: # Match found, loop has been shortcut. pass diff --git a/django/db/models/related.py b/django/db/models/related.py index ac1ec50ca2..b76da34079 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -68,7 +68,10 @@ class RelatedObject(object): # object return [attr] else: - return [None] * self.field.rel.num_in_admin + if self.field.rel.min_num_in_admin: + return [None] * max(self.field.rel.num_in_admin, self.field.rel.min_num_in_admin) + else: + return [None] * self.field.rel.num_in_admin def get_db_prep_lookup(self, lookup_type, value): # Defer to the actual field definition for db prep diff --git a/django/http/__init__.py b/django/http/__init__.py index 48f10329fd..a6538b7d4a 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -160,7 +160,7 @@ class HttpResponse(object): self._charset = settings.DEFAULT_CHARSET if not mimetype: mimetype = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, settings.DEFAULT_CHARSET) - if hasattr(content, '__iter__'): + if not isinstance(content, basestring) and hasattr(content, '__iter__'): self._container = content self._is_string = False else: diff --git a/django/newforms/fields.py b/django/newforms/fields.py index e9e1fb7746..1391bd1b2f 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -356,7 +356,7 @@ class ChoiceField(Field): return value valid_values = set([str(k) for k, v in self.choices]) if value not in valid_values: - raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % value) + raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.')) return value class MultipleChoiceField(ChoiceField): diff --git a/django/template/__init__.py b/django/template/__init__.py index 7718801684..d01d73e15e 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -117,8 +117,14 @@ class TemplateDoesNotExist(Exception): pass class VariableDoesNotExist(Exception): - pass + def __init__(self, msg, params=()): + self.msg = msg + self.params = params + + def __str__(self): + return self.msg % self.params + class InvalidTemplateLibrary(Exception): pass @@ -660,7 +666,7 @@ def resolve_variable(path, context): try: # list-index lookup current = current[int(bits[0])] except (IndexError, ValueError, KeyError): - raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute + raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bits[0], current)) # missing attribute except Exception, e: if getattr(e, 'silent_variable_failure', False): current = settings.TEMPLATE_STRING_IF_INVALID diff --git a/django/template/context.py b/django/template/context.py index ba23e95ab7..25397b0e1a 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -49,6 +49,9 @@ class Context(object): return True return False + def __contains__(self, key): + return self.has_key(key) + def get(self, key, otherwise=None): for d in self.dicts: if d.has_key(key): diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 1d0f78ce12..a95a5dff6f 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -119,6 +119,21 @@ def truncatewords(value, arg): value = str(value) return truncate_words(value, length) +def truncatewords_html(value, arg): + """ + Truncates HTML after a certain number of words + + Argument: Number of words to truncate after + """ + from django.utils.text import truncate_html_words + try: + length = int(arg) + except ValueError: # invalid literal for int() + return value # Fail silently. + if not isinstance(value, basestring): + value = str(value) + return truncate_html_words(value, length) + def upper(value): "Converts a string into all uppercase" return value.upper() @@ -126,6 +141,8 @@ def upper(value): def urlencode(value): "Escapes a value for use in a URL" import urllib + if not isinstance(value, basestring): + value = str(value) return urllib.quote(value) def urlize(value): @@ -534,6 +551,7 @@ register.filter(timesince) register.filter(timeuntil) register.filter(title) register.filter(truncatewords) +register.filter(truncatewords_html) register.filter(unordered_list) register.filter(upper) register.filter(urlencode) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 3f3f4bda56..6aa53cfd8b 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -315,6 +315,25 @@ class TemplateTagNode(Node): def render(self, context): return self.mapping.get(self.tagtype, '') +class URLNode(Node): + def __init__(self, view_name, args, kwargs): + self.view_name = view_name + self.args = args + self.kwargs = kwargs + + def render(self, context): + from django.core.urlresolvers import reverse, NoReverseMatch + args = [arg.resolve(context) for arg in self.args] + kwargs = dict([(k, v.resolve(context)) for k, v in self.kwargs.items()]) + try: + return reverse(self.view_name, args=args, kwargs=kwargs) + except NoReverseMatch: + try: + project_name = settings.SETTINGS_MODULE.split('.')[0] + return reverse(project_name + '.' + self.view_name, args=args, kwargs=kwargs) + except NoReverseMatch: + return '' + class WidthRatioNode(Node): def __init__(self, val_expr, max_expr, max_width): self.val_expr = val_expr @@ -868,6 +887,50 @@ def templatetag(parser, token): return TemplateTagNode(tag) templatetag = register.tag(templatetag) +def url(parser, token): + """ + Returns an absolute URL matching given view with its parameters. This is a + way to define links that aren't tied to a particular url configuration: + + {% url path.to.some_view arg1,arg2,name1=value1 %} + + The first argument is a path to a view. It can be an absolute python path + or just ``app_name.view_name`` without the project name if the view is + located inside the project. Other arguments are comma-separated values + that will be filled in place of positional and keyword arguments in the + URL. All arguments for the URL should be present. + + For example if you have a view ``app_name.client`` taking client's id and + the corresponding line in a urlconf looks like this: + + ('^client/(\d+)/$', 'app_name.client') + + and this app's urlconf is included into the project's urlconf under some + path: + + ('^clients/', include('project_name.app_name.urls')) + + then in a template you can create a link for a certain client like this: + + {% url app_name.client client.id %} + + The URL will look like ``/clients/client/123/``. + """ + bits = token.contents.split(' ', 2) + if len(bits) < 2: + raise TemplateSyntaxError, "'%s' takes at least one argument (path to a view)" % bits[0] + args = [] + kwargs = {} + if len(bits) > 2: + for arg in bits[2].split(','): + if '=' in arg: + k, v = arg.split('=', 1) + kwargs[k] = parser.compile_filter(v) + else: + args.append(parser.compile_filter(arg)) + return URLNode(bits[1], args, kwargs) +url = register.tag(url) + #@register.tag def widthratio(parser, token): """ diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index e329b1bb36..4439e0b010 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -129,7 +129,7 @@ def do_block(parser, token): parser.__loaded_blocks.append(block_name) except AttributeError: # parser.__loaded_blocks isn't a list yet parser.__loaded_blocks = [block_name] - nodelist = parser.parse(('endblock',)) + nodelist = parser.parse(('endblock', 'endblock %s' % block_name)) parser.delete_first_token() return BlockNode(block_name, nodelist) diff --git a/django/test/client.py b/django/test/client.py index 6e0b443f83..ca1a04e659 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -1,6 +1,9 @@ +import sys from cStringIO import StringIO +from django.conf import settings from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest +from django.core.signals import got_request_exception from django.dispatch import dispatcher from django.http import urlencode, SimpleCookie from django.test import signals @@ -97,7 +100,16 @@ class Client: def __init__(self, **defaults): self.handler = ClientHandler() self.defaults = defaults - self.cookie = SimpleCookie() + self.cookies = SimpleCookie() + self.session = {} + self.exc_info = None + + def store_exc_info(self, *args, **kwargs): + """ + Utility method that can be used to store exceptions when they are + generated by a view. + """ + self.exc_info = sys.exc_info() def request(self, **request): """ @@ -108,7 +120,7 @@ class Client: """ environ = { - 'HTTP_COOKIE': self.cookie, + 'HTTP_COOKIE': self.cookies, 'PATH_INFO': '/', 'QUERY_STRING': '', 'REQUEST_METHOD': 'GET', @@ -126,6 +138,9 @@ class Client: on_template_render = curry(store_rendered_templates, data) dispatcher.connect(on_template_render, signal=signals.template_rendered) + # Capture exceptions created by the handler + dispatcher.connect(self.store_exc_info, signal=got_request_exception) + response = self.handler(environ) # Add any rendered template detail to the response @@ -140,9 +155,20 @@ class Client: else: setattr(response, detail, None) + # Look for a signalled exception and reraise it + if self.exc_info: + raise self.exc_info[1], None, self.exc_info[2] + + # Update persistent cookie and session data if response.cookies: - self.cookie.update(response.cookies) + self.cookies.update(response.cookies) + if 'django.contrib.sessions' in settings.INSTALLED_APPS: + from django.contrib.sessions.middleware import SessionWrapper + cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None) + if cookie: + self.session = SessionWrapper(cookie.value) + return response def get(self, path, data={}, **extra): diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index 2eb27a40b7..9397789d6a 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -40,7 +40,7 @@ class SyndicationFeed(object): "Base class for all syndication feeds. Subclasses should provide write()" def __init__(self, title, link, description, language=None, author_email=None, author_name=None, author_link=None, subtitle=None, categories=None, - feed_url=None): + feed_url=None, feed_copyright=None): self.feed = { 'title': title, 'link': link, @@ -52,12 +52,13 @@ class SyndicationFeed(object): 'subtitle': subtitle, 'categories': categories or (), 'feed_url': feed_url, + 'feed_copyright': feed_copyright, } self.items = [] def add_item(self, title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, - unique_id=None, enclosure=None, categories=()): + unique_id=None, enclosure=None, categories=(), item_copyright=None): """ Adds an item to the feed. All args are expected to be Python Unicode objects except pubdate, which is a datetime.datetime object, and @@ -75,6 +76,7 @@ class SyndicationFeed(object): 'unique_id': unique_id, 'enclosure': enclosure, 'categories': categories or (), + 'item_copyright': item_copyright, }) def num_items(self): @@ -128,6 +130,8 @@ class RssFeed(SyndicationFeed): handler.addQuickElement(u"language", self.feed['language']) for cat in self.feed['categories']: handler.addQuickElement(u"category", cat) + if self.feed['feed_copyright'] is not None: + handler.addQuickElement(u"copyright", self.feed['feed_copyright']) self.write_items(handler) self.endChannelElement(handler) handler.endElement(u"rss") @@ -212,6 +216,8 @@ class Atom1Feed(SyndicationFeed): handler.addQuickElement(u"subtitle", self.feed['subtitle']) for cat in self.feed['categories']: handler.addQuickElement(u"category", "", {u"term": cat}) + if self.feed['feed_copyright'] is not None: + handler.addQuickElement(u"rights", self.feed['feed_copyright']) self.write_items(handler) handler.endElement(u"feed") @@ -252,10 +258,14 @@ class Atom1Feed(SyndicationFeed): u"length": item['enclosure'].length, u"type": item['enclosure'].mime_type}) - # Categories: + # Categories. for cat in item['categories']: handler.addQuickElement(u"category", u"", {u"term": cat}) + # Rights. + if item['item_copyright'] is not None: + handler.addQuickElement(u"rights", item['item_copyright']) + handler.endElement(u"entry") # This isolates the decision of what the system default is, so calling code can diff --git a/django/utils/text.py b/django/utils/text.py index 217f42491b..1c1c456e2d 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -41,6 +41,66 @@ def truncate_words(s, num): words.append('...') return ' '.join(words) +def truncate_html_words(s, num): + """ + Truncates html to a certain number of words (not counting tags and comments). + Closes opened tags if they were correctly closed in the given html. + """ + length = int(num) + if length <= 0: + return '' + html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input') + # Set up regular expressions + re_words = re.compile(r'&.*?;|<.*?>|([A-Za-z0-9][\w-]*)') + re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>') + # Count non-HTML words and keep note of open tags + pos = 0 + ellipsis_pos = 0 + words = 0 + open_tags = [] + while words <= length: + m = re_words.search(s, pos) + if not m: + # Checked through whole string + break + pos = m.end(0) + if m.group(1): + # It's an actual non-HTML word + words += 1 + if words == length: + ellipsis_pos = pos + continue + # Check for tag + tag = re_tag.match(m.group(0)) + if not tag or ellipsis_pos: + # Don't worry about non tags or tags after our truncate point + continue + closing_tag, tagname, self_closing = tag.groups() + tagname = tagname.lower() # Element names are always case-insensitive + if self_closing or tagname in html4_singlets: + pass + elif closing_tag: + # Check for match in open tags list + try: + i = open_tags.index(tagname) + except ValueError: + pass + else: + # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags + open_tags = open_tags[i+1:] + else: + # Add it to the start of the open tags list + open_tags.insert(0, tagname) + if words <= length: + # Don't try to close tags if we don't need to truncate + return s + out = s[:ellipsis_pos] + ' ...' + # Close any tags still open + for tag in open_tags: + out += '' % tag + # Return string + return out + def get_valid_filename(s): """ Returns the given string converted to a string that can be used for a clean diff --git a/docs/contributing.txt b/docs/contributing.txt index d802d3eaf6..6b2b64f672 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -484,6 +484,29 @@ Alternatively, you can use a symlink called ``django`` that points to the location of the branch's ``django`` package. If you want to switch back, just change the symlink to point to the old code. +A third option is to use a `path file`_ (``.pth``) which should +work on all systems (including Windows, which doesn't have symlinks +available). First, make sure there are no files, directories or symlinks named +``django`` in your ``site-packages`` directory. Then create a text file named +``django.pth`` and save it to your ``site-packages`` directory. That file +should contain a path to your copy of Django on a single line and optional +comments. Here is an example that points to multiple branches. Just uncomment +the line for the branch you want to use ('Trunk' in this example) and make +sure all other lines are commented:: + + # Trunk is a svn checkout of: + # http://code.djangoproject.com/svn/django/trunk/ + # + /path/to/trunk + + # is a svn checkout of: + # http://code.djangoproject.com/svn/django/branches// + # + #/path/to/ + + # On windows a path may look like this: + # C:/path/to/ + If you're using Django 0.95 or earlier and installed it using ``python setup.py install``, you'll have a directory called something like ``Django-0.95-py2.4.egg`` instead of ``django``. In this case, edit the file @@ -491,6 +514,8 @@ If you're using Django 0.95 or earlier and installed it using file. Then copy the branch's version of the ``django`` directory into ``site-packages``. +.. _path file: http://docs.python.org/lib/module-site.html + Official releases ================= diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 310e8dff0e..cf15168030 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -17,7 +17,12 @@ two things for you before delegating to ``django-admin.py``: The ``django-admin.py`` script should be on your system path if you installed Django via its ``setup.py`` utility. If it's not on your path, you can find it in ``site-packages/django/bin`` within your Python installation. Consider -symlinking to it from some place on your path, such as ``/usr/local/bin``. +symlinking it from some place on your path, such as ``/usr/local/bin``. + +For Windows users, who do not have symlinking functionality available, you +can copy ``django-admin.py`` to a location on your existing path or edit the +``PATH`` settings (under ``Settings - Control Panel - System - Advanced - Environment...``) +to point to its installed location. Generally, when working on a single Django project, it's easier to use ``manage.py``. Use ``django-admin.py`` with ``DJANGO_SETTINGS_MODULE``, or the diff --git a/docs/fastcgi.txt b/docs/fastcgi.txt index b61df49190..1efeaf09cf 100644 --- a/docs/fastcgi.txt +++ b/docs/fastcgi.txt @@ -274,7 +274,7 @@ In your Web root directory, add this to a file named ``.htaccess`` :: Then, create a small script that tells Apache how to spawn your FastCGI program. Create a file ``mysite.fcgi`` and place it in your Web directory, and -be sure to make it executable :: +be sure to make it executable:: #!/usr/bin/python import sys, os diff --git a/docs/forms.txt b/docs/forms.txt index 3fa11fea64..fc10e3f17a 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -173,10 +173,10 @@ creation view that takes validation into account:: # Check for validation errors errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) if errors: return render_to_response('places/errors.html', {'errors': errors}) else: - manipulator.do_html2python(new_data) new_place = manipulator.save(new_data) return HttpResponse("Place created: %s" % new_place) @@ -229,10 +229,10 @@ Below is the finished view:: # Check for errors. errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) if not errors: # No errors. This means we can save the data! - manipulator.do_html2python(new_data) new_place = manipulator.save(new_data) # Redirect to the object's "edit" page. Always use a redirect @@ -324,8 +324,8 @@ about editing an existing one? It's shockingly similar to creating a new one:: if request.method == 'POST': new_data = request.POST.copy() errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) if not errors: - manipulator.do_html2python(new_data) manipulator.save(new_data) # Do a post-after-redirect so that reload works, etc. @@ -406,8 +406,8 @@ Here's a simple function that might drive the above form:: if request.method == 'POST': new_data = request.POST.copy() errors = manipulator.get_validation_errors(new_data) + manipulator.do_html2python(new_data) if not errors: - manipulator.do_html2python(new_data) # Send e-mail using new_data here... diff --git a/docs/outputting_pdf.txt b/docs/outputting_pdf.txt index 464bf7fcb8..bd6ae7a660 100644 --- a/docs/outputting_pdf.txt +++ b/docs/outputting_pdf.txt @@ -29,7 +29,7 @@ Test your installation by importing it in the Python interactive interpreter:: If that command doesn't raise any errors, the installation worked. -.. _user guide: http://www.reportlab.org/rsrc/userguide.pdf +.. _user guide: http://www.reportlab.com/docs/userguide.pdf Write your view =============== diff --git a/docs/settings.txt b/docs/settings.txt index cdf440ed6b..b9e46a858c 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -827,6 +827,11 @@ manual configuration option (see below), Django will *not* touch the ``TZ`` environment variable, and it'll be up to you to ensure your processes are running in the correct environment. +.. note:: + Django cannot reliably use alternate time zones in a Windows environment. + If you're running Django on Windows, this variable must be set to match the + system timezone. + URL_VALIDATOR_USER_AGENT ------------------------ diff --git a/docs/syndication_feeds.txt b/docs/syndication_feeds.txt index 72b05ff16a..a64914de3f 100644 --- a/docs/syndication_feeds.txt +++ b/docs/syndication_feeds.txt @@ -127,7 +127,7 @@ put into those elements. it two template context variables: * ``{{ obj }}`` -- The current object (one of whichever objects you - returned in ``items()``). + returned in ``items()``). * ``{{ site }}`` -- A ``django.models.core.sites.Site`` object representing the current site. This is useful for ``{{ site.domain }}`` or ``{{ site.name }}``. @@ -478,6 +478,22 @@ This example illustrates all possible attributes and methods for a ``Feed`` clas categories = ("python", "django") # Hard-coded list of categories. + # COPYRIGHT NOTICE -- One of the following three is optional. The + # framework looks for them in this order. + + def copyright(self, obj): + """ + Takes the object returned by get_object() and returns the feed's + copyright notice as a normal Python string. + """ + + def copyright(self): + """ + Returns the feed's copyright notice as a normal Python string. + """ + + copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice. + # ITEMS -- One of the following three is required. The framework looks # for them in this order. @@ -659,6 +675,23 @@ This example illustrates all possible attributes and methods for a ``Feed`` clas item_categories = ("python", "django") # Hard-coded categories. + # ITEM COPYRIGHT NOTICE (only applicable to Atom feeds) -- One of the + # following three is optional. The framework looks for them in this + # order. + + def item_copyright(self, obj): + """ + Takes an item, as returned by items(), and returns the item's + copyright notice as a normal Python string. + """ + + def item_copyright(self): + """ + Returns the copyright notice for every item in the feed. + """ + + item_copyright = 'Copyright (c) 2007, Sally Smith' # Hard-coded copyright notice. + The low-level framework ======================= diff --git a/docs/templates.txt b/docs/templates.txt index 9f8fe446b4..5a007c13ae 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -253,6 +253,16 @@ Here are some tips for working with inheritance: if you want to add to the contents of a parent block instead of completely overriding it. + * **New in Django development version:** For extra readability, you can + optionally give a *name* to your ``{% endblock %}`` tag. For example:: + + {% block content %} + ... + {% endblock content %} + + In larger templates, this technique helps you see which ``{% block %}`` + tags are being closed. + Finally, note that you can't define multiple ``{% block %}`` tags with the same name in the same template. This limitation exists because a block tag works in "both" directions. That is, a block tag doesn't just provide a hole to fill -- @@ -819,6 +829,40 @@ The argument tells which template bit to output: Note: ``opencomment`` and ``closecomment`` are new in the Django development version. +url +~~~ + +**New in Django development version** + +**Note that the syntax for this tag may change in the future, as we make it more robust.** + +Returns an absolute URL (i.e., a URL without the domain name) matching a given +view function and optional parameters. This is a way to output links without +violating the DRY principle by having to hard-code URLs in your templates:: + + {% url path.to.some_view arg1,arg2,name1=value1 %} + +The first argument is a path to a view function in the format +``package.package.module.function``. Additional arguments are optional and +should be comma-separated values that will be used as positional and keyword +arguments in the URL. All arguments required by the URLconf should be present. + +For example, suppose you have a view, ``app_name.client``, whose URLconf takes +a client ID. The URLconf line might look like this:: + + ('^client/(\d+)/$', 'app_name.client') + +If this app's URLconf is included into the project's URLconf under a path +such as this:: + + ('^clients/', include('project_name.app_name.urls')) + +...then, in a template, you can create a link to this view like this:: + + {% url app_name.client client.id %} + +The template tag will output the string ``/clients/client/123/``. + widthratio ~~~~~~~~~~ @@ -1133,6 +1177,16 @@ Truncates a string after a certain number of words. **Argument:** Number of words to truncate after +truncatewords_html +~~~~~~~~~~~~~~~~~~ + +Similar to ``truncatewords``, except that it is aware of HTML tags. Any tags +that are opened in the string and not closed before the truncation point, are +closed immediately after the truncation. + +This is less efficient than ``truncatewords``, so should only be used when it +is being passed HTML text. + unordered_list ~~~~~~~~~~~~~~ diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 5f9c5bde43..a6b565ed5c 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -801,6 +801,70 @@ Python 2.4 and above:: If you leave off the ``name`` argument, as in the second example above, Django will use the function's name as the tag name. +Passing template variables to the tag +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Although you can pass any number of arguments to a template tag using +``token.split_contents()``, the arguments are all unpacked as +string literals. A little more work is required in order to dynamic content (a +template variable) to a template tag as an argument. + +While the previous examples have formatted the current time into a string and +returned the string, suppose you wanted to pass in a ``DateTimeField`` from an +object and have the template tag format that date-time:: + +

This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.

+ +Initially, ``token.split_contents()`` will return three values: + + 1. The tag name ``format_time``. + 2. The string "blog_entry.date_updated" (without the surrounding quotes). + 3. The formatting string "%Y-%m-%d %I:%M %p". The return value from + ``split_contents()`` will include the leading and trailing quotes for + string literals like this. + +Now your tag should begin to look like this:: + + from django import template + def do_format_time(parser, token): + try: + # split_contents() knows not to split quoted strings. + tag_name, date_to_be_formatted, format_string = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires exactly two arguments" % token.contents[0] + if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): + raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name + return FormatTimeNode(date_to_be_formatted, format_string[1:-1]) + +You also have to change the renderer to retrieve the actual contents of the +``date_updated`` property of the ``blog_entry`` object. This can be +accomplished by using the ``resolve_variable()`` function in +``django.template``. You pass ``resolve_variable()`` the variable name and the +current context, available in the ``render`` method:: + + from django import template + from django.template import resolve_variable + import datetime + class FormatTimeNode(template.Node): + def __init__(self, date_to_be_formatted, format_string): + self.date_to_be_formatted = date_to_be_formatted + self.format_string = format_string + + def render(self, context): + try: + actual_date = resolve_variable(self.date_to_be_formatted, context) + return actual_date.strftime(self.format_string) + except VariableDoesNotExist: + return '' + +``resolve_variable`` will try to resolve ``blog_entry.date_updated`` and then +format it accordingly. + +.. note:: + The ``resolve_variable()`` function will throw a ``VariableDoesNotExist`` + exception if it cannot resolve the string passed to it in the current + context of the page. + Shortcut for simple tags ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/testing.txt b/docs/testing.txt index e7c1a3b161..cab31ed63b 100644 --- a/docs/testing.txt +++ b/docs/testing.txt @@ -198,11 +198,6 @@ used as test conditions. .. _Twill: http://twill.idyll.org/ .. _Selenium: http://www.openqa.org/selenium/ -The Test Client is stateful; if a cookie is returned as part of a response, -that cookie is provided as part of the next request issued to that Client -instance. Expiry policies for these cookies are not followed; if you want -a cookie to expire, either delete it manually from ``client.cookies``, or -create a new Client instance (which will effectively delete all cookies). Making requests ~~~~~~~~~~~~~~~ @@ -296,6 +291,44 @@ for testing purposes: .. _RFC2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html +Exceptions +~~~~~~~~~~ + +If you point the Test Client at a view that raises an exception, that exception +will be visible in the test case. You can then use a standard ``try...catch`` +block, or ``unittest.TestCase.assertRaises()`` to test for exceptions. + +The only exceptions that are not visible in a Test Case are ``Http404``, +``PermissionDenied`` and ``SystemExit``. Django catches these exceptions +internally and converts them into the appropriate HTTP responses codes. + +Persistent state +~~~~~~~~~~~~~~~~ + +The Test Client is stateful; if a cookie is returned as part of a response, +that cookie is provided as part of the next request issued by that Client +instance. Expiry policies for these cookies are not followed; if you want +a cookie to expire, either delete it manually or create a new Client +instance (which will effectively delete all cookies). + +There are two properties of the Test Client which are used to store persistent +state information. If necessary, these properties can be interrogated as +part of a test condition. + + =============== ========================================================== + Property Description + =============== ========================================================== + ``cookies`` A Python ``SimpleCookie`` object, containing the current + values of all the client cookies. + + ``session`` A dictionary-like object containing session information. + See the `session documentation`_ for full details. + +.. _`session documentation`: ../sessions/ + +Example +~~~~~~~ + The following is a simple unit test using the Test Client:: import unittest diff --git a/scripts/rpm-install.sh b/scripts/rpm-install.sh new file mode 100644 index 0000000000..07a087c447 --- /dev/null +++ b/scripts/rpm-install.sh @@ -0,0 +1,19 @@ +#! /bin/sh +# +# this file is *inserted* into the install section of the generated +# spec file +# + +# this is, what dist.py normally does +python setup.py install --root=${RPM_BUILD_ROOT} --record="INSTALLED_FILES" + +for i in `cat INSTALLED_FILES`; do + if [ -f ${RPM_BUILD_ROOT}/$i ]; then + echo $i >>FILES + fi + if [ -d ${RPM_BUILD_ROOT}/$i ]; then + echo %dir $i >>DIRS + fi +done + +cat DIRS FILES >INSTALLED_FILES diff --git a/setup.cfg b/setup.cfg index d3d908abf5..ce9779aa00 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,4 @@ [bdist_rpm] doc_files = docs/*.txt +install-script = scripts/rpm-install.sh diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index aa903d1a64..106c97d3b4 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -58,6 +58,17 @@ Article 4 >>> Article.objects.filter(headline__startswith='Blah blah').count() 0L +# count() should respect sliced query sets. +>>> articles = Article.objects.all() +>>> articles.count() +7L +>>> articles[:4].count() +4 +>>> articles[1:100].count() +6L +>>> articles[10:100].count() +0 + # Date and date/time lookups can also be done with strings. >>> Article.objects.filter(pub_date__exact='2005-07-27 00:00:00').count() 3L @@ -198,6 +209,8 @@ DoesNotExist: Article matching query does not exist. [] >>> Article.objects.none().count() 0 +>>> [article for article in Article.objects.none().iterator()] +[] # using __in with an empty list should return an empty query set >>> Article.objects.filter(id__in=[]) @@ -206,4 +219,15 @@ DoesNotExist: Article matching query does not exist. >>> Article.objects.exclude(id__in=[]) [, , , , , , , , , ] +# Programming errors are pointed out with nice error messages +>>> Article.objects.filter(pub_date_year='2005').count() +Traceback (most recent call last): + ... +TypeError: Cannot resolve keyword 'pub_date_year' into field + +>>> Article.objects.filter(headline__starts='Article') +Traceback (most recent call last): + ... +TypeError: Cannot resolve keyword 'headline__starts' into field + """} diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index c5b1a241ca..74f5e9700c 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -99,3 +99,29 @@ class ClientTest(unittest.TestCase): response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword') self.assertFalse(response) + + def test_session_modifying_view(self): + "Request a page that modifies the session" + # Session value isn't set initially + try: + self.client.session['tobacconist'] + self.fail("Shouldn't have a session value") + except KeyError: + pass + + from django.contrib.sessions.models import Session + response = self.client.post('/test_client/session_view/') + + # Check that the session was modified + self.assertEquals(self.client.session['tobacconist'], 'hovercraft') + + def test_view_with_exception(self): + "Request a page that is known to throw an error" + self.assertRaises(KeyError, self.client.get, "/test_client/broken_view/") + + #Try the same assertion, a different way + try: + self.client.get('/test_client/broken_view/') + self.fail('Should raise an error') + except KeyError: + pass diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py index 09bba5c007..96da4ec34e 100644 --- a/tests/modeltests/test_client/urls.py +++ b/tests/modeltests/test_client/urls.py @@ -6,4 +6,6 @@ urlpatterns = patterns('', (r'^post_view/$', views.post_view), (r'^redirect_view/$', views.redirect_view), (r'^login_protected_view/$', views.login_protected_view), + (r'^session_view/$', views.session_view), + (r'^broken_view/$', views.broken_view) ) diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py index 7acfc2db60..b1f2739845 100644 --- a/tests/modeltests/test_client/views.py +++ b/tests/modeltests/test_client/views.py @@ -32,4 +32,17 @@ def login_protected_view(request): c = Context({'user': request.user}) return HttpResponse(t.render(c)) -login_protected_view = login_required(login_protected_view) \ No newline at end of file +login_protected_view = login_required(login_protected_view) + +def session_view(request): + "A view that modifies the session" + request.session['tobacconist'] = 'hovercraft' + + t = Template('This is a view that modifies the session.', + name='Session Modifying View Template') + c = Context() + return HttpResponse(t.render(c)) + +def broken_view(request): + """A view which just raises an exception, simulating a broken view.""" + raise KeyError("Oops! Looks like you wrote some bad code.") diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index 439a40c31b..b4ec9a0b03 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -87,6 +87,20 @@ u'\xeb' >>> truncatewords('A sentence with a few words in it', 'not a number') 'A sentence with a few words in it' +>>> truncatewords_html('

one two - three
four
five

', 0) +'' + +>>> truncatewords_html('

one two - three
four
five

', 2) +'

one two ...

' + +>>> truncatewords_html('

one two - three
four
five

', 4) +'

one two - three
four ...

' + +>>> truncatewords_html('

one two - three
four
five

', 5) +'

one two - three
four
five

' + +>>> truncatewords_html('

one two - three
four
five

', 100) +'

one two - three
four
five

' >>> upper('Mixed case input') 'MIXED CASE INPUT' @@ -97,6 +111,8 @@ u'\xcb' >>> urlencode('jack & jill') 'jack%20%26%20jill' +>>> urlencode(1) +'1' >>> urlizetrunc('http://short.com/', 20) diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 20a1937f56..be17864b9d 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -1493,7 +1493,7 @@ u'1' >>> f.clean('3') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] >>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False) >>> f.clean('') @@ -1507,7 +1507,7 @@ u'1' >>> f.clean('3') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] >>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) >>> f.clean('J') @@ -1515,7 +1515,7 @@ u'J' >>> f.clean('John') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. John is not one of the available choices.'] +ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] # NullBooleanField ############################################################ diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 0a41f5b5b7..3bae6a2609 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -390,6 +390,21 @@ class Templates(unittest.TestCase): 'include03': ('{% include template_name %}', {'template_name': 'basic-syntax02', 'headline': 'Included'}, "Included"), 'include04': ('a{% include "nonexistent" %}b', {}, "ab"), + ### NAMED ENDBLOCKS ####################################################### + + # Basic test + 'namedendblocks01': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock first %}3", {}, '1_2_3'), + + # Unbalanced blocks + 'namedendblocks02': ("1{% block first %}_{% block second %}2{% endblock first %}_{% endblock second %}3", {}, template.TemplateSyntaxError), + 'namedendblocks03': ("1{% block first %}_{% block second %}2{% endblock %}_{% endblock second %}3", {}, template.TemplateSyntaxError), + 'namedendblocks04': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock third %}3", {}, template.TemplateSyntaxError), + 'namedendblocks05': ("1{% block first %}_{% block second %}2{% endblock first %}", {}, template.TemplateSyntaxError), + + # Mixed named and unnamed endblocks + 'namedendblocks06': ("1{% block first %}_{% block second %}2{% endblock %}_{% endblock first %}3", {}, '1_2_3'), + 'namedendblocks07': ("1{% block first %}_{% block second %}2{% endblock second %}_{% endblock %}3", {}, '1_2_3'), + ### INHERITANCE ########################################################### # Standard template with no inheritance @@ -630,6 +645,17 @@ class Templates(unittest.TestCase): # Compare to a given parameter 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'), 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'), + + ### URL TAG ######################################################## + # Successes + 'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), + 'url02' : ('{% url regressiontests.templates.views.client_action client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url03' : ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'), + + # Failures + 'url04' : ('{% url %}', {}, template.TemplateSyntaxError), + 'url05' : ('{% url no_such_view %}', {}, ''), + 'url06' : ('{% url regressiontests.templates.views.client no_such_param="value" %}', {}, ''), } # Register our custom template loader. diff --git a/tests/regressiontests/templates/urls.py b/tests/regressiontests/templates/urls.py new file mode 100644 index 0000000000..dc5b36b08b --- /dev/null +++ b/tests/regressiontests/templates/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import * +from regressiontests.templates import views + +urlpatterns = patterns('', + + # Test urls for testing reverse lookups + (r'^$', views.index), + (r'^client/(\d+)/$', views.client), + (r'^client/(\d+)/(?P[^/]+)/$', views.client_action), +) diff --git a/tests/regressiontests/templates/views.py b/tests/regressiontests/templates/views.py new file mode 100644 index 0000000000..b68809944a --- /dev/null +++ b/tests/regressiontests/templates/views.py @@ -0,0 +1,10 @@ +# Fake views for testing url reverse lookup + +def index(request): + pass + +def client(request, id): + pass + +def client_action(request, id, action): + pass diff --git a/tests/runtests.py b/tests/runtests.py index 20189c2d99..1c15f18510 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -77,13 +77,19 @@ def django_tests(verbosity, tests_to_run): old_root_urlconf = settings.ROOT_URLCONF old_template_dirs = settings.TEMPLATE_DIRS old_use_i18n = settings.USE_I18N + old_middleware_classes = settings.MIDDLEWARE_CLASSES - # Redirect some settings for the duration of these tests + # Redirect some settings for the duration of these tests. settings.TEST_DATABASE_NAME = TEST_DATABASE_NAME settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS settings.ROOT_URLCONF = 'urls' settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),) settings.USE_I18N = True + settings.MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.common.CommonMiddleware', + ) # Load all the ALWAYS_INSTALLED_APPS. # (This import statement is intentionally delayed until after we @@ -91,7 +97,7 @@ def django_tests(verbosity, tests_to_run): from django.db.models.loading import get_apps, load_app get_apps() - # Load all the test model apps + # Load all the test model apps. test_models = [] for model_dir, model_name in get_test_models(): model_label = '.'.join([model_dir, model_name]) @@ -109,7 +115,7 @@ def django_tests(verbosity, tests_to_run): sys.stderr.write("Error while importing %s:" % model_name + ''.join(traceback.format_exception(*sys.exc_info())[1:])) continue - # Add tests for invalid models + # Add tests for invalid models. extra_tests = [] for model_dir, model_name in get_invalid_models(): model_label = '.'.join([model_dir, model_name]) @@ -120,12 +126,13 @@ def django_tests(verbosity, tests_to_run): from django.test.simple import run_tests run_tests(test_models, verbosity, extra_tests=extra_tests) - # Restore the old settings + # Restore the old settings. settings.INSTALLED_APPS = old_installed_apps settings.TESTS_DATABASE_NAME = old_test_database_name settings.ROOT_URLCONF = old_root_urlconf settings.TEMPLATE_DIRS = old_template_dirs settings.USE_I18N = old_use_i18n + settings.MIDDLEWARE_CLASSES = old_middleware_classes if __name__ == "__main__": from optparse import OptionParser diff --git a/tests/urls.py b/tests/urls.py index 39d5aaee6b..9fefd93624 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -7,4 +7,7 @@ urlpatterns = patterns('', # 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.login'), + + # test urlconf for {% url %} template tag + (r'^url_tag/', include('regressiontests.templates.urls')), )