From 5e5fe5b5a2abe9fe680085ae1aa89a765ae276cd Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:28:03 +0000 Subject: Added docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@855 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 243 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/design_philosophies.txt diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt new file mode 100644 index 0000000000..9bfe81ddc1 --- /dev/null +++ b/docs/design_philosophies.txt @@ -0,0 +1,243 @@ +=================== +Design philosophies +=================== + +This document explains some of the fundamental philosophies Django's developers +have used in creating the framework. Its goal is to explain the past and guide +the future. + +Overall +======= + +Loose coupling +-------------- + +A fundamental goal of Django's stack is `loose coupling and tight cohesion`_. +The various layers of the framework shouldn't "know" about each other unless +absolutely necessary. + +For example, the template system knows nothing about Web requests, the database +layer knows nothing about data display and the view system doesn't care which +template system a programmer uses. + +.. _`loose coupling and tight cohesion`: http://c2.com/cgi/wiki?CouplingAndCohesion + +Less code +--------- + +Django apps should use as little code as possible; they should lack boilerplate. +Django should take full advantage of Python's dynamic capabilities, such as +introspection. + +Quick development +----------------- + +The point of a Web framework in the 21st century is to make the tedious aspects +of Web development fast. Django should allow for incredibly quick Web +development. + +Don't repeat yourself (DRY) +--------------------------- + +Every distinct concept and/or piece of data should live in one, and only one, +place. Redundancy is bad. Normalization is good. + +The framework, within reason, should deduce as much as possible from as little +as possible. + +Explicit is better than implicit +-------------------------------- + +This, a `core Python principle`_, means Django shouldn't do too much "magic." +Magic shouldn't happen unless there's a really good reason for it. + +.. _`core Python principle`: http://www.python.org/doc/Humor.html#zen + +Consistency +----------- + +The framework should be consistent at all levels. Consistency applies to +everything from low-level (the Python coding style used) to high-level (the +"experience" of using Django). + +Models +====== + +Explicit is better than implicit +-------------------------------- + +Fields shouldn't assume certain behaviors based solely on the name of the +field. This requires too much knowledge of the system and is prone to errors. +Instead, behaviors should be based on keyword arguments and, in some cases, on +the type of the field. + +Include all relevant domain logic +--------------------------------- + +Models should encapsulate every aspect of an "object," following Martin +Fowler's `Active Record`_ design pattern. + +This is why model-specific admin options are included in the model itself; data +related to a model should be stored *in* the model. + +.. _`Active Record`: http://www.martinfowler.com/eaaCatalog/activeRecord.html + +Database API +============ + +The core goals of the database API are: + +SQL efficiency +-------------- + +It should execute SQL statements as few times as possible, and it should +optimize statements internally. + +This is why developers need to call ``save()`` explicitly, rather than the +framework saving things behind the scenes silently. + +This is also why the ``select_related`` argument exists. It's an optional +performance booster for the common case of selecting "every related object." + +Terse, powerful syntax +---------------------- + +The database API should allow rich, expressive statements in as little syntax +as possible. It should not rely on importing other modules or helper objects. + +Joins should be performed automatically, behind the scenes, when necessary. + +Every object should be able to access every related object, systemwide. This +access should work both ways. + +Option to drop into raw SQL easily, when needed +----------------------------------------------- + +The database API should realize it's a shortcut but not necessarily an +end-all-be-all. The framework should make it easy to write custom SQL -- entire +statements, or just custom ``WHERE`` clauses as custom parameters to API calls. + +URL design +========== + +Loose coupling +-------------- + +URLs in a Django app should not be coupled to the underlying Python code. Tying +URLs to Python function names is a Bad And Ugly Thing. + +Along these lines, the Django URL system should allow URLs for the same app to +be different in different contexts. For example, one site may put stories at +``/stories/``, while another may use ``/news/``. + +Infinite flexibility +-------------------- + +URLs should be as flexible as possible. Any conceivable URL design should be +allowed. + +Encourage best practices +------------------------ + +The framework should make it just as easy (or even easier) for a developer to +design pretty URLs than ugly ones. + +File extensions in Web-page URLs should be avoided. + +Definitive URLs +--------------- + +Technically, ``foo.com/bar`` and ``foo.com/bar/`` are two different URLs, and +search-engine robots (and some Web traffic-analyzing tools) would treat them as +separate pages. Django should make an effort to "normalize" URLs so that +search-engine robots don't get confused. + +This is the reasoning behind the ``APPEND_SLASH`` setting. + +Template system +=============== + +Separate logic from presentation +-------------------------------- + +We see a template system as a tool that controls presentation and +presentation-related logic -- and that's it. The template system shouldn't +support functionality that goes beyond this basic goal. + +If we wanted to put everything in templates, we'd be using PHP. Been there, +done that, wised up. + +Discourage redundancy +--------------------- + +The majority of dynamic Web sites use some sort of common sitewide design -- +a common header, footer, navigation bar, etc. The Django template system should +make it easy to store those elements in a single place, eliminating duplicate +code. + +This is the philosophy behind template inheritance. + +Be decoupled from HTML +---------------------- + +The template system shouldn't be designed so that it only outputs HTML. It +should be equally good at generating other text-based formats, or just plain +text. + +Assume designer competence +-------------------------- + +The template system shouldn't be designed so that templates are displayed +nicely in WYSIWYG editors such as Dreamweaver. That is too severe of a +limitation and wouldn't allow the syntax to be as nice as it is. Django expects +some level of competence in template authors. + +Treat whitespace obviously +-------------------------- + +The template system do magic things with whitespace. If a template includes +whitespace, the system should treat it as it treats text -- just display it. + +Don't invent a programming language +----------------------------------- + +The template system intentionally doesn't allow the following: + + * Assignment to variables + * Advanced logic + +The goal is not to invent a programming language. The goal is to offer just +enough programming-esque functionality, such as branching and looping, that are +essential for making presentation-related decisions. + +Extensibility +------------- + +The template system should recognize that advanced template authors may want +to extend its technology. + +This is the philosophy behind custom template tags and filters. + +Views +===== + +Simplicity +---------- + +Writing a view should be as simple as writing a Python function. Developers +shouldn't have to instantiate a class when a function will do. + +Use request objects +------------------- + +Views should have access to a request object -- an object that stores metadata +about the current request. The object should be passed directly to a view +function, rather than the view function having to access the request data from +a global variable. This makes it light, clean and easy to test views by passing +in "fake" request objects. + +Loose coupling +-------------- + +A view shouldn't care about which template system the developer uses -- or even +whether a template system is used at all. -- cgit v1.3 From cdfbdfb1b23be17ac2aac00a10db8874b54fd7e2 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:32:34 +0000 Subject: Cleaned up 'Assume designer competence' section in docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@856 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 9bfe81ddc1..9553e4fb44 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -187,10 +187,10 @@ text. Assume designer competence -------------------------- -The template system shouldn't be designed so that templates are displayed -nicely in WYSIWYG editors such as Dreamweaver. That is too severe of a -limitation and wouldn't allow the syntax to be as nice as it is. Django expects -some level of competence in template authors. +The template system shouldn't be designed so that templates necessarily are +displayed nicely in WYSIWYG editors such as Dreamweaver. That is too severe of +a limitation and wouldn't allow the syntax to be as nice as it is. Django +expects some level of code competence in template authors. Treat whitespace obviously -------------------------- -- cgit v1.3 From d5bbe395c216f3b71ea1ab4a0d820df9a229d5b1 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:33:54 +0000 Subject: Small grammar cleanup to design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@857 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 9553e4fb44..257c3b0c2e 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -196,7 +196,8 @@ Treat whitespace obviously -------------------------- The template system do magic things with whitespace. If a template includes -whitespace, the system should treat it as it treats text -- just display it. +whitespace, the system should treat the whitespace as it treats text -- just +display it. Don't invent a programming language ----------------------------------- -- cgit v1.3 From f2088d456c4ca394f59d179e990c6e611e92ff5f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:43:01 +0000 Subject: Fixed #618 -- Added DATABASE_PORT setting. Thanks, Esaj git-svn-id: http://code.djangoproject.com/svn/django/trunk@858 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 3 ++- django/conf/project_template/settings/main.py | 1 + django/core/db/backends/mysql.py | 14 +++++++++++--- django/core/db/backends/postgresql.py | 4 +++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index c651543ff9..ea2fc440de 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -49,7 +49,8 @@ DATABASE_ENGINE = 'postgresql' # 'postgresql', 'mysql', or 'sqlite3'. DATABASE_NAME = '' DATABASE_USER = '' DATABASE_PASSWORD = '' -DATABASE_HOST = '' # Set to empty string for localhost +DATABASE_HOST = '' # Set to empty string for localhost. +DATABASE_PORT = '' # Set to empty string for default. # Host for sending e-mail. EMAIL_HOST = 'localhost' diff --git a/django/conf/project_template/settings/main.py b/django/conf/project_template/settings/main.py index 56b1f023ce..38df2ad01d 100644 --- a/django/conf/project_template/settings/main.py +++ b/django/conf/project_template/settings/main.py @@ -15,6 +15,7 @@ DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. SITE_ID = 1 diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py index af0dbca6c0..4399b6a6a0 100644 --- a/django/core/db/backends/mysql.py +++ b/django/core/db/backends/mysql.py @@ -53,10 +53,18 @@ class DatabaseWrapper: self.queries = [] def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG + from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG if self.connection is None: - self.connection = Database.connect(user=DATABASE_USER, db=DATABASE_NAME, - passwd=DATABASE_PASSWORD, host=DATABASE_HOST, conv=django_conversions) + kwargs = { + 'user': DATABASE_USER, + 'db': DATABASE_NAME, + 'passwd': DATABASE_PASSWORD, + 'host': DATABASE_HOST, + 'conv': django_conversions, + } + if DATABASE_PORT: + kwargs['port'] = DATABASE_PORT + self.connection = Database.connect(**kwargs) if DEBUG: return base.CursorDebugWrapper(MysqlDebugWrapper(self.connection.cursor()), self) return self.connection.cursor() diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py index 6ec7bfbfcb..c922fd42f6 100644 --- a/django/core/db/backends/postgresql.py +++ b/django/core/db/backends/postgresql.py @@ -15,7 +15,7 @@ class DatabaseWrapper: self.queries = [] def cursor(self): - from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PASSWORD, DEBUG, TIME_ZONE + from django.conf.settings import DATABASE_USER, DATABASE_NAME, DATABASE_HOST, DATABASE_PORT, DATABASE_PASSWORD, DEBUG, TIME_ZONE if self.connection is None: if DATABASE_NAME == '': from django.core.exceptions import ImproperlyConfigured @@ -27,6 +27,8 @@ class DatabaseWrapper: conn_string += " password=%s" % DATABASE_PASSWORD if DATABASE_HOST: conn_string += " host=%s" % DATABASE_HOST + if DATABASE_PORT: + conn_string += " port=%s" % DATABASE_PORT self.connection = Database.connect(conn_string) self.connection.set_isolation_level(1) # make transactions transparent to all cursors cursor = self.connection.cursor() -- cgit v1.3 From ed1c9b20466324b085fb055b28c515ea3443e2a9 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 03:48:27 +0000 Subject: Fixed #622 -- Added default_if_none filter. Thanks, Eric git-svn-id: http://code.djangoproject.com/svn/django/trunk@859 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/defaultfilters.py | 6 ++++ docs/templates.txt | 84 ++++++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/django/core/defaultfilters.py b/django/core/defaultfilters.py index eb3ec11c95..42b46d03ea 100644 --- a/django/core/defaultfilters.py +++ b/django/core/defaultfilters.py @@ -333,6 +333,12 @@ def default(value, arg): "If value is unavailable, use given default" return value or arg +def default_if_none(value, arg): + "If value is None, use given default" + if value is None: + return arg + return value + def divisibleby(value, arg): "Returns true if the value is devisible by the argument" return int(value) % int(arg) == 0 diff --git a/docs/templates.txt b/docs/templates.txt index a6848a9638..15abaaedaa 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -376,9 +376,9 @@ Built-in tag reference ========================== ================================================ ``forloop.counter`` The current iteration of the loop (1-indexed) ``forloop.counter0`` The current iteration of the loop (0-indexed) - ``forloop.revcounter`` The number of iterations from the end of the + ``forloop.revcounter`` The number of iterations from the end of the loop (1-indexed) - ``forloop.revcounter0`` The number of iterations from the end of the + ``forloop.revcounter0`` The number of iterations from the end of the loop (0-indexed) ``forloop.first`` True if this is the first time through the loop ``forloop.last`` True if this is the last time through the loop @@ -569,25 +569,28 @@ Built-in filter reference ------------------------- ``add`` - Adds the arg to the value + Adds the arg to the value. ``addslashes`` - Adds slashes - useful for passing strings to JavaScript, for example. + Adds slashes. Useful for passing strings to JavaScript, for example. ``capfirst`` - Capitalizes the first character of the value + Capitalizes the first character of the value. ``center`` - Centers the value in a field of a given width + Centers the value in a field of a given width. ``cut`` - Removes all values of arg from the given string + Removes all values of arg from the given string. ``date`` - Formats a date according to the given format (same as the ``now`` tag) + Formats a date according to the given format (same as the ``now`` tag). ``default`` - If value is unavailable, use given default + If value is unavailable, use given default. + +``default_if_none`` + If value is ``None``, use given default. ``dictsort`` Takes a list of dicts, returns that list sorted by the property given in the @@ -598,24 +601,24 @@ Built-in filter reference given in the argument. ``divisibleby`` - Returns true if the value is divisible by the argument + Returns true if the value is divisible by the argument. ``escape`` - Escapes a string's HTML + Escapes a string's HTML. ``filesizeformat`` Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 bytes, etc). ``first`` - Returns the first item in a list + Returns the first item in a list. ``fix_ampersands`` - Replaces ampersands with ``&`` entities + Replaces ampersands with ``&`` entities. ``floatformat`` Displays a floating point number as 34.2 (with one decimal places) - but - only if there's a point to be displayed + only if there's a point to be displayed. ``get_digit`` Given a whole number, returns the requested digit of it, where 1 is the @@ -624,52 +627,52 @@ Built-in filter reference or if argument is less than 1). Otherwise, output is always an integer. ``join`` - Joins a list with a string, like Python's ``str.join(list)`` + Joins a list with a string, like Python's ``str.join(list)``. ``length`` - Returns the length of the value - useful for lists + Returns the length of the value. Useful for lists. ``length_is`` - Returns a boolean of whether the value's length is the argument + Returns a boolean of whether the value's length is the argument. ``linebreaks`` - Converts newlines into

and
s + Converts newlines into ``

`` and ``
``s. ``linebreaksbr`` - Converts newlines into
s + Converts newlines into ``
``s. ``linenumbers`` - Displays text with line numbers + Displays text with line numbers. ``ljust`` - Left-aligns the value in a field of a given width + Left-aligns the value in a field of a given width. **Argument:** field size ``lower`` - Converts a string into all lowercase + Converts a string into all lowercase. ``make_list`` Returns the value turned into a list. For an integer, it's a list of digits. For a string, it's a list of characters. ``phone2numeric`` - Takes a phone number and converts it in to its numerical equivalent + Takes a phone number and converts it in to its numerical equivalent. ``pluralize`` - Returns 's' if the value is not 1, for '1 vote' vs. '2 votes' + Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'. ``pprint`` - A wrapper around pprint.pprint -- for debugging, really + A wrapper around pprint.pprint -- for debugging, really. ``random`` - Returns a random item from the list + Returns a random item from the list. ``removetags`` - Removes a space separated list of [X]HTML tags from the output + Removes a space separated list of [X]HTML tags from the output. ``rjust`` - Right-aligns the value in a field of a given width + Right-aligns the value in a field of a given width. **Argument:** field size @@ -696,19 +699,19 @@ Built-in filter reference of Python string formatting ``striptags`` - Strips all [X]HTML tags + Strips all [X]HTML tags. ``time`` Formats a time according to the given format (same as the ``now`` tag). ``timesince`` - Formats a date as the time since that date (i.e. "4 days, 6 hours") + Formats a date as the time since that date (i.e. "4 days, 6 hours"). ``title`` - Converts a string into titlecase + Converts a string into titlecase. ``truncatewords`` - Truncates a string after a certain number of words + Truncates a string after a certain number of words. **Argument:** Number of words to truncate after @@ -733,26 +736,27 @@ Built-in filter reference ``upper`` - Converts a string into all uppercase + Converts a string into all uppercase. ``urlencode`` - Escapes a value for use in a URL + Escapes a value for use in a URL. ``urlize`` - Converts URLs in plain text into clickable links + Converts URLs in plain text into clickable links. ``urlizetrunc`` - Converts URLs into clickable links, truncating URLs to the given character limit + Converts URLs into clickable links, truncating URLs to the given character + limit. - **Argument:** Length to truncate URLs to. + **Argument:** Length to truncate URLs to ``wordcount`` - Returns the number of words + Returns the number of words. ``wordwrap`` - Wraps words at specified line length + Wraps words at specified line length. - **Argument:** number of words to wrap the text at. + **Argument:** number of words at which to wrap the text ``yesno`` Given a string mapping values for true, false and (optionally) None, -- cgit v1.3 From fdcc9da59c92dae4d17b0c0e71d4cc666ba759f5 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 14:05:46 +0000 Subject: Fixed typo in docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@860 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 257c3b0c2e..b718942a09 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -195,9 +195,9 @@ expects some level of code competence in template authors. Treat whitespace obviously -------------------------- -The template system do magic things with whitespace. If a template includes -whitespace, the system should treat the whitespace as it treats text -- just -display it. +The template system shouldn't do magic things with whitespace. If a template +includes whitespace, the system should treat the whitespace as it treats text +-- just display it. Don't invent a programming language ----------------------------------- -- cgit v1.3 From 42f8a069aa55a6edce316a1164800965c200ad65 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 14:06:49 +0000 Subject: Grammar fix to docs/design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@861 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index b718942a09..720694e7a3 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -208,7 +208,7 @@ The template system intentionally doesn't allow the following: * Advanced logic The goal is not to invent a programming language. The goal is to offer just -enough programming-esque functionality, such as branching and looping, that are +enough programming-esque functionality, such as branching and looping, that is essential for making presentation-related decisions. Extensibility -- cgit v1.3 From f2b8e85360ddc115ee2ed6ed781be260c0110150 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 15:02:44 +0000 Subject: Fixed ReST bugs in docs/templates.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@862 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/templates.txt b/docs/templates.txt index 15abaaedaa..843ed0cbaa 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -636,10 +636,10 @@ Built-in filter reference Returns a boolean of whether the value's length is the argument. ``linebreaks`` - Converts newlines into ``

`` and ``
``s. + Converts newlines into

and
s. ``linebreaksbr`` - Converts newlines into ``
``s. + Converts newlines into
s. ``linenumbers`` Displays text with line numbers. -- cgit v1.3 From 2db3d9c7ccf5e43713592cec4832fdbe4bc4b40f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 15:04:21 +0000 Subject: Reworded part of design_philosophies.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@863 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/design_philosophies.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design_philosophies.txt b/docs/design_philosophies.txt index 720694e7a3..2084c992a5 100644 --- a/docs/design_philosophies.txt +++ b/docs/design_philosophies.txt @@ -190,7 +190,7 @@ Assume designer competence The template system shouldn't be designed so that templates necessarily are displayed nicely in WYSIWYG editors such as Dreamweaver. That is too severe of a limitation and wouldn't allow the syntax to be as nice as it is. Django -expects some level of code competence in template authors. +expects template authors are comfortable editing HTML directly. Treat whitespace obviously -------------------------- -- cgit v1.3 From b773bf45c3011dc95617dcbec9584c8d139c27cc Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 14 Oct 2005 18:37:16 +0000 Subject: Registered default_if_none filter from [859] git-svn-id: http://code.djangoproject.com/svn/django/trunk@865 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/defaultfilters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/core/defaultfilters.py b/django/core/defaultfilters.py index 42b46d03ea..b9e0e67745 100644 --- a/django/core/defaultfilters.py +++ b/django/core/defaultfilters.py @@ -422,6 +422,7 @@ template.register_filter('center', center, True) template.register_filter('cut', cut, True) template.register_filter('date', date, True) template.register_filter('default', default, True) +template.register_filter('default_if_none', default_if_none, True) template.register_filter('dictsort', dictsort, True) template.register_filter('dictsortreversed', dictsortreversed, True) template.register_filter('divisibleby', divisibleby, True) -- cgit v1.3 From f71f8546283dbdf698c7578f8f9154045c84f9e7 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 20:10:13 +0000 Subject: Fixed #626 -- Moved template modules to django.core.template package. django.core.template_loader is deprecated, in favor of django.core.template.loader. git-svn-id: http://code.djangoproject.com/svn/django/trunk@867 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/defaultfilters.py | 465 ----------------- django/core/defaulttags.py | 770 ---------------------------- django/core/extensions.py | 5 +- django/core/rss.py | 13 +- django/core/template.py | 494 ------------------ django/core/template/__init__.py | 494 ++++++++++++++++++ django/core/template/defaultfilters.py | 465 +++++++++++++++++ django/core/template/defaulttags.py | 771 +++++++++++++++++++++++++++++ django/core/template/loader.py | 163 ++++++ django/core/template/loaders/__init__.py | 0 django/core/template/loaders/filesystem.py | 21 + django/core/template_file.py | 21 - django/core/template_loader.py | 167 +------ django/middleware/admin.py | 10 +- django/views/admin/doc.py | 3 +- django/views/admin/template.py | 9 +- django/views/auth/login.py | 2 +- django/views/defaults.py | 7 +- django/views/registration/passwords.py | 5 +- docs/forms.txt | 36 +- docs/sessions.txt | 4 +- docs/templates_python.txt | 10 +- docs/tutorial03.txt | 9 +- docs/tutorial04.txt | 4 +- tests/othertests/templates.py | 11 +- 25 files changed, 1970 insertions(+), 1989 deletions(-) delete mode 100644 django/core/defaultfilters.py delete mode 100644 django/core/defaulttags.py delete mode 100644 django/core/template.py create mode 100644 django/core/template/__init__.py create mode 100644 django/core/template/defaultfilters.py create mode 100644 django/core/template/defaulttags.py create mode 100644 django/core/template/loader.py create mode 100644 django/core/template/loaders/__init__.py create mode 100644 django/core/template/loaders/filesystem.py delete mode 100644 django/core/template_file.py diff --git a/django/core/defaultfilters.py b/django/core/defaultfilters.py deleted file mode 100644 index b9e0e67745..0000000000 --- a/django/core/defaultfilters.py +++ /dev/null @@ -1,465 +0,0 @@ -"Default variable filters" - -import template, re -import random as random_module - -################### -# STRINGS # -################### - -def addslashes(value, _): - "Adds slashes - useful for passing strings to JavaScript, for example." - return value.replace('"', '\\"').replace("'", "\\'") - -def capfirst(value, _): - "Capitalizes the first character of the value" - value = str(value) - return value and value[0].upper() + value[1:] - -def fix_ampersands(value, _): - "Replaces ampersands with ``&`` entities" - from django.utils.html import fix_ampersands - return fix_ampersands(value) - -def floatformat(text, _): - """ - Displays a floating point number as 34.2 (with one decimal place) - but - only if there's a point to be displayed - """ - from math import modf - if not text: - return '' - if modf(float(text))[0] < 0.1: - return text - return "%.1f" % float(text) - -def linenumbers(value, _): - "Displays text with line numbers" - from django.utils.html import escape - lines = value.split('\n') - # Find the maximum width of the line count, for use with zero padding string format command - width = str(len(str(len(lines)))) - for i, line in enumerate(lines): - lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) - return '\n'.join(lines) - -def lower(value, _): - "Converts a string into all lowercase" - return value.lower() - -def make_list(value, _): - """ - Returns the value turned into a list. For an integer, it's a list of - digits. For a string, it's a list of characters. - """ - return list(str(value)) - -def slugify(value, _): - "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" - value = re.sub('[^\w\s-]', '', value).strip().lower() - return re.sub('\s+', '-', value) - -def stringformat(value, arg): - """ - Formats the variable according to the argument, a string formatting specifier. - This specifier uses Python string formating syntax, with the exception that - the leading "%" is dropped. - - See http://docs.python.org/lib/typesseq-strings.html for documentation - of Python string formatting - """ - try: - return ("%" + arg) % value - except (ValueError, TypeError): - return "" - -def title(value, _): - "Converts a string into titlecase" - return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) - -def truncatewords(value, arg): - """ - Truncates a string after a certain number of words - - Argument: Number of words to truncate after - """ - from django.utils.text import truncate_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_words(value, length) - -def upper(value, _): - "Converts a string into all uppercase" - return value.upper() - -def urlencode(value, _): - "Escapes a value for use in a URL" - import urllib - return urllib.quote(value) - -def urlize(value, _): - "Converts URLs in plain text into clickable links" - from django.utils.html import urlize - return urlize(value, nofollow=True) - -def urlizetrunc(value, limit): - """ - Converts URLs into clickable links, truncating URLs to the given character limit - - Argument: Length to truncate URLs to. - """ - from django.utils.html import urlize - return urlize(value, trim_url_limit=int(limit), nofollow=True) - -def wordcount(value, _): - "Returns the number of words" - return len(value.split()) - -def wordwrap(value, arg): - """ - Wraps words at specified line length - - Argument: number of words to wrap the text at. - """ - from django.utils.text import wrap - return wrap(value, int(arg)) - -def ljust(value, arg): - """ - Left-aligns the value in a field of a given width - - Argument: field size - """ - return str(value).ljust(int(arg)) - -def rjust(value, arg): - """ - Right-aligns the value in a field of a given width - - Argument: field size - """ - return str(value).rjust(int(arg)) - -def center(value, arg): - "Centers the value in a field of a given width" - return str(value).center(int(arg)) - -def cut(value, arg): - "Removes all values of arg from the given string" - return value.replace(arg, '') - -################### -# HTML STRINGS # -################### - -def escape(value, _): - "Escapes a string's HTML" - from django.utils.html import escape - return escape(value) - -def linebreaks(value, _): - "Converts newlines into

and
s" - from django.utils.html import linebreaks - return linebreaks(value) - -def linebreaksbr(value, _): - "Converts newlines into
s" - return value.replace('\n', '
') - -def removetags(value, tags): - "Removes a space separated list of [X]HTML tags from the output" - tags = [re.escape(tag) for tag in tags.split()] - tags_re = '(%s)' % '|'.join(tags) - starttag_re = re.compile('<%s(>|(\s+[^>]*>))' % tags_re) - endtag_re = re.compile('' % tags_re) - value = starttag_re.sub('', value) - value = endtag_re.sub('', value) - return value - -def striptags(value, _): - "Strips all [X]HTML tags" - from django.utils.html import strip_tags - if not isinstance(value, basestring): - value = str(value) - return strip_tags(value) - -################### -# LISTS # -################### - -def dictsort(value, arg): - """ - Takes a list of dicts, returns that list sorted by the property given in - the argument. - """ - decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value] - decorated.sort() - return [item[1] for item in decorated] - -def dictsortreversed(value, arg): - """ - Takes a list of dicts, returns that list sorted in reverse order by the - property given in the argument. - """ - decorated = [(template.resolve_variable('var.' + arg, {'var' : item}), item) for item in value] - decorated.sort() - decorated.reverse() - return [item[1] for item in decorated] - -def first(value, _): - "Returns the first item in a list" - try: - return value[0] - except IndexError: - return '' - -def join(value, arg): - "Joins a list with a string, like Python's ``str.join(list)``" - try: - return arg.join(map(str, value)) - except AttributeError: # fail silently but nicely - return value - -def length(value, _): - "Returns the length of the value - useful for lists" - return len(value) - -def length_is(value, arg): - "Returns a boolean of whether the value's length is the argument" - return len(value) == int(arg) - -def random(value, _): - "Returns a random item from the list" - return random_module.choice(value) - -def slice_(value, arg): - """ - Returns a slice of the list. - - Uses the same syntax as Python's list slicing; see - http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice - for an introduction. - """ - try: - return value[slice(*[x and int(x) or None for x in arg.split(':')])] - except (ValueError, TypeError): - return value # Fail silently. - -def unordered_list(value, _): - """ - Recursively takes a self-nested list and returns an HTML unordered list -- - WITHOUT opening and closing

    tags. - - The list is assumed to be in the proper format. For example, if ``var`` contains - ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``, - then ``{{ var|unordered_list }}`` would return:: - -
  • States -
      -
    • Kansas -
        -
      • Lawrence
      • -
      • Topeka
      • -
      -
    • -
    • Illinois
    • -
    -
  • - """ - def _helper(value, tabs): - indent = '\t' * tabs - if value[1]: - return '%s
  • %s\n%s
      \n%s\n%s
    \n%s
  • ' % (indent, value[0], indent, - '\n'.join([unordered_list(v, tabs+1) for v in value[1]]), indent, indent) - else: - return '%s
  • %s
  • ' % (indent, value[0]) - return _helper(value, 1) - -################### -# INTEGERS # -################### - -def add(value, arg): - "Adds the arg to the value" - return int(value) + int(arg) - -def get_digit(value, arg): - """ - Given a whole number, returns the requested digit of it, where 1 is the - right-most digit, 2 is the second-right-most digit, etc. Returns the - original value for invalid input (if input or argument is not an integer, - or if argument is less than 1). Otherwise, output is always an integer. - """ - try: - arg = int(arg) - value = int(value) - except ValueError: - return value # Fail silently for an invalid argument - if arg < 1: - return value - try: - return int(str(value)[-arg]) - except IndexError: - return 0 - -################### -# DATES # -################### - -def date(value, arg): - "Formats a date according to the given format" - from django.utils.dateformat import format - return format(value, arg) - -def time(value, arg): - "Formats a time according to the given format" - from django.utils.dateformat import time_format - return time_format(value, arg) - -def timesince(value, _): - 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' - from django.utils.timesince import timesince - return timesince(value) - -################### -# LOGIC # -################### - -def default(value, arg): - "If value is unavailable, use given default" - return value or arg - -def default_if_none(value, arg): - "If value is None, use given default" - if value is None: - return arg - return value - -def divisibleby(value, arg): - "Returns true if the value is devisible by the argument" - return int(value) % int(arg) == 0 - -def yesno(value, arg): - """ - Given a string mapping values for true, false and (optionally) None, - returns one of those strings accoding to the value: - - ========== ====================== ================================== - Value Argument Outputs - ========== ====================== ================================== - ``True`` ``"yeah,no,maybe"`` ``yeah`` - ``False`` ``"yeah,no,maybe"`` ``no`` - ``None`` ``"yeah,no,maybe"`` ``maybe`` - ``None`` ``"yeah,no"`` ``"no"`` (converts None to False - if no mapping for None is given. - ========== ====================== ================================== - """ - bits = arg.split(',') - if len(bits) < 2: - return value # Invalid arg. - try: - yes, no, maybe = bits - except ValueError: # unpack list of wrong size (no "maybe" value provided) - yes, no, maybe = bits[0], bits[1], bits[1] - if value is None: - return maybe - if value: - return yes - return no - -################### -# MISC # -################### - -def filesizeformat(bytes, _): - """ - Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 - bytes, etc). - """ - bytes = float(bytes) - if bytes < 1024: - return "%d byte%s" % (bytes, bytes != 1 and 's' or '') - if bytes < 1024 * 1024: - return "%.1f KB" % (bytes / 1024) - if bytes < 1024 * 1024 * 1024: - return "%.1f MB" % (bytes / (1024 * 1024)) - return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) - -def pluralize(value, _): - "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'" - try: - if int(value) != 1: - return 's' - except ValueError: # invalid string that's not a number - pass - except TypeError: # value isn't a string or a number; maybe it's a list? - try: - if len(value) != 1: - return 's' - except TypeError: # len() of unsized object - pass - return '' - -def phone2numeric(value, _): - "Takes a phone number and converts it in to its numerical equivalent" - from django.utils.text import phone2numeric - return phone2numeric(value) - -def pprint(value, _): - "A wrapper around pprint.pprint -- for debugging, really" - from pprint import pformat - return pformat(value) - -# Syntax: template.register_filter(name of filter, callback, has_argument) -template.register_filter('add', add, True) -template.register_filter('addslashes', addslashes, False) -template.register_filter('capfirst', capfirst, False) -template.register_filter('center', center, True) -template.register_filter('cut', cut, True) -template.register_filter('date', date, True) -template.register_filter('default', default, True) -template.register_filter('default_if_none', default_if_none, True) -template.register_filter('dictsort', dictsort, True) -template.register_filter('dictsortreversed', dictsortreversed, True) -template.register_filter('divisibleby', divisibleby, True) -template.register_filter('escape', escape, False) -template.register_filter('filesizeformat', filesizeformat, False) -template.register_filter('first', first, False) -template.register_filter('fix_ampersands', fix_ampersands, False) -template.register_filter('floatformat', floatformat, False) -template.register_filter('get_digit', get_digit, True) -template.register_filter('join', join, True) -template.register_filter('length', length, False) -template.register_filter('length_is', length_is, True) -template.register_filter('linebreaks', linebreaks, False) -template.register_filter('linebreaksbr', linebreaksbr, False) -template.register_filter('linenumbers', linenumbers, False) -template.register_filter('ljust', ljust, True) -template.register_filter('lower', lower, False) -template.register_filter('make_list', make_list, False) -template.register_filter('phone2numeric', phone2numeric, False) -template.register_filter('pluralize', pluralize, False) -template.register_filter('pprint', pprint, False) -template.register_filter('removetags', removetags, True) -template.register_filter('random', random, False) -template.register_filter('rjust', rjust, True) -template.register_filter('slice', slice_, True) -template.register_filter('slugify', slugify, False) -template.register_filter('stringformat', stringformat, True) -template.register_filter('striptags', striptags, False) -template.register_filter('time', time, True) -template.register_filter('timesince', timesince, False) -template.register_filter('title', title, False) -template.register_filter('truncatewords', truncatewords, True) -template.register_filter('unordered_list', unordered_list, False) -template.register_filter('upper', upper, False) -template.register_filter('urlencode', urlencode, False) -template.register_filter('urlize', urlize, False) -template.register_filter('urlizetrunc', urlizetrunc, True) -template.register_filter('wordcount', wordcount, False) -template.register_filter('wordwrap', wordwrap, True) -template.register_filter('yesno', yesno, True) diff --git a/django/core/defaulttags.py b/django/core/defaulttags.py deleted file mode 100644 index e86b385f9c..0000000000 --- a/django/core/defaulttags.py +++ /dev/null @@ -1,770 +0,0 @@ -"Default tags used by the template system, available to all templates." - -import sys -import template - -class CommentNode(template.Node): - def render(self, context): - return '' - -class CycleNode(template.Node): - def __init__(self, cyclevars): - self.cyclevars = cyclevars - self.cyclevars_len = len(cyclevars) - self.counter = -1 - - def render(self, context): - self.counter += 1 - return self.cyclevars[self.counter % self.cyclevars_len] - -class DebugNode(template.Node): - def render(self, context): - from pprint import pformat - output = [pformat(val) for val in context] - output.append('\n\n') - output.append(pformat(sys.modules)) - return ''.join(output) - -class FilterNode(template.Node): - def __init__(self, filters, nodelist): - self.filters, self.nodelist = filters, nodelist - - def render(self, context): - output = self.nodelist.render(context) - # apply filters - for f in self.filters: - output = template.registered_filters[f[0]][0](output, f[1]) - return output - -class FirstOfNode(template.Node): - def __init__(self, vars): - self.vars = vars - - def render(self, context): - for var in self.vars: - value = template.resolve_variable(var, context) - if value: - return str(value) - return '' - -class ForNode(template.Node): - def __init__(self, loopvar, sequence, reversed, nodelist_loop): - self.loopvar, self.sequence = loopvar, sequence - self.reversed = reversed - self.nodelist_loop = nodelist_loop - - def __repr__(self): - if self.reversed: - reversed = ' reversed' - else: - reversed = '' - return "" % \ - (self.loopvar, self.sequence, len(self.nodelist_loop), reversed) - - def __iter__(self): - for node in self.nodelist_loop: - yield node - - def get_nodes_by_type(self, nodetype): - nodes = [] - if isinstance(self, nodetype): - nodes.append(self) - nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) - return nodes - - def render(self, context): - nodelist = template.NodeList() - if context.has_key('forloop'): - parentloop = context['forloop'] - else: - parentloop = {} - context.push() - try: - values = template.resolve_variable_with_filters(self.sequence, context) - except template.VariableDoesNotExist: - values = [] - if values is None: - values = [] - len_values = len(values) - if self.reversed: - # From http://www.python.org/doc/current/tut/node11.html - def reverse(data): - for index in range(len(data)-1, -1, -1): - yield data[index] - values = reverse(values) - for i, item in enumerate(values): - context['forloop'] = { - # shortcuts for current loop iteration number - 'counter0': i, - 'counter': i+1, - # reverse counter iteration numbers - 'revcounter': len_values - i, - 'revcounter0': len_values - i - 1, - # boolean values designating first and last times through loop - 'first': (i == 0), - 'last': (i == len_values - 1), - 'parentloop': parentloop, - } - context[self.loopvar] = item - for node in self.nodelist_loop: - nodelist.append(node.render(context)) - context.pop() - return nodelist.render(context) - -class IfChangedNode(template.Node): - def __init__(self, nodelist): - self.nodelist = nodelist - self._last_seen = None - - def render(self, context): - content = self.nodelist.render(context) - if content != self._last_seen: - firstloop = (self._last_seen == None) - self._last_seen = content - context.push() - context['ifchanged'] = {'firstloop': firstloop} - content = self.nodelist.render(context) - context.pop() - return content - else: - return '' - -class IfEqualNode(template.Node): - def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): - self.var1, self.var2 = var1, var2 - self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false - self.negate = negate - - def __repr__(self): - return "" - - def render(self, context): - val1 = template.resolve_variable(self.var1, context) - val2 = template.resolve_variable(self.var2, context) - if (self.negate and val1 != val2) or (not self.negate and val1 == val2): - return self.nodelist_true.render(context) - return self.nodelist_false.render(context) - -class IfNode(template.Node): - def __init__(self, boolvars, nodelist_true, nodelist_false): - self.boolvars = boolvars - self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false - - def __repr__(self): - return "" - - def __iter__(self): - for node in self.nodelist_true: - yield node - for node in self.nodelist_false: - yield node - - def get_nodes_by_type(self, nodetype): - nodes = [] - if isinstance(self, nodetype): - nodes.append(self) - nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype)) - nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) - return nodes - - def render(self, context): - for ifnot, boolvar in self.boolvars: - try: - value = template.resolve_variable_with_filters(boolvar, context) - except template.VariableDoesNotExist: - value = None - if (value and not ifnot) or (ifnot and not value): - return self.nodelist_true.render(context) - return self.nodelist_false.render(context) - -class RegroupNode(template.Node): - def __init__(self, target_var, expression, var_name): - self.target_var, self.expression = target_var, expression - self.var_name = var_name - - def render(self, context): - obj_list = template.resolve_variable_with_filters(self.target_var, context) - if obj_list == '': # target_var wasn't found in context; fail silently - context[self.var_name] = [] - return '' - output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} - for obj in obj_list: - grouper = template.resolve_variable_with_filters('var.%s' % self.expression, \ - template.Context({'var': obj})) - if output and repr(output[-1]['grouper']) == repr(grouper): - output[-1]['list'].append(obj) - else: - output.append({'grouper': grouper, 'list': [obj]}) - context[self.var_name] = output - return '' - -def include_is_allowed(filepath): - from django.conf.settings import ALLOWED_INCLUDE_ROOTS - for root in ALLOWED_INCLUDE_ROOTS: - if filepath.startswith(root): - return True - return False - -class SsiNode(template.Node): - def __init__(self, filepath, parsed): - self.filepath, self.parsed = filepath, parsed - - def render(self, context): - if not include_is_allowed(self.filepath): - return '' # Fail silently for invalid includes. - try: - fp = open(self.filepath, 'r') - output = fp.read() - fp.close() - except IOError: - output = '' - if self.parsed: - try: - t = template.Template(output) - return t.render(context) - except template.TemplateSyntaxError: - return '' # Fail silently for invalid included templates. - return output - -class LoadNode(template.Node): - def __init__(self, taglib): - self.taglib = taglib - - def load_taglib(taglib): - mod = __import__("django.templatetags.%s" % taglib.split('.')[-1], '', '', ['']) - reload(mod) - return mod - load_taglib = staticmethod(load_taglib) - - def render(self, context): - "Import the relevant module" - try: - self.__class__.load_taglib(self.taglib) - except ImportError: - pass # Fail silently for invalid loads. - return '' - -class NowNode(template.Node): - def __init__(self, format_string): - self.format_string = format_string - - def render(self, context): - from datetime import datetime - from django.utils.dateformat import DateFormat - df = DateFormat(datetime.now()) - return df.format(self.format_string) - -class TemplateTagNode(template.Node): - mapping = {'openblock': template.BLOCK_TAG_START, - 'closeblock': template.BLOCK_TAG_END, - 'openvariable': template.VARIABLE_TAG_START, - 'closevariable': template.VARIABLE_TAG_END} - - def __init__(self, tagtype): - self.tagtype = tagtype - - def render(self, context): - return self.mapping.get(self.tagtype, '') - -class WidthRatioNode(template.Node): - def __init__(self, val_var, max_var, max_width): - self.val_var = val_var - self.max_var = max_var - self.max_width = max_width - - def render(self, context): - try: - value = template.resolve_variable_with_filters(self.val_var, context) - maxvalue = template.resolve_variable_with_filters(self.max_var, context) - except template.VariableDoesNotExist: - return '' - try: - value = float(value) - maxvalue = float(maxvalue) - ratio = (value / maxvalue) * int(self.max_width) - except (ValueError, ZeroDivisionError): - return '' - return str(int(round(ratio))) - -def do_comment(parser, token): - """ - Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` - """ - nodelist = parser.parse(('endcomment',)) - parser.delete_first_token() - return CommentNode() - -def do_cycle(parser, token): - """ - Cycle among the given strings each time this tag is encountered - - Within a loop, cycles among the given strings each time through - the loop:: - - {% for o in some_list %} - - ... - - {% endfor %} - - Outside of a loop, give the values a unique name the first time you call - it, then use that name each sucessive time through:: - - ... - ... - ... - - You can use any number of values, seperated by commas. Make sure not to - put spaces between the values -- only commas. - """ - - # Note: This returns the exact same node on each {% cycle name %} call; that - # is, the node object returned from {% cycle a,b,c as name %} and the one - # returned from {% cycle name %} are the exact same object. This shouldn't - # cause problems (heh), but if it does, now you know. - # - # Ugly hack warning: this stuffs the named template dict into parser so - # that names are only unique within each template (as opposed to using - # a global variable, which would make cycle names have to be unique across - # *all* templates. - - args = token.contents.split() - if len(args) < 2: - raise template.TemplateSyntaxError("'Cycle' statement requires at least two arguments") - - elif len(args) == 2 and "," in args[1]: - # {% cycle a,b,c %} - cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks - return CycleNode(cyclevars) - # {% cycle name %} - - elif len(args) == 2: - name = args[1] - if not parser._namedCycleNodes.has_key(name): - raise template.TemplateSyntaxError("Named cycle '%s' does not exist" % name) - return parser._namedCycleNodes[name] - - elif len(args) == 4: - # {% cycle a,b,c as name %} - if args[2] != 'as': - raise template.TemplateSyntaxError("Second 'cycle' argument must be 'as'") - cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks - name = args[3] - node = CycleNode(cyclevars) - - if not hasattr(parser, '_namedCycleNodes'): - parser._namedCycleNodes = {} - - parser._namedCycleNodes[name] = node - return node - - else: - raise template.TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) - -def do_debug(parser, token): - "Print a whole load of debugging information, including the context and imported modules" - return DebugNode() - -def do_filter(parser, token): - """ - Filter the contents of the blog through variable filters. - - Filters can also be piped through each other, and they can have - arguments -- just like in variable syntax. - - Sample usage:: - - {% filter escape|lower %} - This text will be HTML-escaped, and will appear in lowercase. - {% endfilter %} - """ - _, rest = token.contents.split(None, 1) - _, filters = template.get_filters_from_token('var|%s' % rest) - nodelist = parser.parse(('endfilter',)) - parser.delete_first_token() - return FilterNode(filters, nodelist) - -def do_firstof(parser, token): - """ - Outputs the first variable passed that is not False. - - Outputs nothing if all the passed variables are False. - - Sample usage:: - - {% firstof var1 var2 var3 %} - - This is equivalent to:: - - {% if var1 %} - {{ var1 }} - {% else %}{% if var2 %} - {{ var2 }} - {% else %}{% if var3 %} - {{ var3 }} - {% endif %}{% endif %}{% endif %} - - but obviously much cleaner! - """ - bits = token.contents.split()[1:] - if len(bits) < 1: - raise template.TemplateSyntaxError, "'firstof' statement requires at least one argument" - return FirstOfNode(bits) - - -def do_for(parser, token): - """ - Loop over each item in an array. - - For example, to display a list of athletes given ``athlete_list``:: - -
      - {% for athlete in athlete_list %} -
    • {{ athlete.name }}
    • - {% endfor %} -
    - - You can also loop over a list in reverse by using - ``{% for obj in list reversed %}``. - - The for loop sets a number of variables available within the loop: - - ========================== ================================================ - Variable Description - ========================== ================================================ - ``forloop.counter`` The current iteration of the loop (1-indexed) - ``forloop.counter0`` The current iteration of the loop (0-indexed) - ``forloop.revcounter`` The number of iterations from the end of the - loop (1-indexed) - ``forloop.revcounter0`` The number of iterations from the end of the - loop (0-indexed) - ``forloop.first`` True if this is the first time through the loop - ``forloop.last`` True if this is the last time through the loop - ``forloop.parentloop`` For nested loops, this is the loop "above" the - current one - ========================== ================================================ - - """ - bits = token.contents.split() - if len(bits) == 5 and bits[4] != 'reversed': - raise template.TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents - if len(bits) not in (4, 5): - raise template.TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents - if bits[2] != 'in': - raise template.TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents - loopvar = bits[1] - sequence = bits[3] - reversed = (len(bits) == 5) - nodelist_loop = parser.parse(('endfor',)) - parser.delete_first_token() - return ForNode(loopvar, sequence, reversed, nodelist_loop) - -def do_ifequal(parser, token, negate): - """ - Output the contents of the block if the two arguments equal/don't equal each other. - - Examples:: - - {% ifequal user.id comment.user_id %} - ... - {% endifequal %} - - {% ifnotequal user.id comment.user_id %} - ... - {% else %} - ... - {% endifnotequal %} - """ - bits = token.contents.split() - if len(bits) != 3: - raise template.TemplateSyntaxError, "%r takes two arguments" % bits[0] - end_tag = 'end' + bits[0] - nodelist_true = parser.parse(('else', end_tag)) - token = parser.next_token() - if token.contents == 'else': - nodelist_false = parser.parse((end_tag,)) - parser.delete_first_token() - else: - nodelist_false = template.NodeList() - return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) - -def do_if(parser, token): - """ - The ``{% if %}`` tag evaluates a variable, and if that variable is "true" - (i.e. exists, is not empty, and is not a false boolean value) the contents - of the block are output: - - :: - - {% if althlete_list %} - Number of athletes: {{ althete_list|count }} - {% else %} - No athletes. - {% endif %} - - In the above, if ``athlete_list`` is not empty, the number of athletes will - be displayed by the ``{{ athlete_list|count }}`` variable. - - As you can see, the ``if`` tag can take an option ``{% else %}`` clause that - will be displayed if the test fails. - - ``if`` tags may use ``or`` or ``not`` to test a number of variables or to - negate a given variable:: - - {% if not athlete_list %} - There are no athletes. - {% endif %} - - {% if athlete_list or coach_list %} - There are some athletes or some coaches. - {% endif %} - - {% if not athlete_list or coach_list %} - There are no athletes or there are some coaches (OK, so - writing English translations of boolean logic sounds - stupid; it's not my fault). - {% endif %} - - For simplicity, ``if`` tags do not allow ``and`` clauses; use nested ``if`` - tags instead:: - - {% if athlete_list %} - {% if coach_list %} - Number of athletes: {{ athlete_list|count }}. - Number of coaches: {{ coach_list|count }}. - {% endif %} - {% endif %} - """ - bits = token.contents.split() - del bits[0] - if not bits: - raise template.TemplateSyntaxError, "'if' statement requires at least one argument" - # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] - boolpairs = ' '.join(bits).split(' or ') - boolvars = [] - for boolpair in boolpairs: - if ' ' in boolpair: - not_, boolvar = boolpair.split() - if not_ != 'not': - raise template.TemplateSyntaxError, "Expected 'not' in if statement" - boolvars.append((True, boolvar)) - else: - boolvars.append((False, boolpair)) - nodelist_true = parser.parse(('else', 'endif')) - token = parser.next_token() - if token.contents == 'else': - nodelist_false = parser.parse(('endif',)) - parser.delete_first_token() - else: - nodelist_false = template.NodeList() - return IfNode(boolvars, nodelist_true, nodelist_false) - -def do_ifchanged(parser, token): - """ - Check if a value has changed from the last iteration of a loop. - - The 'ifchanged' block tag is used within a loop. It checks its own rendered - contents against its previous state and only displays its content if the - value has changed:: - -

    Archive for {{ year }}

    - - {% for date in days %} - {% ifchanged %}

    {{ date|date:"F" }}

    {% endifchanged %} - {{ date|date:"j" }} - {% endfor %} - """ - bits = token.contents.split() - if len(bits) != 1: - raise template.TemplateSyntaxError, "'ifchanged' tag takes no arguments" - nodelist = parser.parse(('endifchanged',)) - parser.delete_first_token() - return IfChangedNode(nodelist) - -def do_ssi(parser, token): - """ - Output the contents of a given file into the page. - - Like a simple "include" tag, the ``ssi`` tag includes the contents - of another file -- which must be specified using an absolute page -- - in the current page:: - - {% ssi /home/html/ljworld.com/includes/right_generic.html %} - - If the optional "parsed" parameter is given, the contents of the included - file are evaluated as template code, with the current context:: - - {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} - """ - bits = token.contents.split() - parsed = False - if len(bits) not in (2, 3): - raise template.TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" - if len(bits) == 3: - if bits[2] == 'parsed': - parsed = True - else: - raise template.TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] - return SsiNode(bits[1], parsed) - -def do_load(parser, token): - """ - Load a custom template tag set. - - For example, to load the template tags in ``django/templatetags/news/photos.py``:: - - {% load news.photos %} - """ - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "'load' statement takes one argument" - taglib = bits[1] - # check at compile time that the module can be imported - try: - LoadNode.load_taglib(taglib) - except ImportError: - raise template.TemplateSyntaxError, "'%s' is not a valid tag library" % taglib - return LoadNode(taglib) - -def do_now(parser, token): - """ - Display the date, formatted according to the given string. - - Uses the same format as PHP's ``date()`` function; see http://php.net/date - for all the possible values. - - Sample usage:: - - It is {% now "jS F Y H:i" %} - """ - bits = token.contents.split('"') - if len(bits) != 3: - raise template.TemplateSyntaxError, "'now' statement takes one argument" - format_string = bits[1] - return NowNode(format_string) - -def do_regroup(parser, token): - """ - Regroup a list of alike objects by a common attribute. - - This complex tag is best illustrated by use of an example: say that - ``people`` is a list of ``Person`` objects that have ``first_name``, - ``last_name``, and ``gender`` attributes, and you'd like to display a list - that looks like: - - * Male: - * George Bush - * Bill Clinton - * Female: - * Margaret Thatcher - * Colendeeza Rice - * Unknown: - * Pat Smith - - The following snippet of template code would accomplish this dubious task:: - - {% regroup people by gender as grouped %} -
      - {% for group in grouped %} -
    • {{ group.grouper }} -
        - {% for item in group.list %} -
      • {{ item }}
      • - {% endfor %} -
      - {% endfor %} -
    - - As you can see, ``{% regroup %}`` populates a variable with a list of - objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the - item that was grouped by; ``list`` contains the list of objects that share - that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female`` - and ``Unknown``, and ``list`` is the list of people with those genders. - - Note that `{% regroup %}`` does not work when the list to be grouped is not - sorted by the key you are grouping by! This means that if your list of - people was not sorted by gender, you'd need to make sure it is sorted before - using it, i.e.:: - - {% regroup people|dictsort:"gender" by gender as grouped %} - - """ - firstbits = token.contents.split(None, 3) - if len(firstbits) != 4: - raise template.TemplateSyntaxError, "'regroup' tag takes five arguments" - target_var = firstbits[1] - if firstbits[2] != 'by': - raise template.TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" - lastbits_reversed = firstbits[3][::-1].split(None, 2) - if lastbits_reversed[1][::-1] != 'as': - raise template.TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" - expression = lastbits_reversed[2][::-1] - var_name = lastbits_reversed[0][::-1] - return RegroupNode(target_var, expression, var_name) - -def do_templatetag(parser, token): - """ - Output one of the bits used to compose template tags. - - Since the template system has no concept of "escaping", to display one of - the bits used in template tags, you must use the ``{% templatetag %}`` tag. - - The argument tells which template bit to output: - - ================== ======= - Argument Outputs - ================== ======= - ``openblock`` ``{%`` - ``closeblock`` ``%}`` - ``openvariable`` ``{{`` - ``closevariable`` ``}}`` - ================== ======= - """ - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "'templatetag' statement takes one argument" - tag = bits[1] - if not TemplateTagNode.mapping.has_key(tag): - raise template.TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ - (tag, TemplateTagNode.mapping.keys()) - return TemplateTagNode(tag) - -def do_widthratio(parser, token): - """ - For creating bar charts and such, this tag calculates the ratio of a given - value to a maximum value, and then applies that ratio to a constant. - - For example:: - - - - Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in - the above example will be 88 pixels wide (because 175/200 = .875; .875 * - 100 = 87.5 which is rounded up to 88). - """ - bits = token.contents.split() - if len(bits) != 4: - raise template.TemplateSyntaxError("widthratio takes three arguments") - tag, this_value_var, max_value_var, max_width = bits - try: - max_width = int(max_width) - except ValueError: - raise template.TemplateSyntaxError("widthratio final argument must be an integer") - return WidthRatioNode(this_value_var, max_value_var, max_width) - -template.register_tag('comment', do_comment) -template.register_tag('cycle', do_cycle) -template.register_tag('debug', do_debug) -template.register_tag('filter', do_filter) -template.register_tag('firstof', do_firstof) -template.register_tag('for', do_for) -template.register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False)) -template.register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True)) -template.register_tag('if', do_if) -template.register_tag('ifchanged', do_ifchanged) -template.register_tag('regroup', do_regroup) -template.register_tag('ssi', do_ssi) -template.register_tag('load', do_load) -template.register_tag('now', do_now) -template.register_tag('templatetag', do_templatetag) -template.register_tag('widthratio', do_widthratio) diff --git a/django/core/extensions.py b/django/core/extensions.py index 6909ace33d..a7eaf3b06f 100644 --- a/django/core/extensions.py +++ b/django/core/extensions.py @@ -2,14 +2,13 @@ # of MVC. In other words, these functions/classes introduce controlled coupling # for convenience's sake. -from django.core import template_loader from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.template import Context +from django.core.template import Context, loader from django.conf.settings import DEBUG, INTERNAL_IPS from django.utils.httpwrappers import HttpResponse def render_to_response(*args, **kwargs): - return HttpResponse(template_loader.render_to_string(*args, **kwargs)) + return HttpResponse(loader.render_to_string(*args, **kwargs)) load_and_render = render_to_response # For backwards compatibility. def get_object_or_404(mod, **kwargs): diff --git a/django/core/rss.py b/django/core/rss.py index cd2bcf4ad1..7563d2ea82 100644 --- a/django/core/rss.py +++ b/django/core/rss.py @@ -1,6 +1,5 @@ -from django.core import template_loader from django.core.exceptions import ObjectDoesNotExist -from django.core.template import Context +from django.core.template import Context, loader from django.models.core import sites from django.utils import feedgenerator from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE @@ -28,7 +27,7 @@ class FeedConfiguration: get_list_kwargs_cb -- Function that takes the param and returns a dictionary to use in addition to get_list_kwargs (if applicable). - + get_pubdate_cb -- Function that takes the object and returns a datetime to use as the publication date in the feed. @@ -49,7 +48,7 @@ class FeedConfiguration: self.enc_url = enc_url self.enc_length = enc_length self.enc_mime_type = enc_mime_type - + def get_feed(self, param_slug=None): """ Returns a utils.feedgenerator.DefaultRssFeed object, fully populated, @@ -64,8 +63,8 @@ class FeedConfiguration: param = None current_site = sites.get_current() f = self._get_feed_generator_object(param) - title_template = template_loader.get_template('rss/%s_title' % self.slug) - description_template = template_loader.get_template('rss/%s_description' % self.slug) + title_template = loader.get_template('rss/%s_title' % self.slug) + description_template = loader.get_template('rss/%s_description' % self.slug) kwargs = self.get_list_kwargs.copy() if param and self.get_list_kwargs_cb: kwargs.update(self.get_list_kwargs_cb(param)) @@ -102,7 +101,7 @@ class FeedConfiguration: pubdate = self.get_pubdate_cb and self.get_pubdate_cb(obj) or None, ) return f - + def _get_feed_generator_object(self, param): current_site = sites.get_current() link = self.link_cb(param).decode() diff --git a/django/core/template.py b/django/core/template.py deleted file mode 100644 index 71a8e621c8..0000000000 --- a/django/core/template.py +++ /dev/null @@ -1,494 +0,0 @@ -""" -This is the Django template system. - -How it works: - -The tokenize() function converts a template string (i.e., a string containing -markup with custom template tags) to tokens, which can be either plain text -(TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK). - -The Parser() class takes a list of tokens in its constructor, and its parse() -method returns a compiled template -- which is, under the hood, a list of -Node objects. - -Each Node is responsible for creating some sort of output -- e.g. simple text -(TextNode), variable values in a given context (VariableNode), results of basic -logic (IfNode), results of looping (ForNode), or anything else. The core Node -types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can -define their own custom node types. - -Each Node has a render() method, which takes a Context and returns a string of -the rendered node. For example, the render() method of a Variable Node returns -the variable's value as a string. The render() method of an IfNode returns the -rendered output of whatever was inside the loop, recursively. - -The Template class is a convenient wrapper that takes care of template -compilation and rendering. - -Usage: - -The only thing you should ever use directly in this file is the Template class. -Create a compiled template object with a template_string, then call render() -with a context. In the compilation stage, the TemplateSyntaxError exception -will be raised if the template doesn't have proper syntax. - -Sample code: - ->>> import template ->>> s = ''' -... -... {% if test %} -...

    {{ varvalue }}

    -... {% endif %} -... -... ''' ->>> t = template.Template(s) - -(t is now a compiled template, and its render() method can be called multiple -times with multiple contexts) - ->>> c = template.Context({'test':True, 'varvalue': 'Hello'}) ->>> t.render(c) -'\n\n\n

    Hello

    \n\n\n' ->>> c = template.Context({'test':False, 'varvalue': 'Hello'}) ->>> t.render(c) -'\n\n\n\n' -""" -import re -from django.conf.settings import DEFAULT_CHARSET - -__all__ = ('Template','Context','compile_string') - -TOKEN_TEXT = 0 -TOKEN_VAR = 1 -TOKEN_BLOCK = 2 - -# template syntax constants -FILTER_SEPARATOR = '|' -FILTER_ARGUMENT_SEPARATOR = ':' -VARIABLE_ATTRIBUTE_SEPARATOR = '.' -BLOCK_TAG_START = '{%' -BLOCK_TAG_END = '%}' -VARIABLE_TAG_START = '{{' -VARIABLE_TAG_END = '}}' - -ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' - -# match a variable or block tag and capture the entire tag, including start/end delimiters -tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), - re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) - -# global dict used by register_tag; maps custom tags to callback functions -registered_tags = {} - -# global dict used by register_filter; maps custom filters to callback functions -registered_filters = {} - -class TemplateSyntaxError(Exception): - pass - -class ContextPopException(Exception): - "pop() has been called more times than push()" - pass - -class TemplateDoesNotExist(Exception): - pass - -class VariableDoesNotExist(Exception): - pass - -class SilentVariableFailure(Exception): - "Any function raising this exception will be ignored by resolve_variable" - pass - -class Template: - def __init__(self, template_string): - "Compilation stage" - self.nodelist = compile_string(template_string) - - def __iter__(self): - for node in self.nodelist: - for subnode in node: - yield subnode - - def render(self, context): - "Display stage -- can be called many times" - return self.nodelist.render(context) - -def compile_string(template_string): - "Compiles template_string into NodeList ready for rendering" - tokens = tokenize(template_string) - parser = Parser(tokens) - return parser.parse() - -class Context: - "A stack container for variable context" - def __init__(self, dict=None): - dict = dict or {} - self.dicts = [dict] - - def __repr__(self): - return repr(self.dicts) - - def __iter__(self): - for d in self.dicts: - yield d - - def push(self): - self.dicts = [{}] + self.dicts - - def pop(self): - if len(self.dicts) == 1: - raise ContextPopException - del self.dicts[0] - - def __setitem__(self, key, value): - "Set a variable in the current context" - self.dicts[0][key] = value - - def __getitem__(self, key): - "Get a variable's value, starting at the current context and going upward" - for dict in self.dicts: - if dict.has_key(key): - return dict[key] - return '' - - def __delitem__(self, key): - "Delete a variable from the current context" - del self.dicts[0][key] - - def has_key(self, key): - for dict in self.dicts: - if dict.has_key(key): - return True - return False - - def update(self, other_dict): - "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." - self.dicts = [other_dict] + self.dicts - -class Token: - def __init__(self, token_type, contents): - "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK" - self.token_type, self.contents = token_type, contents - - def __str__(self): - return '<%s token: "%s...">' % ( - {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type], - self.contents[:20].replace('\n', '') - ) - -def tokenize(template_string): - "Return a list of tokens from a given template_string" - # remove all empty strings, because the regex has a tendency to add them - bits = filter(None, tag_re.split(template_string)) - return map(create_token, bits) - -def create_token(token_string): - "Convert the given token string into a new Token object and return it" - if token_string.startswith(VARIABLE_TAG_START): - return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) - elif token_string.startswith(BLOCK_TAG_START): - return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) - else: - return Token(TOKEN_TEXT, token_string) - -class Parser: - def __init__(self, tokens): - self.tokens = tokens - - def parse(self, parse_until=[]): - nodelist = NodeList() - while self.tokens: - token = self.next_token() - if token.token_type == TOKEN_TEXT: - nodelist.append(TextNode(token.contents)) - elif token.token_type == TOKEN_VAR: - if not token.contents: - raise TemplateSyntaxError, "Empty variable tag" - nodelist.append(VariableNode(token.contents)) - elif token.token_type == TOKEN_BLOCK: - if token.contents in parse_until: - # put token back on token list so calling code knows why it terminated - self.prepend_token(token) - return nodelist - try: - command = token.contents.split()[0] - except IndexError: - raise TemplateSyntaxError, "Empty block tag" - try: - # execute callback function for this tag and append resulting node - nodelist.append(registered_tags[command](self, token)) - except KeyError: - raise TemplateSyntaxError, "Invalid block tag: '%s'" % command - if parse_until: - raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until) - return nodelist - - def next_token(self): - return self.tokens.pop(0) - - def prepend_token(self, token): - self.tokens.insert(0, token) - - def delete_first_token(self): - del self.tokens[0] - -class FilterParser: - """Parse a variable token and its optional filters (all as a single string), - and return a list of tuples of the filter name and arguments. - Sample: - >>> token = 'variable|default:"Default value"|date:"Y-m-d"' - >>> p = FilterParser(token) - >>> p.filters - [('default', 'Default value'), ('date', 'Y-m-d')] - >>> p.var - 'variable' - - This class should never be instantiated outside of the - get_filters_from_token helper function. - """ - def __init__(self, s): - self.s = s - self.i = -1 - self.current = '' - self.filters = [] - self.current_filter_name = None - self.current_filter_arg = None - # First read the variable part - self.var = self.read_alphanumeric_token() - if not self.var: - raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s - if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_': - raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var - # Have we reached the end? - if self.current is None: - return - if self.current != FILTER_SEPARATOR: - raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) - # We have a filter separator; start reading the filters - self.read_filters() - - def next_char(self): - self.i = self.i + 1 - try: - self.current = self.s[self.i] - except IndexError: - self.current = None - - def read_alphanumeric_token(self): - """Read a variable name or filter name, which are continuous strings of - alphanumeric characters + the underscore""" - var = '' - while 1: - self.next_char() - if self.current is None: - break - if self.current not in ALLOWED_VARIABLE_CHARS: - break - var += self.current - return var - - def read_filters(self): - while 1: - filter_name, arg = self.read_filter() - if not registered_filters.has_key(filter_name): - raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name - if registered_filters[filter_name][1] == True and arg is None: - raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name - if registered_filters[filter_name][1] == False and arg is not None: - raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg) - self.filters.append((filter_name, arg)) - if self.current is None: - break - - def read_filter(self): - self.current_filter_name = self.read_alphanumeric_token() - self.current_filter_arg = None - # Have we reached the end? - if self.current is None: - return (self.current_filter_name, None) - # Does the filter have an argument? - if self.current == FILTER_ARGUMENT_SEPARATOR: - self.current_filter_arg = self.read_arg() - return (self.current_filter_name, self.current_filter_arg) - # Next thing MUST be a pipe - if self.current != FILTER_SEPARATOR: - raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) - return (self.current_filter_name, self.current_filter_arg) - - def read_arg(self): - # First read a " - self.next_char() - if self.current != '"': - raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current - self.escaped = False - arg = '' - while 1: - self.next_char() - if self.current == '"' and not self.escaped: - break - if self.current == '\\' and not self.escaped: - self.escaped = True - continue - if self.current == '\\' and self.escaped: - arg += '\\' - self.escaped = False - continue - if self.current == '"' and self.escaped: - arg += '"' - self.escaped = False - continue - if self.escaped and self.current not in '\\"': - raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s - if self.current is None: - raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s - arg += self.current - # self.current must now be '"' - self.next_char() - return arg - -def get_filters_from_token(token): - "Convenient wrapper for FilterParser" - p = FilterParser(token) - return (p.var, p.filters) - -def resolve_variable(path, context): - """ - Returns the resolved variable, which may contain attribute syntax, within - the given context. The variable may be a hard-coded string (if it begins - and ends with single or double quote marks). - - >>> c = {'article': {'section':'News'}} - >>> resolve_variable('article.section', c) - 'News' - >>> resolve_variable('article', c) - {'section': 'News'} - >>> class AClass: pass - >>> c = AClass() - >>> c.article = AClass() - >>> c.article.section = 'News' - >>> resolve_variable('article.section', c) - 'News' - - (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') - """ - if path[0] in ('"', "'") and path[0] == path[-1]: - current = path[1:-1] - else: - current = context - bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR) - while bits: - try: # dictionary lookup - current = current[bits[0]] - except (TypeError, AttributeError, KeyError): - try: # attribute lookup - current = getattr(current, bits[0]) - if callable(current): - if getattr(current, 'alters_data', False): - current = '' - else: - try: # method call (assuming no args required) - current = current() - except SilentVariableFailure: - current = '' - except TypeError: # arguments *were* required - current = '' # invalid method call - except (TypeError, AttributeError): - 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 - del bits[0] - return current - -def resolve_variable_with_filters(var_string, context): - """ - var_string is a full variable expression with optional filters, like: - a.b.c|lower|date:"y/m/d" - This function resolves the variable in the context, applies all filters and - returns the object. - """ - var, filters = get_filters_from_token(var_string) - try: - obj = resolve_variable(var, context) - except VariableDoesNotExist: - obj = '' - for name, arg in filters: - obj = registered_filters[name][0](obj, arg) - return obj - -class Node: - def render(self, context): - "Return the node rendered as a string" - pass - - def __iter__(self): - yield self - - def get_nodes_by_type(self, nodetype): - "Return a list of all nodes (within this node and its nodelist) of the given type" - nodes = [] - if isinstance(self, nodetype): - nodes.append(self) - if hasattr(self, 'nodelist'): - nodes.extend(self.nodelist.get_nodes_by_type(nodetype)) - return nodes - -class NodeList(list): - def render(self, context): - bits = [] - for node in self: - if isinstance(node, Node): - bits.append(node.render(context)) - else: - bits.append(node) - return ''.join(bits) - - def get_nodes_by_type(self, nodetype): - "Return a list of all nodes of the given type" - nodes = [] - for node in self: - nodes.extend(node.get_nodes_by_type(nodetype)) - return nodes - -class TextNode(Node): - def __init__(self, s): - self.s = s - - def __repr__(self): - return "" % self.s[:25] - - def render(self, context): - return self.s - -class VariableNode(Node): - def __init__(self, var_string): - self.var_string = var_string - - def __repr__(self): - return "" % self.var_string - - def render(self, context): - output = resolve_variable_with_filters(self.var_string, context) - # Check type so that we don't run str() on a Unicode object - if not isinstance(output, basestring): - output = str(output) - elif isinstance(output, unicode): - output = output.encode(DEFAULT_CHARSET) - return output - -def register_tag(token_command, callback_function): - registered_tags[token_command] = callback_function - -def unregister_tag(token_command): - del registered_tags[token_command] - -def register_filter(filter_name, callback_function, has_arg): - registered_filters[filter_name] = (callback_function, has_arg) - -def unregister_filter(filter_name): - del registered_filters[filter_name] - -import defaulttags -import defaultfilters diff --git a/django/core/template/__init__.py b/django/core/template/__init__.py new file mode 100644 index 0000000000..71a8e621c8 --- /dev/null +++ b/django/core/template/__init__.py @@ -0,0 +1,494 @@ +""" +This is the Django template system. + +How it works: + +The tokenize() function converts a template string (i.e., a string containing +markup with custom template tags) to tokens, which can be either plain text +(TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK). + +The Parser() class takes a list of tokens in its constructor, and its parse() +method returns a compiled template -- which is, under the hood, a list of +Node objects. + +Each Node is responsible for creating some sort of output -- e.g. simple text +(TextNode), variable values in a given context (VariableNode), results of basic +logic (IfNode), results of looping (ForNode), or anything else. The core Node +types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can +define their own custom node types. + +Each Node has a render() method, which takes a Context and returns a string of +the rendered node. For example, the render() method of a Variable Node returns +the variable's value as a string. The render() method of an IfNode returns the +rendered output of whatever was inside the loop, recursively. + +The Template class is a convenient wrapper that takes care of template +compilation and rendering. + +Usage: + +The only thing you should ever use directly in this file is the Template class. +Create a compiled template object with a template_string, then call render() +with a context. In the compilation stage, the TemplateSyntaxError exception +will be raised if the template doesn't have proper syntax. + +Sample code: + +>>> import template +>>> s = ''' +... +... {% if test %} +...

    {{ varvalue }}

    +... {% endif %} +... +... ''' +>>> t = template.Template(s) + +(t is now a compiled template, and its render() method can be called multiple +times with multiple contexts) + +>>> c = template.Context({'test':True, 'varvalue': 'Hello'}) +>>> t.render(c) +'\n\n\n

    Hello

    \n\n\n' +>>> c = template.Context({'test':False, 'varvalue': 'Hello'}) +>>> t.render(c) +'\n\n\n\n' +""" +import re +from django.conf.settings import DEFAULT_CHARSET + +__all__ = ('Template','Context','compile_string') + +TOKEN_TEXT = 0 +TOKEN_VAR = 1 +TOKEN_BLOCK = 2 + +# template syntax constants +FILTER_SEPARATOR = '|' +FILTER_ARGUMENT_SEPARATOR = ':' +VARIABLE_ATTRIBUTE_SEPARATOR = '.' +BLOCK_TAG_START = '{%' +BLOCK_TAG_END = '%}' +VARIABLE_TAG_START = '{{' +VARIABLE_TAG_END = '}}' + +ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' + +# match a variable or block tag and capture the entire tag, including start/end delimiters +tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), + re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) + +# global dict used by register_tag; maps custom tags to callback functions +registered_tags = {} + +# global dict used by register_filter; maps custom filters to callback functions +registered_filters = {} + +class TemplateSyntaxError(Exception): + pass + +class ContextPopException(Exception): + "pop() has been called more times than push()" + pass + +class TemplateDoesNotExist(Exception): + pass + +class VariableDoesNotExist(Exception): + pass + +class SilentVariableFailure(Exception): + "Any function raising this exception will be ignored by resolve_variable" + pass + +class Template: + def __init__(self, template_string): + "Compilation stage" + self.nodelist = compile_string(template_string) + + def __iter__(self): + for node in self.nodelist: + for subnode in node: + yield subnode + + def render(self, context): + "Display stage -- can be called many times" + return self.nodelist.render(context) + +def compile_string(template_string): + "Compiles template_string into NodeList ready for rendering" + tokens = tokenize(template_string) + parser = Parser(tokens) + return parser.parse() + +class Context: + "A stack container for variable context" + def __init__(self, dict=None): + dict = dict or {} + self.dicts = [dict] + + def __repr__(self): + return repr(self.dicts) + + def __iter__(self): + for d in self.dicts: + yield d + + def push(self): + self.dicts = [{}] + self.dicts + + def pop(self): + if len(self.dicts) == 1: + raise ContextPopException + del self.dicts[0] + + def __setitem__(self, key, value): + "Set a variable in the current context" + self.dicts[0][key] = value + + def __getitem__(self, key): + "Get a variable's value, starting at the current context and going upward" + for dict in self.dicts: + if dict.has_key(key): + return dict[key] + return '' + + def __delitem__(self, key): + "Delete a variable from the current context" + del self.dicts[0][key] + + def has_key(self, key): + for dict in self.dicts: + if dict.has_key(key): + return True + return False + + def update(self, other_dict): + "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." + self.dicts = [other_dict] + self.dicts + +class Token: + def __init__(self, token_type, contents): + "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK" + self.token_type, self.contents = token_type, contents + + def __str__(self): + return '<%s token: "%s...">' % ( + {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type], + self.contents[:20].replace('\n', '') + ) + +def tokenize(template_string): + "Return a list of tokens from a given template_string" + # remove all empty strings, because the regex has a tendency to add them + bits = filter(None, tag_re.split(template_string)) + return map(create_token, bits) + +def create_token(token_string): + "Convert the given token string into a new Token object and return it" + if token_string.startswith(VARIABLE_TAG_START): + return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) + elif token_string.startswith(BLOCK_TAG_START): + return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) + else: + return Token(TOKEN_TEXT, token_string) + +class Parser: + def __init__(self, tokens): + self.tokens = tokens + + def parse(self, parse_until=[]): + nodelist = NodeList() + while self.tokens: + token = self.next_token() + if token.token_type == TOKEN_TEXT: + nodelist.append(TextNode(token.contents)) + elif token.token_type == TOKEN_VAR: + if not token.contents: + raise TemplateSyntaxError, "Empty variable tag" + nodelist.append(VariableNode(token.contents)) + elif token.token_type == TOKEN_BLOCK: + if token.contents in parse_until: + # put token back on token list so calling code knows why it terminated + self.prepend_token(token) + return nodelist + try: + command = token.contents.split()[0] + except IndexError: + raise TemplateSyntaxError, "Empty block tag" + try: + # execute callback function for this tag and append resulting node + nodelist.append(registered_tags[command](self, token)) + except KeyError: + raise TemplateSyntaxError, "Invalid block tag: '%s'" % command + if parse_until: + raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until) + return nodelist + + def next_token(self): + return self.tokens.pop(0) + + def prepend_token(self, token): + self.tokens.insert(0, token) + + def delete_first_token(self): + del self.tokens[0] + +class FilterParser: + """Parse a variable token and its optional filters (all as a single string), + and return a list of tuples of the filter name and arguments. + Sample: + >>> token = 'variable|default:"Default value"|date:"Y-m-d"' + >>> p = FilterParser(token) + >>> p.filters + [('default', 'Default value'), ('date', 'Y-m-d')] + >>> p.var + 'variable' + + This class should never be instantiated outside of the + get_filters_from_token helper function. + """ + def __init__(self, s): + self.s = s + self.i = -1 + self.current = '' + self.filters = [] + self.current_filter_name = None + self.current_filter_arg = None + # First read the variable part + self.var = self.read_alphanumeric_token() + if not self.var: + raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s + if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_': + raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var + # Have we reached the end? + if self.current is None: + return + if self.current != FILTER_SEPARATOR: + raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) + # We have a filter separator; start reading the filters + self.read_filters() + + def next_char(self): + self.i = self.i + 1 + try: + self.current = self.s[self.i] + except IndexError: + self.current = None + + def read_alphanumeric_token(self): + """Read a variable name or filter name, which are continuous strings of + alphanumeric characters + the underscore""" + var = '' + while 1: + self.next_char() + if self.current is None: + break + if self.current not in ALLOWED_VARIABLE_CHARS: + break + var += self.current + return var + + def read_filters(self): + while 1: + filter_name, arg = self.read_filter() + if not registered_filters.has_key(filter_name): + raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name + if registered_filters[filter_name][1] == True and arg is None: + raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name + if registered_filters[filter_name][1] == False and arg is not None: + raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg) + self.filters.append((filter_name, arg)) + if self.current is None: + break + + def read_filter(self): + self.current_filter_name = self.read_alphanumeric_token() + self.current_filter_arg = None + # Have we reached the end? + if self.current is None: + return (self.current_filter_name, None) + # Does the filter have an argument? + if self.current == FILTER_ARGUMENT_SEPARATOR: + self.current_filter_arg = self.read_arg() + return (self.current_filter_name, self.current_filter_arg) + # Next thing MUST be a pipe + if self.current != FILTER_SEPARATOR: + raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) + return (self.current_filter_name, self.current_filter_arg) + + def read_arg(self): + # First read a " + self.next_char() + if self.current != '"': + raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current + self.escaped = False + arg = '' + while 1: + self.next_char() + if self.current == '"' and not self.escaped: + break + if self.current == '\\' and not self.escaped: + self.escaped = True + continue + if self.current == '\\' and self.escaped: + arg += '\\' + self.escaped = False + continue + if self.current == '"' and self.escaped: + arg += '"' + self.escaped = False + continue + if self.escaped and self.current not in '\\"': + raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s + if self.current is None: + raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s + arg += self.current + # self.current must now be '"' + self.next_char() + return arg + +def get_filters_from_token(token): + "Convenient wrapper for FilterParser" + p = FilterParser(token) + return (p.var, p.filters) + +def resolve_variable(path, context): + """ + Returns the resolved variable, which may contain attribute syntax, within + the given context. The variable may be a hard-coded string (if it begins + and ends with single or double quote marks). + + >>> c = {'article': {'section':'News'}} + >>> resolve_variable('article.section', c) + 'News' + >>> resolve_variable('article', c) + {'section': 'News'} + >>> class AClass: pass + >>> c = AClass() + >>> c.article = AClass() + >>> c.article.section = 'News' + >>> resolve_variable('article.section', c) + 'News' + + (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') + """ + if path[0] in ('"', "'") and path[0] == path[-1]: + current = path[1:-1] + else: + current = context + bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR) + while bits: + try: # dictionary lookup + current = current[bits[0]] + except (TypeError, AttributeError, KeyError): + try: # attribute lookup + current = getattr(current, bits[0]) + if callable(current): + if getattr(current, 'alters_data', False): + current = '' + else: + try: # method call (assuming no args required) + current = current() + except SilentVariableFailure: + current = '' + except TypeError: # arguments *were* required + current = '' # invalid method call + except (TypeError, AttributeError): + 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 + del bits[0] + return current + +def resolve_variable_with_filters(var_string, context): + """ + var_string is a full variable expression with optional filters, like: + a.b.c|lower|date:"y/m/d" + This function resolves the variable in the context, applies all filters and + returns the object. + """ + var, filters = get_filters_from_token(var_string) + try: + obj = resolve_variable(var, context) + except VariableDoesNotExist: + obj = '' + for name, arg in filters: + obj = registered_filters[name][0](obj, arg) + return obj + +class Node: + def render(self, context): + "Return the node rendered as a string" + pass + + def __iter__(self): + yield self + + def get_nodes_by_type(self, nodetype): + "Return a list of all nodes (within this node and its nodelist) of the given type" + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + if hasattr(self, 'nodelist'): + nodes.extend(self.nodelist.get_nodes_by_type(nodetype)) + return nodes + +class NodeList(list): + def render(self, context): + bits = [] + for node in self: + if isinstance(node, Node): + bits.append(node.render(context)) + else: + bits.append(node) + return ''.join(bits) + + def get_nodes_by_type(self, nodetype): + "Return a list of all nodes of the given type" + nodes = [] + for node in self: + nodes.extend(node.get_nodes_by_type(nodetype)) + return nodes + +class TextNode(Node): + def __init__(self, s): + self.s = s + + def __repr__(self): + return "" % self.s[:25] + + def render(self, context): + return self.s + +class VariableNode(Node): + def __init__(self, var_string): + self.var_string = var_string + + def __repr__(self): + return "" % self.var_string + + def render(self, context): + output = resolve_variable_with_filters(self.var_string, context) + # Check type so that we don't run str() on a Unicode object + if not isinstance(output, basestring): + output = str(output) + elif isinstance(output, unicode): + output = output.encode(DEFAULT_CHARSET) + return output + +def register_tag(token_command, callback_function): + registered_tags[token_command] = callback_function + +def unregister_tag(token_command): + del registered_tags[token_command] + +def register_filter(filter_name, callback_function, has_arg): + registered_filters[filter_name] = (callback_function, has_arg) + +def unregister_filter(filter_name): + del registered_filters[filter_name] + +import defaulttags +import defaultfilters diff --git a/django/core/template/defaultfilters.py b/django/core/template/defaultfilters.py new file mode 100644 index 0000000000..9ef367ee52 --- /dev/null +++ b/django/core/template/defaultfilters.py @@ -0,0 +1,465 @@ +"Default variable filters" + +from django.core.template import register_filter, resolve_variable +import re +import random as random_module + +################### +# STRINGS # +################### + +def addslashes(value, _): + "Adds slashes - useful for passing strings to JavaScript, for example." + return value.replace('"', '\\"').replace("'", "\\'") + +def capfirst(value, _): + "Capitalizes the first character of the value" + value = str(value) + return value and value[0].upper() + value[1:] + +def fix_ampersands(value, _): + "Replaces ampersands with ``&`` entities" + from django.utils.html import fix_ampersands + return fix_ampersands(value) + +def floatformat(text, _): + """ + Displays a floating point number as 34.2 (with one decimal place) - but + only if there's a point to be displayed + """ + from math import modf + if not text: + return '' + if modf(float(text))[0] < 0.1: + return text + return "%.1f" % float(text) + +def linenumbers(value, _): + "Displays text with line numbers" + from django.utils.html import escape + lines = value.split('\n') + # Find the maximum width of the line count, for use with zero padding string format command + width = str(len(str(len(lines)))) + for i, line in enumerate(lines): + lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) + return '\n'.join(lines) + +def lower(value, _): + "Converts a string into all lowercase" + return value.lower() + +def make_list(value, _): + """ + Returns the value turned into a list. For an integer, it's a list of + digits. For a string, it's a list of characters. + """ + return list(str(value)) + +def slugify(value, _): + "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" + value = re.sub('[^\w\s-]', '', value).strip().lower() + return re.sub('\s+', '-', value) + +def stringformat(value, arg): + """ + Formats the variable according to the argument, a string formatting specifier. + This specifier uses Python string formating syntax, with the exception that + the leading "%" is dropped. + + See http://docs.python.org/lib/typesseq-strings.html for documentation + of Python string formatting + """ + try: + return ("%" + arg) % value + except (ValueError, TypeError): + return "" + +def title(value, _): + "Converts a string into titlecase" + return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) + +def truncatewords(value, arg): + """ + Truncates a string after a certain number of words + + Argument: Number of words to truncate after + """ + from django.utils.text import truncate_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_words(value, length) + +def upper(value, _): + "Converts a string into all uppercase" + return value.upper() + +def urlencode(value, _): + "Escapes a value for use in a URL" + import urllib + return urllib.quote(value) + +def urlize(value, _): + "Converts URLs in plain text into clickable links" + from django.utils.html import urlize + return urlize(value, nofollow=True) + +def urlizetrunc(value, limit): + """ + Converts URLs into clickable links, truncating URLs to the given character limit + + Argument: Length to truncate URLs to. + """ + from django.utils.html import urlize + return urlize(value, trim_url_limit=int(limit), nofollow=True) + +def wordcount(value, _): + "Returns the number of words" + return len(value.split()) + +def wordwrap(value, arg): + """ + Wraps words at specified line length + + Argument: number of words to wrap the text at. + """ + from django.utils.text import wrap + return wrap(value, int(arg)) + +def ljust(value, arg): + """ + Left-aligns the value in a field of a given width + + Argument: field size + """ + return str(value).ljust(int(arg)) + +def rjust(value, arg): + """ + Right-aligns the value in a field of a given width + + Argument: field size + """ + return str(value).rjust(int(arg)) + +def center(value, arg): + "Centers the value in a field of a given width" + return str(value).center(int(arg)) + +def cut(value, arg): + "Removes all values of arg from the given string" + return value.replace(arg, '') + +################### +# HTML STRINGS # +################### + +def escape(value, _): + "Escapes a string's HTML" + from django.utils.html import escape + return escape(value) + +def linebreaks(value, _): + "Converts newlines into

    and
    s" + from django.utils.html import linebreaks + return linebreaks(value) + +def linebreaksbr(value, _): + "Converts newlines into
    s" + return value.replace('\n', '
    ') + +def removetags(value, tags): + "Removes a space separated list of [X]HTML tags from the output" + tags = [re.escape(tag) for tag in tags.split()] + tags_re = '(%s)' % '|'.join(tags) + starttag_re = re.compile('<%s(>|(\s+[^>]*>))' % tags_re) + endtag_re = re.compile('' % tags_re) + value = starttag_re.sub('', value) + value = endtag_re.sub('', value) + return value + +def striptags(value, _): + "Strips all [X]HTML tags" + from django.utils.html import strip_tags + if not isinstance(value, basestring): + value = str(value) + return strip_tags(value) + +################### +# LISTS # +################### + +def dictsort(value, arg): + """ + Takes a list of dicts, returns that list sorted by the property given in + the argument. + """ + decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] + decorated.sort() + return [item[1] for item in decorated] + +def dictsortreversed(value, arg): + """ + Takes a list of dicts, returns that list sorted in reverse order by the + property given in the argument. + """ + decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] + decorated.sort() + decorated.reverse() + return [item[1] for item in decorated] + +def first(value, _): + "Returns the first item in a list" + try: + return value[0] + except IndexError: + return '' + +def join(value, arg): + "Joins a list with a string, like Python's ``str.join(list)``" + try: + return arg.join(map(str, value)) + except AttributeError: # fail silently but nicely + return value + +def length(value, _): + "Returns the length of the value - useful for lists" + return len(value) + +def length_is(value, arg): + "Returns a boolean of whether the value's length is the argument" + return len(value) == int(arg) + +def random(value, _): + "Returns a random item from the list" + return random_module.choice(value) + +def slice_(value, arg): + """ + Returns a slice of the list. + + Uses the same syntax as Python's list slicing; see + http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice + for an introduction. + """ + try: + return value[slice(*[x and int(x) or None for x in arg.split(':')])] + except (ValueError, TypeError): + return value # Fail silently. + +def unordered_list(value, _): + """ + Recursively takes a self-nested list and returns an HTML unordered list -- + WITHOUT opening and closing

      tags. + + The list is assumed to be in the proper format. For example, if ``var`` contains + ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``, + then ``{{ var|unordered_list }}`` would return:: + +
    • States +
        +
      • Kansas +
          +
        • Lawrence
        • +
        • Topeka
        • +
        +
      • +
      • Illinois
      • +
      +
    • + """ + def _helper(value, tabs): + indent = '\t' * tabs + if value[1]: + return '%s
    • %s\n%s
        \n%s\n%s
      \n%s
    • ' % (indent, value[0], indent, + '\n'.join([unordered_list(v, tabs+1) for v in value[1]]), indent, indent) + else: + return '%s
    • %s
    • ' % (indent, value[0]) + return _helper(value, 1) + +################### +# INTEGERS # +################### + +def add(value, arg): + "Adds the arg to the value" + return int(value) + int(arg) + +def get_digit(value, arg): + """ + Given a whole number, returns the requested digit of it, where 1 is the + right-most digit, 2 is the second-right-most digit, etc. Returns the + original value for invalid input (if input or argument is not an integer, + or if argument is less than 1). Otherwise, output is always an integer. + """ + try: + arg = int(arg) + value = int(value) + except ValueError: + return value # Fail silently for an invalid argument + if arg < 1: + return value + try: + return int(str(value)[-arg]) + except IndexError: + return 0 + +################### +# DATES # +################### + +def date(value, arg): + "Formats a date according to the given format" + from django.utils.dateformat import format + return format(value, arg) + +def time(value, arg): + "Formats a time according to the given format" + from django.utils.dateformat import time_format + return time_format(value, arg) + +def timesince(value, _): + 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' + from django.utils.timesince import timesince + return timesince(value) + +################### +# LOGIC # +################### + +def default(value, arg): + "If value is unavailable, use given default" + return value or arg + +def default_if_none(value, arg): + "If value is None, use given default" + if value is None: + return arg + return value + +def divisibleby(value, arg): + "Returns true if the value is devisible by the argument" + return int(value) % int(arg) == 0 + +def yesno(value, arg): + """ + Given a string mapping values for true, false and (optionally) None, + returns one of those strings accoding to the value: + + ========== ====================== ================================== + Value Argument Outputs + ========== ====================== ================================== + ``True`` ``"yeah,no,maybe"`` ``yeah`` + ``False`` ``"yeah,no,maybe"`` ``no`` + ``None`` ``"yeah,no,maybe"`` ``maybe`` + ``None`` ``"yeah,no"`` ``"no"`` (converts None to False + if no mapping for None is given. + ========== ====================== ================================== + """ + bits = arg.split(',') + if len(bits) < 2: + return value # Invalid arg. + try: + yes, no, maybe = bits + except ValueError: # unpack list of wrong size (no "maybe" value provided) + yes, no, maybe = bits[0], bits[1], bits[1] + if value is None: + return maybe + if value: + return yes + return no + +################### +# MISC # +################### + +def filesizeformat(bytes, _): + """ + Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 + bytes, etc). + """ + bytes = float(bytes) + if bytes < 1024: + return "%d byte%s" % (bytes, bytes != 1 and 's' or '') + if bytes < 1024 * 1024: + return "%.1f KB" % (bytes / 1024) + if bytes < 1024 * 1024 * 1024: + return "%.1f MB" % (bytes / (1024 * 1024)) + return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) + +def pluralize(value, _): + "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'" + try: + if int(value) != 1: + return 's' + except ValueError: # invalid string that's not a number + pass + except TypeError: # value isn't a string or a number; maybe it's a list? + try: + if len(value) != 1: + return 's' + except TypeError: # len() of unsized object + pass + return '' + +def phone2numeric(value, _): + "Takes a phone number and converts it in to its numerical equivalent" + from django.utils.text import phone2numeric + return phone2numeric(value) + +def pprint(value, _): + "A wrapper around pprint.pprint -- for debugging, really" + from pprint import pformat + return pformat(value) + +# Syntax: register_filter(name of filter, callback, has_argument) +register_filter('add', add, True) +register_filter('addslashes', addslashes, False) +register_filter('capfirst', capfirst, False) +register_filter('center', center, True) +register_filter('cut', cut, True) +register_filter('date', date, True) +register_filter('default', default, True) +register_filter('dictsort', dictsort, True) +register_filter('dictsortreversed', dictsortreversed, True) +register_filter('divisibleby', divisibleby, True) +register_filter('escape', escape, False) +register_filter('filesizeformat', filesizeformat, False) +register_filter('first', first, False) +register_filter('fix_ampersands', fix_ampersands, False) +register_filter('floatformat', floatformat, False) +register_filter('get_digit', get_digit, True) +register_filter('join', join, True) +register_filter('length', length, False) +register_filter('length_is', length_is, True) +register_filter('linebreaks', linebreaks, False) +register_filter('linebreaksbr', linebreaksbr, False) +register_filter('linenumbers', linenumbers, False) +register_filter('ljust', ljust, True) +register_filter('lower', lower, False) +register_filter('make_list', make_list, False) +register_filter('phone2numeric', phone2numeric, False) +register_filter('pluralize', pluralize, False) +register_filter('pprint', pprint, False) +register_filter('removetags', removetags, True) +register_filter('random', random, False) +register_filter('rjust', rjust, True) +register_filter('slice', slice_, True) +register_filter('slugify', slugify, False) +register_filter('stringformat', stringformat, True) +register_filter('striptags', striptags, False) +register_filter('time', time, True) +register_filter('timesince', timesince, False) +register_filter('title', title, False) +register_filter('truncatewords', truncatewords, True) +register_filter('unordered_list', unordered_list, False) +register_filter('upper', upper, False) +register_filter('urlencode', urlencode, False) +register_filter('urlize', urlize, False) +register_filter('urlizetrunc', urlizetrunc, True) +register_filter('wordcount', wordcount, False) +register_filter('wordwrap', wordwrap, True) +register_filter('yesno', yesno, True) diff --git a/django/core/template/defaulttags.py b/django/core/template/defaulttags.py new file mode 100644 index 0000000000..ea21ebafce --- /dev/null +++ b/django/core/template/defaulttags.py @@ -0,0 +1,771 @@ +"Default tags used by the template system, available to all templates." + +from django.core.template import Node, NodeList, Template, Context, resolve_variable, resolve_variable_with_filters, get_filters_from_token, registered_filters +from django.core.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, register_tag +import sys + +class CommentNode(Node): + def render(self, context): + return '' + +class CycleNode(Node): + def __init__(self, cyclevars): + self.cyclevars = cyclevars + self.cyclevars_len = len(cyclevars) + self.counter = -1 + + def render(self, context): + self.counter += 1 + return self.cyclevars[self.counter % self.cyclevars_len] + +class DebugNode(Node): + def render(self, context): + from pprint import pformat + output = [pformat(val) for val in context] + output.append('\n\n') + output.append(pformat(sys.modules)) + return ''.join(output) + +class FilterNode(Node): + def __init__(self, filters, nodelist): + self.filters, self.nodelist = filters, nodelist + + def render(self, context): + output = self.nodelist.render(context) + # apply filters + for f in self.filters: + output = registered_filters[f[0]][0](output, f[1]) + return output + +class FirstOfNode(Node): + def __init__(self, vars): + self.vars = vars + + def render(self, context): + for var in self.vars: + value = resolve_variable(var, context) + if value: + return str(value) + return '' + +class ForNode(Node): + def __init__(self, loopvar, sequence, reversed, nodelist_loop): + self.loopvar, self.sequence = loopvar, sequence + self.reversed = reversed + self.nodelist_loop = nodelist_loop + + def __repr__(self): + if self.reversed: + reversed = ' reversed' + else: + reversed = '' + return "" % \ + (self.loopvar, self.sequence, len(self.nodelist_loop), reversed) + + def __iter__(self): + for node in self.nodelist_loop: + yield node + + def get_nodes_by_type(self, nodetype): + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) + return nodes + + def render(self, context): + nodelist = NodeList() + if context.has_key('forloop'): + parentloop = context['forloop'] + else: + parentloop = {} + context.push() + try: + values = resolve_variable_with_filters(self.sequence, context) + except VariableDoesNotExist: + values = [] + if values is None: + values = [] + len_values = len(values) + if self.reversed: + # From http://www.python.org/doc/current/tut/node11.html + def reverse(data): + for index in range(len(data)-1, -1, -1): + yield data[index] + values = reverse(values) + for i, item in enumerate(values): + context['forloop'] = { + # shortcuts for current loop iteration number + 'counter0': i, + 'counter': i+1, + # reverse counter iteration numbers + 'revcounter': len_values - i, + 'revcounter0': len_values - i - 1, + # boolean values designating first and last times through loop + 'first': (i == 0), + 'last': (i == len_values - 1), + 'parentloop': parentloop, + } + context[self.loopvar] = item + for node in self.nodelist_loop: + nodelist.append(node.render(context)) + context.pop() + return nodelist.render(context) + +class IfChangedNode(Node): + def __init__(self, nodelist): + self.nodelist = nodelist + self._last_seen = None + + def render(self, context): + content = self.nodelist.render(context) + if content != self._last_seen: + firstloop = (self._last_seen == None) + self._last_seen = content + context.push() + context['ifchanged'] = {'firstloop': firstloop} + content = self.nodelist.render(context) + context.pop() + return content + else: + return '' + +class IfEqualNode(Node): + def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): + self.var1, self.var2 = var1, var2 + self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false + self.negate = negate + + def __repr__(self): + return "" + + def render(self, context): + val1 = resolve_variable(self.var1, context) + val2 = resolve_variable(self.var2, context) + if (self.negate and val1 != val2) or (not self.negate and val1 == val2): + return self.nodelist_true.render(context) + return self.nodelist_false.render(context) + +class IfNode(Node): + def __init__(self, boolvars, nodelist_true, nodelist_false): + self.boolvars = boolvars + self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false + + def __repr__(self): + return "" + + def __iter__(self): + for node in self.nodelist_true: + yield node + for node in self.nodelist_false: + yield node + + def get_nodes_by_type(self, nodetype): + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype)) + nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) + return nodes + + def render(self, context): + for ifnot, boolvar in self.boolvars: + try: + value = resolve_variable_with_filters(boolvar, context) + except VariableDoesNotExist: + value = None + if (value and not ifnot) or (ifnot and not value): + return self.nodelist_true.render(context) + return self.nodelist_false.render(context) + +class RegroupNode(Node): + def __init__(self, target_var, expression, var_name): + self.target_var, self.expression = target_var, expression + self.var_name = var_name + + def render(self, context): + obj_list = resolve_variable_with_filters(self.target_var, context) + if obj_list == '': # target_var wasn't found in context; fail silently + context[self.var_name] = [] + return '' + output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} + for obj in obj_list: + grouper = resolve_variable_with_filters('var.%s' % self.expression, \ + Context({'var': obj})) + if output and repr(output[-1]['grouper']) == repr(grouper): + output[-1]['list'].append(obj) + else: + output.append({'grouper': grouper, 'list': [obj]}) + context[self.var_name] = output + return '' + +def include_is_allowed(filepath): + from django.conf.settings import ALLOWED_INCLUDE_ROOTS + for root in ALLOWED_INCLUDE_ROOTS: + if filepath.startswith(root): + return True + return False + +class SsiNode(Node): + def __init__(self, filepath, parsed): + self.filepath, self.parsed = filepath, parsed + + def render(self, context): + if not include_is_allowed(self.filepath): + return '' # Fail silently for invalid includes. + try: + fp = open(self.filepath, 'r') + output = fp.read() + fp.close() + except IOError: + output = '' + if self.parsed: + try: + t = Template(output) + return t.render(context) + except TemplateSyntaxError: + return '' # Fail silently for invalid included templates. + return output + +class LoadNode(Node): + def __init__(self, taglib): + self.taglib = taglib + + def load_taglib(taglib): + mod = __import__("django.templatetags.%s" % taglib.split('.')[-1], '', '', ['']) + reload(mod) + return mod + load_taglib = staticmethod(load_taglib) + + def render(self, context): + "Import the relevant module" + try: + self.__class__.load_taglib(self.taglib) + except ImportError: + pass # Fail silently for invalid loads. + return '' + +class NowNode(Node): + def __init__(self, format_string): + self.format_string = format_string + + def render(self, context): + from datetime import datetime + from django.utils.dateformat import DateFormat + df = DateFormat(datetime.now()) + return df.format(self.format_string) + +class TemplateTagNode(Node): + mapping = {'openblock': BLOCK_TAG_START, + 'closeblock': BLOCK_TAG_END, + 'openvariable': VARIABLE_TAG_START, + 'closevariable': VARIABLE_TAG_END} + + def __init__(self, tagtype): + self.tagtype = tagtype + + def render(self, context): + return self.mapping.get(self.tagtype, '') + +class WidthRatioNode(Node): + def __init__(self, val_var, max_var, max_width): + self.val_var = val_var + self.max_var = max_var + self.max_width = max_width + + def render(self, context): + try: + value = resolve_variable_with_filters(self.val_var, context) + maxvalue = resolve_variable_with_filters(self.max_var, context) + except VariableDoesNotExist: + return '' + try: + value = float(value) + maxvalue = float(maxvalue) + ratio = (value / maxvalue) * int(self.max_width) + except (ValueError, ZeroDivisionError): + return '' + return str(int(round(ratio))) + +def do_comment(parser, token): + """ + Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` + """ + nodelist = parser.parse(('endcomment',)) + parser.delete_first_token() + return CommentNode() + +def do_cycle(parser, token): + """ + Cycle among the given strings each time this tag is encountered + + Within a loop, cycles among the given strings each time through + the loop:: + + {% for o in some_list %} + + ... + + {% endfor %} + + Outside of a loop, give the values a unique name the first time you call + it, then use that name each sucessive time through:: + + ... + ... + ... + + You can use any number of values, seperated by commas. Make sure not to + put spaces between the values -- only commas. + """ + + # Note: This returns the exact same node on each {% cycle name %} call; that + # is, the node object returned from {% cycle a,b,c as name %} and the one + # returned from {% cycle name %} are the exact same object. This shouldn't + # cause problems (heh), but if it does, now you know. + # + # Ugly hack warning: this stuffs the named template dict into parser so + # that names are only unique within each template (as opposed to using + # a global variable, which would make cycle names have to be unique across + # *all* templates. + + args = token.contents.split() + if len(args) < 2: + raise TemplateSyntaxError("'Cycle' statement requires at least two arguments") + + elif len(args) == 2 and "," in args[1]: + # {% cycle a,b,c %} + cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks + return CycleNode(cyclevars) + # {% cycle name %} + + elif len(args) == 2: + name = args[1] + if not parser._namedCycleNodes.has_key(name): + raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) + return parser._namedCycleNodes[name] + + elif len(args) == 4: + # {% cycle a,b,c as name %} + if args[2] != 'as': + raise TemplateSyntaxError("Second 'cycle' argument must be 'as'") + cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks + name = args[3] + node = CycleNode(cyclevars) + + if not hasattr(parser, '_namedCycleNodes'): + parser._namedCycleNodes = {} + + parser._namedCycleNodes[name] = node + return node + + else: + raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) + +def do_debug(parser, token): + "Print a whole load of debugging information, including the context and imported modules" + return DebugNode() + +def do_filter(parser, token): + """ + Filter the contents of the blog through variable filters. + + Filters can also be piped through each other, and they can have + arguments -- just like in variable syntax. + + Sample usage:: + + {% filter escape|lower %} + This text will be HTML-escaped, and will appear in lowercase. + {% endfilter %} + """ + _, rest = token.contents.split(None, 1) + _, filters = get_filters_from_token('var|%s' % rest) + nodelist = parser.parse(('endfilter',)) + parser.delete_first_token() + return FilterNode(filters, nodelist) + +def do_firstof(parser, token): + """ + Outputs the first variable passed that is not False. + + Outputs nothing if all the passed variables are False. + + Sample usage:: + + {% firstof var1 var2 var3 %} + + This is equivalent to:: + + {% if var1 %} + {{ var1 }} + {% else %}{% if var2 %} + {{ var2 }} + {% else %}{% if var3 %} + {{ var3 }} + {% endif %}{% endif %}{% endif %} + + but obviously much cleaner! + """ + bits = token.contents.split()[1:] + if len(bits) < 1: + raise TemplateSyntaxError, "'firstof' statement requires at least one argument" + return FirstOfNode(bits) + + +def do_for(parser, token): + """ + Loop over each item in an array. + + For example, to display a list of athletes given ``athlete_list``:: + +
        + {% for athlete in athlete_list %} +
      • {{ athlete.name }}
      • + {% endfor %} +
      + + You can also loop over a list in reverse by using + ``{% for obj in list reversed %}``. + + The for loop sets a number of variables available within the loop: + + ========================== ================================================ + Variable Description + ========================== ================================================ + ``forloop.counter`` The current iteration of the loop (1-indexed) + ``forloop.counter0`` The current iteration of the loop (0-indexed) + ``forloop.revcounter`` The number of iterations from the end of the + loop (1-indexed) + ``forloop.revcounter0`` The number of iterations from the end of the + loop (0-indexed) + ``forloop.first`` True if this is the first time through the loop + ``forloop.last`` True if this is the last time through the loop + ``forloop.parentloop`` For nested loops, this is the loop "above" the + current one + ========================== ================================================ + + """ + bits = token.contents.split() + if len(bits) == 5 and bits[4] != 'reversed': + raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents + if len(bits) not in (4, 5): + raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents + if bits[2] != 'in': + raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents + loopvar = bits[1] + sequence = bits[3] + reversed = (len(bits) == 5) + nodelist_loop = parser.parse(('endfor',)) + parser.delete_first_token() + return ForNode(loopvar, sequence, reversed, nodelist_loop) + +def do_ifequal(parser, token, negate): + """ + Output the contents of the block if the two arguments equal/don't equal each other. + + Examples:: + + {% ifequal user.id comment.user_id %} + ... + {% endifequal %} + + {% ifnotequal user.id comment.user_id %} + ... + {% else %} + ... + {% endifnotequal %} + """ + bits = token.contents.split() + if len(bits) != 3: + raise TemplateSyntaxError, "%r takes two arguments" % bits[0] + end_tag = 'end' + bits[0] + nodelist_true = parser.parse(('else', end_tag)) + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse((end_tag,)) + parser.delete_first_token() + else: + nodelist_false = NodeList() + return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) + +def do_if(parser, token): + """ + The ``{% if %}`` tag evaluates a variable, and if that variable is "true" + (i.e. exists, is not empty, and is not a false boolean value) the contents + of the block are output: + + :: + + {% if althlete_list %} + Number of athletes: {{ althete_list|count }} + {% else %} + No athletes. + {% endif %} + + In the above, if ``athlete_list`` is not empty, the number of athletes will + be displayed by the ``{{ athlete_list|count }}`` variable. + + As you can see, the ``if`` tag can take an option ``{% else %}`` clause that + will be displayed if the test fails. + + ``if`` tags may use ``or`` or ``not`` to test a number of variables or to + negate a given variable:: + + {% if not athlete_list %} + There are no athletes. + {% endif %} + + {% if athlete_list or coach_list %} + There are some athletes or some coaches. + {% endif %} + + {% if not athlete_list or coach_list %} + There are no athletes or there are some coaches (OK, so + writing English translations of boolean logic sounds + stupid; it's not my fault). + {% endif %} + + For simplicity, ``if`` tags do not allow ``and`` clauses; use nested ``if`` + tags instead:: + + {% if athlete_list %} + {% if coach_list %} + Number of athletes: {{ athlete_list|count }}. + Number of coaches: {{ coach_list|count }}. + {% endif %} + {% endif %} + """ + bits = token.contents.split() + del bits[0] + if not bits: + raise TemplateSyntaxError, "'if' statement requires at least one argument" + # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] + boolpairs = ' '.join(bits).split(' or ') + boolvars = [] + for boolpair in boolpairs: + if ' ' in boolpair: + not_, boolvar = boolpair.split() + if not_ != 'not': + raise TemplateSyntaxError, "Expected 'not' in if statement" + boolvars.append((True, boolvar)) + else: + boolvars.append((False, boolpair)) + nodelist_true = parser.parse(('else', 'endif')) + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse(('endif',)) + parser.delete_first_token() + else: + nodelist_false = NodeList() + return IfNode(boolvars, nodelist_true, nodelist_false) + +def do_ifchanged(parser, token): + """ + Check if a value has changed from the last iteration of a loop. + + The 'ifchanged' block tag is used within a loop. It checks its own rendered + contents against its previous state and only displays its content if the + value has changed:: + +

      Archive for {{ year }}

      + + {% for date in days %} + {% ifchanged %}

      {{ date|date:"F" }}

      {% endifchanged %} + {{ date|date:"j" }} + {% endfor %} + """ + bits = token.contents.split() + if len(bits) != 1: + raise TemplateSyntaxError, "'ifchanged' tag takes no arguments" + nodelist = parser.parse(('endifchanged',)) + parser.delete_first_token() + return IfChangedNode(nodelist) + +def do_ssi(parser, token): + """ + Output the contents of a given file into the page. + + Like a simple "include" tag, the ``ssi`` tag includes the contents + of another file -- which must be specified using an absolute page -- + in the current page:: + + {% ssi /home/html/ljworld.com/includes/right_generic.html %} + + If the optional "parsed" parameter is given, the contents of the included + file are evaluated as template code, with the current context:: + + {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} + """ + bits = token.contents.split() + parsed = False + if len(bits) not in (2, 3): + raise TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" + if len(bits) == 3: + if bits[2] == 'parsed': + parsed = True + else: + raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] + return SsiNode(bits[1], parsed) + +def do_load(parser, token): + """ + Load a custom template tag set. + + For example, to load the template tags in ``django/templatetags/news/photos.py``:: + + {% load news.photos %} + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'load' statement takes one argument" + taglib = bits[1] + # check at compile time that the module can be imported + try: + LoadNode.load_taglib(taglib) + except ImportError: + raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib + return LoadNode(taglib) + +def do_now(parser, token): + """ + Display the date, formatted according to the given string. + + Uses the same format as PHP's ``date()`` function; see http://php.net/date + for all the possible values. + + Sample usage:: + + It is {% now "jS F Y H:i" %} + """ + bits = token.contents.split('"') + if len(bits) != 3: + raise TemplateSyntaxError, "'now' statement takes one argument" + format_string = bits[1] + return NowNode(format_string) + +def do_regroup(parser, token): + """ + Regroup a list of alike objects by a common attribute. + + This complex tag is best illustrated by use of an example: say that + ``people`` is a list of ``Person`` objects that have ``first_name``, + ``last_name``, and ``gender`` attributes, and you'd like to display a list + that looks like: + + * Male: + * George Bush + * Bill Clinton + * Female: + * Margaret Thatcher + * Colendeeza Rice + * Unknown: + * Pat Smith + + The following snippet of template code would accomplish this dubious task:: + + {% regroup people by gender as grouped %} +
        + {% for group in grouped %} +
      • {{ group.grouper }} +
          + {% for item in group.list %} +
        • {{ item }}
        • + {% endfor %} +
        + {% endfor %} +
      + + As you can see, ``{% regroup %}`` populates a variable with a list of + objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the + item that was grouped by; ``list`` contains the list of objects that share + that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female`` + and ``Unknown``, and ``list`` is the list of people with those genders. + + Note that `{% regroup %}`` does not work when the list to be grouped is not + sorted by the key you are grouping by! This means that if your list of + people was not sorted by gender, you'd need to make sure it is sorted before + using it, i.e.:: + + {% regroup people|dictsort:"gender" by gender as grouped %} + + """ + firstbits = token.contents.split(None, 3) + if len(firstbits) != 4: + raise TemplateSyntaxError, "'regroup' tag takes five arguments" + target_var = firstbits[1] + if firstbits[2] != 'by': + raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" + lastbits_reversed = firstbits[3][::-1].split(None, 2) + if lastbits_reversed[1][::-1] != 'as': + raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" + expression = lastbits_reversed[2][::-1] + var_name = lastbits_reversed[0][::-1] + return RegroupNode(target_var, expression, var_name) + +def do_templatetag(parser, token): + """ + Output one of the bits used to compose template tags. + + Since the template system has no concept of "escaping", to display one of + the bits used in template tags, you must use the ``{% templatetag %}`` tag. + + The argument tells which template bit to output: + + ================== ======= + Argument Outputs + ================== ======= + ``openblock`` ``{%`` + ``closeblock`` ``%}`` + ``openvariable`` ``{{`` + ``closevariable`` ``}}`` + ================== ======= + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'templatetag' statement takes one argument" + tag = bits[1] + if not TemplateTagNode.mapping.has_key(tag): + raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ + (tag, TemplateTagNode.mapping.keys()) + return TemplateTagNode(tag) + +def do_widthratio(parser, token): + """ + For creating bar charts and such, this tag calculates the ratio of a given + value to a maximum value, and then applies that ratio to a constant. + + For example:: + + + + Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in + the above example will be 88 pixels wide (because 175/200 = .875; .875 * + 100 = 87.5 which is rounded up to 88). + """ + bits = token.contents.split() + if len(bits) != 4: + raise TemplateSyntaxError("widthratio takes three arguments") + tag, this_value_var, max_value_var, max_width = bits + try: + max_width = int(max_width) + except ValueError: + raise TemplateSyntaxError("widthratio final argument must be an integer") + return WidthRatioNode(this_value_var, max_value_var, max_width) + +register_tag('comment', do_comment) +register_tag('cycle', do_cycle) +register_tag('debug', do_debug) +register_tag('filter', do_filter) +register_tag('firstof', do_firstof) +register_tag('for', do_for) +register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False)) +register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True)) +register_tag('if', do_if) +register_tag('ifchanged', do_ifchanged) +register_tag('regroup', do_regroup) +register_tag('ssi', do_ssi) +register_tag('load', do_load) +register_tag('now', do_now) +register_tag('templatetag', do_templatetag) +register_tag('widthratio', do_widthratio) diff --git a/django/core/template/loader.py b/django/core/template/loader.py new file mode 100644 index 0000000000..65a6c735b3 --- /dev/null +++ b/django/core/template/loader.py @@ -0,0 +1,163 @@ +"Wrapper for loading templates from storage of some sort (e.g. files or db)" + +from django.core.template import Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, register_tag +from django.core.template.loaders.filesystem import load_template_source + +class ExtendsError(Exception): + pass + +def get_template(template_name): + """ + Returns a compiled Template object for the given template name, + handling template inheritance recursively. + """ + return get_template_from_string(load_template_source(template_name)) + +def get_template_from_string(source): + """ + Returns a compiled Template object for the given template code, + handling template inheritance recursively. + """ + return Template(source) + +def render_to_string(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + dictionary = dictionary or {} + if isinstance(template_name, (list, tuple)): + t = select_template(template_name) + else: + t = get_template(template_name) + if context_instance: + context_instance.update(dictionary) + else: + context_instance = Context(dictionary) + return t.render(context_instance) + +def select_template(template_name_list): + "Given a list of template names, returns the first that can be loaded." + for template_name in template_name_list: + try: + return get_template(template_name) + except TemplateDoesNotExist: + continue + # If we get here, none of the templates could be loaded + raise TemplateDoesNotExist, ', '.join(template_name_list) + +class BlockNode(Node): + def __init__(self, name, nodelist, parent=None): + self.name, self.nodelist, self.parent = name, nodelist, parent + + def __repr__(self): + return "" % (self.name, self.nodelist) + + def render(self, context): + context.push() + # Save context in case of block.super(). + self.context = context + context['block'] = self + result = self.nodelist.render(context) + context.pop() + return result + + def super(self): + if self.parent: + return self.parent.render(self.context) + return '' + + def add_parent(self, nodelist): + if self.parent: + self.parent.add_parent(nodelist) + else: + self.parent = BlockNode(self.name, nodelist) + +class ExtendsNode(Node): + def __init__(self, nodelist, parent_name, parent_name_var, template_dirs=None): + self.nodelist = nodelist + self.parent_name, self.parent_name_var = parent_name, parent_name_var + self.template_dirs = template_dirs + + def get_parent(self, context): + if self.parent_name_var: + self.parent_name = resolve_variable_with_filters(self.parent_name_var, context) + parent = self.parent_name + if not parent: + error_msg = "Invalid template name in 'extends' tag: %r." % parent + if self.parent_name_var: + error_msg += " Got this from the %r variable." % self.parent_name_var + raise TemplateSyntaxError, error_msg + try: + return get_template_from_string(load_template_source(parent, self.template_dirs)) + except TemplateDoesNotExist: + raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent + + def render(self, context): + compiled_parent = self.get_parent(context) + parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) + parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) + for block_node in self.nodelist.get_nodes_by_type(BlockNode): + # Check for a BlockNode with this node's name, and replace it if found. + try: + parent_block = parent_blocks[block_node.name] + except KeyError: + # This BlockNode wasn't found in the parent template, but the + # parent block might be defined in the parent's *parent*, so we + # add this BlockNode to the parent's ExtendsNode nodelist, so + # it'll be checked when the parent node's render() is called. + if parent_is_child: + compiled_parent.nodelist[0].nodelist.append(block_node) + else: + # Keep any existing parents and add a new one. Used by BlockNode. + parent_block.parent = block_node.parent + parent_block.add_parent(parent_block.nodelist) + parent_block.nodelist = block_node.nodelist + return compiled_parent.render(context) + +def do_block(parser, token): + """ + Define a block that can be overridden by child templates. + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0] + block_name = bits[1] + # Keep track of the names of BlockNodes found in this template, so we can + # check for duplication. + try: + if block_name in parser.__loaded_blocks: + raise TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) + 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',)) + parser.delete_first_token() + return BlockNode(block_name, nodelist) + +def do_extends(parser, token): + """ + Signal that this template extends a parent template. + + This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) + uses the literal value "base" as the name of the parent template to extend, + or ``{% entends variable %}`` uses the value of ``variable`` as the name + of the parent template to extend. + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'%s' takes one argument" % bits[0] + parent_name, parent_name_var = None, None + if (bits[1].startswith('"') and bits[1].endswith('"')) or (bits[1].startswith("'") and bits[1].endswith("'")): + parent_name = bits[1][1:-1] + else: + parent_name_var = bits[1] + nodelist = parser.parse() + if nodelist.get_nodes_by_type(ExtendsNode): + raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] + return ExtendsNode(nodelist, parent_name, parent_name_var) + +register_tag('block', do_block) +register_tag('extends', do_extends) diff --git a/django/core/template/loaders/__init__.py b/django/core/template/loaders/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/core/template/loaders/filesystem.py b/django/core/template/loaders/filesystem.py new file mode 100644 index 0000000000..849f62b94e --- /dev/null +++ b/django/core/template/loaders/filesystem.py @@ -0,0 +1,21 @@ +# Wrapper for loading templates from the filesystem. + +from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION +from django.core.template import TemplateDoesNotExist +import os + +def load_template_source(template_name, template_dirs=None): + if not template_dirs: + template_dirs = TEMPLATE_DIRS + tried = [] + for template_dir in template_dirs: + filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION + try: + return open(filepath).read() + except IOError: + tried.append(filepath) + if template_dirs: + error_msg = "Tried %s" % tried + else: + error_msg = "Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory." + raise TemplateDoesNotExist, error_msg diff --git a/django/core/template_file.py b/django/core/template_file.py deleted file mode 100644 index 6f5a324d61..0000000000 --- a/django/core/template_file.py +++ /dev/null @@ -1,21 +0,0 @@ -# Wrapper for loading templates from files - -from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION -from django.core.template import TemplateDoesNotExist -import os - -def load_template_source(template_name, template_dirs=None): - if not template_dirs: - template_dirs = TEMPLATE_DIRS - tried = [] - for template_dir in template_dirs: - filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION - try: - return open(filepath).read() - except IOError: - tried.append(filepath) - if template_dirs: - error_msg = "Tried %s" % tried - else: - error_msg = "Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory." - raise TemplateDoesNotExist, error_msg diff --git a/django/core/template_loader.py b/django/core/template_loader.py index 7c26cf0faa..e268c390e1 100644 --- a/django/core/template_loader.py +++ b/django/core/template_loader.py @@ -1,162 +1,7 @@ -"Wrapper for loading templates from storage of some sort (e.g. files or db)" -import template -from template_file import load_template_source +# This module is DEPRECATED! +# +# You should no longer be using django.core.template_loader. +# +# Use django.core.template.loader instead. -class ExtendsError(Exception): - pass - -def get_template(template_name): - """ - Returns a compiled template.Template object for the given template name, - handling template inheritance recursively. - """ - return get_template_from_string(load_template_source(template_name)) - -def get_template_from_string(source): - """ - Returns a compiled template.Template object for the given template code, - handling template inheritance recursively. - """ - return template.Template(source) - -def render_to_string(template_name, dictionary=None, context_instance=None): - """ - Loads the given template_name and renders it with the given dictionary as - context. The template_name may be a string to load a single template using - get_template, or it may be a tuple to use select_template to find one of - the templates in the list. Returns a string. - """ - dictionary = dictionary or {} - if isinstance(template_name, (list, tuple)): - t = select_template(template_name) - else: - t = get_template(template_name) - if context_instance: - context_instance.update(dictionary) - else: - context_instance = template.Context(dictionary) - return t.render(context_instance) - -def select_template(template_name_list): - "Given a list of template names, returns the first that can be loaded." - for template_name in template_name_list: - try: - return get_template(template_name) - except template.TemplateDoesNotExist: - continue - # If we get here, none of the templates could be loaded - raise template.TemplateDoesNotExist, ', '.join(template_name_list) - -class BlockNode(template.Node): - def __init__(self, name, nodelist, parent=None): - self.name, self.nodelist, self.parent = name, nodelist, parent - - def __repr__(self): - return "" % (self.name, self.nodelist) - - def render(self, context): - context.push() - # Save context in case of block.super(). - self.context = context - context['block'] = self - result = self.nodelist.render(context) - context.pop() - return result - - def super(self): - if self.parent: - return self.parent.render(self.context) - return '' - - def add_parent(self, nodelist): - if self.parent: - self.parent.add_parent(nodelist) - else: - self.parent = BlockNode(self.name, nodelist) - -class ExtendsNode(template.Node): - def __init__(self, nodelist, parent_name, parent_name_var, template_dirs=None): - self.nodelist = nodelist - self.parent_name, self.parent_name_var = parent_name, parent_name_var - self.template_dirs = template_dirs - - def get_parent(self, context): - if self.parent_name_var: - self.parent_name = template.resolve_variable_with_filters(self.parent_name_var, context) - parent = self.parent_name - if not parent: - error_msg = "Invalid template name in 'extends' tag: %r." % parent - if self.parent_name_var: - error_msg += " Got this from the %r variable." % self.parent_name_var - raise template.TemplateSyntaxError, error_msg - try: - return get_template_from_string(load_template_source(parent, self.template_dirs)) - except template.TemplateDoesNotExist: - raise template.TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent - - def render(self, context): - compiled_parent = self.get_parent(context) - parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) - parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) - for block_node in self.nodelist.get_nodes_by_type(BlockNode): - # Check for a BlockNode with this node's name, and replace it if found. - try: - parent_block = parent_blocks[block_node.name] - except KeyError: - # This BlockNode wasn't found in the parent template, but the - # parent block might be defined in the parent's *parent*, so we - # add this BlockNode to the parent's ExtendsNode nodelist, so - # it'll be checked when the parent node's render() is called. - if parent_is_child: - compiled_parent.nodelist[0].nodelist.append(block_node) - else: - # Keep any existing parents and add a new one. Used by BlockNode. - parent_block.parent = block_node.parent - parent_block.add_parent(parent_block.nodelist) - parent_block.nodelist = block_node.nodelist - return compiled_parent.render(context) - -def do_block(parser, token): - """ - Define a block that can be overridden by child templates. - """ - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0] - block_name = bits[1] - # Keep track of the names of BlockNodes found in this template, so we can - # check for duplication. - try: - if block_name in parser.__loaded_blocks: - raise template.TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) - 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',)) - parser.delete_first_token() - return BlockNode(block_name, nodelist) - -def do_extends(parser, token): - """ - Signal that this template extends a parent template. - - This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) - uses the literal value "base" as the name of the parent template to extend, - or ``{% entends variable %}`` uses the value of ``variable`` as the name - of the parent template to extend. - """ - bits = token.contents.split() - if len(bits) != 2: - raise template.TemplateSyntaxError, "'%s' takes one argument" % bits[0] - parent_name, parent_name_var = None, None - if (bits[1].startswith('"') and bits[1].endswith('"')) or (bits[1].startswith("'") and bits[1].endswith("'")): - parent_name = bits[1][1:-1] - else: - parent_name_var = bits[1] - nodelist = parser.parse() - if nodelist.get_nodes_by_type(ExtendsNode): - raise template.TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] - return ExtendsNode(nodelist, parent_name, parent_name_var) - -template.register_tag('block', do_block) -template.register_tag('extends', do_extends) +from django.core.template.loader import * diff --git a/django/middleware/admin.py b/django/middleware/admin.py index ff21689646..9c20cfc31c 100644 --- a/django/middleware/admin.py +++ b/django/middleware/admin.py @@ -1,6 +1,6 @@ from django.utils import httpwrappers -from django.core import template_loader -from django.core.extensions import DjangoContext as Context +from django.core.extensions import DjangoContext +from django.core.extensions import render_to_response from django.models.auth import users from django.views.registration import passwords from django.views.auth.login import logout @@ -96,14 +96,12 @@ class AdminUserRequired: post_data = encode_post_data(request.POST) else: post_data = encode_post_data({}) - t = template_loader.get_template(self.get_login_template_name()) - c = Context(request, { + return render_to_response(self.get_login_template_name(), { 'title': 'Log in', 'app_path': request.path, 'post_data': post_data, 'error_message': error_message - }) - return httpwrappers.HttpResponse(t.render(c)) + }, context_instance=DjangoContext(request)) def authenticate_user(self, user, password): return user.check_password(password) and user.is_staff diff --git a/django/views/admin/doc.py b/django/views/admin/doc.py index 38d7fdc572..64785385c4 100644 --- a/django/views/admin/doc.py +++ b/django/views/admin/doc.py @@ -4,7 +4,8 @@ from django.conf import settings from django.models.core import sites from django.core.extensions import DjangoContext, render_to_response from django.core.exceptions import Http404, ViewDoesNotExist -from django.core import template, template_loader, defaulttags, defaultfilters, urlresolvers +from django.core import template, template_loader, urlresolvers +from django.core.template import defaulttags, defaultfilters try: from django.parts.admin import doc except ImportError: diff --git a/django/views/admin/template.py b/django/views/admin/template.py index 2bf4f1a1d8..c35770178e 100644 --- a/django/views/admin/template.py +++ b/django/views/admin/template.py @@ -1,5 +1,6 @@ -from django.core import formfields, template_loader, validators +from django.core import formfields, validators from django.core import template +from django.core.template import loader from django.core.extensions import DjangoContext, render_to_response from django.models.core import sites from django.conf import settings @@ -49,7 +50,7 @@ class TemplateValidator(formfields.Manipulator): # so that inheritance works in the site's context, register a new function # for "extends" that uses the site's TEMPLATE_DIR instead def new_do_extends(parser, token): - node = template_loader.do_extends(parser, token) + node = loader.do_extends(parser, token) node.template_dirs = settings_module.TEMPLATE_DIRS return node template.register_tag('extends', new_do_extends) @@ -58,10 +59,10 @@ class TemplateValidator(formfields.Manipulator): # making sure to reset the extends function in any case error = None try: - tmpl = template_loader.get_template_from_string(field_data) + tmpl = loader.get_template_from_string(field_data) tmpl.render(template.Context({})) except template.TemplateSyntaxError, e: error = e - template.register_tag('extends', template_loader.do_extends) + template.register_tag('extends', loader.do_extends) if error: raise validators.ValidationError, e.args diff --git a/django/views/auth/login.py b/django/views/auth/login.py index 75a80c7907..6950ad7efe 100644 --- a/django/views/auth/login.py +++ b/django/views/auth/login.py @@ -1,5 +1,5 @@ from django.parts.auth.formfields import AuthenticationForm -from django.core import formfields, template_loader +from django.core import formfields from django.core.extensions import DjangoContext, render_to_response from django.models.auth import users from django.models.core import sites diff --git a/django/views/defaults.py b/django/views/defaults.py index c75bc6880e..d283e54c1b 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,6 +1,5 @@ -from django.core import template_loader from django.core.exceptions import Http404, ObjectDoesNotExist -from django.core.template import Context +from django.core.template import Context, loader from django.models.core import sites from django.utils import httpwrappers @@ -56,7 +55,7 @@ def page_not_found(request): if r == '': return httpwrappers.HttpResponseGone() return httpwrappers.HttpResponseRedirect(r.new_path) - t = template_loader.get_template('404') + t = loader.get_template('404') c = Context() return httpwrappers.HttpResponseNotFound(t.render(c)) @@ -67,6 +66,6 @@ def server_error(request): Templates: `500` Context: None """ - t = template_loader.get_template('500') + t = loader.get_template('500') c = Context() return httpwrappers.HttpResponseServerError(t.render(c)) diff --git a/django/views/registration/passwords.py b/django/views/registration/passwords.py index 662490eec7..09d3037560 100644 --- a/django/views/registration/passwords.py +++ b/django/views/registration/passwords.py @@ -1,5 +1,6 @@ -from django.core import formfields, template_loader, validators +from django.core import formfields, validators from django.core.extensions import DjangoContext, render_to_response +from django.core.template import loader from django.models.auth import users from django.views.decorators.auth import login_required from django.utils.httpwrappers import HttpResponseRedirect @@ -32,7 +33,7 @@ class PasswordResetForm(formfields.Manipulator): domain = current_site.domain else: site_name = domain = domain_override - t = template_loader.get_template('registration/password_reset_email') + t = loader.get_template('registration/password_reset_email') c = { 'new_password': new_pass, 'email': self.user_cache.email, diff --git a/docs/forms.txt b/docs/forms.txt index c93db95bcd..969ebd428d 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -65,9 +65,8 @@ Using the ``AddManipulator`` We'll start with the ``AddManipulator``. Here's a very simple view that takes POSTed data from the browser and creates a new ``Place`` object:: - from django.core import template_loader from django.core.exceptions import Http404 - from django.core.extensions import DjangoContext as Context + from django.core.extensions import render_to_response from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect from django.models.places import places from django.core import formfields @@ -112,13 +111,7 @@ view with a form that submits to this flawed creation view:: # Create a FormWrapper object that the template can use. Ignore # the last two arguments to FormWrapper for now. form = formfields.FormWrapper(places.AddManipulator(), {}, {}) - - # Create a template, context and response. - t = template_loader.get_template('places/naive_create_form') - c = Context(request, { - 'form': form - }) - return HttpResponse(t.render(c)) + return render_to_response('places/naive_create_form', {'form': form}) (This view, as well as all the following ones, has the same imports as in the first example above.) @@ -169,11 +162,7 @@ creation view that takes validation into account:: # Check for validation errors errors = manipulator.get_validation_errors(new_data) if errors: - t = template_loader.get_template('places/errors') - c = Context(request, { - 'errors': errors - } - return HttpResponse(t.render(c)) + return render_to_response('places/errors', {'errors': errors}) else: manipulator.do_html2python(request.POST) new_place = manipulator.save(request.POST) @@ -245,11 +234,7 @@ Below is the finished view:: # Create the FormWrapper, template, context, response. form = formfields.FormWrapper(manipulator, new_data, errors) - t = template_loader.get_template("places/create_form") - c = Context(request, { - 'form': form, - }) - return HttpResponse(t.render(c)) + return render_to_response('places/create_form', {'form': form}) and here's the ``create_form`` template:: @@ -338,12 +323,7 @@ about editing an existing one? It's shockingly similar to creating a new one:: new_data = place.__dict__ form = formfields.FormWrapper(manipulator, new_data, errors) - t = template_loader.get_template("places/edit_form") - c = Context(request, { - 'form': form, - 'place': place, - }) - return HttpResponse(t.render(c)) + return render_to_response('places/edit_form', {'form': form, 'place': place}) The only real differences are: @@ -422,11 +402,7 @@ Here's a simple function that might drive the above form:: else: errors = new_data = {} form = formfields.FormWrapper(manipulator, new_data, errors) - t = template_loader.get_template("contact_form") - c = Context(request, { - 'form': form, - }) - return HttpResponse(t.render(c)) + return render_to_response('contact_form', {'form': form}) Validators ========== diff --git a/docs/sessions.txt b/docs/sessions.txt index 22d06fdedd..b18ca25a2c 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -136,9 +136,7 @@ Here's a typical usage example:: else: return HttpResponse("Please enable cookies and try again.") request.session.set_test_cookie() - t = template_loader.get_template("foo/login_form") - c = Context(request) - return HttpResponse(t.render(c)) + return render_to_response('foo/login_form') Using sessions out of views =========================== diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 39b768429b..bd8aea62c7 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -307,12 +307,12 @@ The Python API Django has two ways to load templates from files: -``django.core.template_loader.get_template(template_name)`` +``django.core.template.loader.get_template(template_name)`` ``get_template`` returns the compiled template (a ``Template`` object) for the template with the given name. If the template doesn't exist, it raises ``django.core.template.TemplateDoesNotExist``. -``django.core.template_loader.select_template(template_name_list)`` +``django.core.template.loader.select_template(template_name_list)`` ``select_template`` is just like ``get_template``, except it takes a list of template names. Of the list, it returns the first template that exists. @@ -398,8 +398,8 @@ Python code, depending on whether you're writing filters or tags. .. admonition:: Behind the scenes For a ton of examples, read the source code for Django's default filters - and tags. They're in ``django/core/defaultfilters.py`` and - ``django/core/defaulttags.py``, respectively. + and tags. They're in ``django/core/template/defaultfilters.py`` and + ``django/core/template/defaulttags.py``, respectively. Writing custom template filters ------------------------------- @@ -710,4 +710,4 @@ The only new concept here is the ``self.nodelist.render(context)`` in For more examples of complex rendering, see the source code for ``{% if %}``, ``{% for %}``, ``{% ifequal %}`` and ``{% ifchanged %}``. They live in -``django/core/defaulttags.py``. +``django/core/template/defaulttags.py``. diff --git a/docs/tutorial03.txt b/docs/tutorial03.txt index 84fb64abfe..f602bf2ebd 100644 --- a/docs/tutorial03.txt +++ b/docs/tutorial03.txt @@ -191,14 +191,13 @@ There's a problem here, though: The page's design is hard-coded in the view. If you want to change the way the page looks, you'll have to edit this Python code. So let's use Django's template system to separate the design from Python:: - from django.core import template_loader - from django.core.template import Context + from django.core.template import Context, loader from django.models.polls import polls from django.utils.httpwrappers import HttpResponse def index(request): latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5) - t = template_loader.get_template('polls/index') + t = loader.get_template('polls/index') c = Context({ 'latest_poll_list': latest_poll_list, }) @@ -224,7 +223,7 @@ and feel" section of Tutorial 2. When you've done that, create a directory ``polls`` in your template directory. Within that, create a file called ``index.html``. Django requires that templates have ".html" extension. Note that our -``template_loader.get_template('polls/index')`` code from above maps to +``loader.get_template('polls/index')`` code from above maps to "[template_directory]/polls/index.html" on the filesystem. Put the following code in that template:: @@ -256,7 +255,7 @@ provides a shortcut. Here's the full ``index()`` view, rewritten:: latest_poll_list = polls.get_list(order_by=['-pub_date'], limit=5) return render_to_response('polls/index', {'latest_poll_list': latest_poll_list}) -Note that we no longer need to import ``template_loader``, ``Context`` or +Note that we no longer need to import ``loader``, ``Context`` or ``HttpResponse``. The ``render_to_response()`` function takes a template name as its first diff --git a/docs/tutorial04.txt b/docs/tutorial04.txt index 3e6d6205bb..737d8deb1f 100644 --- a/docs/tutorial04.txt +++ b/docs/tutorial04.txt @@ -197,8 +197,8 @@ objects" and "display a detail page for a particular type of object." By default, the ``object_detail`` generic view uses a template called ``/_detail``. In our case, it'll use the template ``"polls/polls_detail"``. Thus, rename your ``polls/detail.html`` template to -``polls/polls_detail.html``, and change the ``template_loader.get_template()`` -line in ``vote()``. +``polls/polls_detail.html``, and change the ``render_to_response()`` line in +``vote()``. Similarly, the ``object_list`` generic view uses a template called ``/_list``. Thus, rename ``polls/index.html`` to diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py index fb96cfeadd..f2da069ea2 100644 --- a/tests/othertests/templates.py +++ b/tests/othertests/templates.py @@ -1,4 +1,5 @@ -from django.core import template, template_loader +from django.core import template +from django.core.template import loader # Helper objects for template tests class SomeClass: @@ -216,7 +217,7 @@ TEMPLATE_TESTS = { 'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError), } -# This replaces the standard template_loader. +# This replaces the standard template loader. def test_template_loader(template_name, template_dirs=None): try: return TEMPLATE_TESTS[template_name][0] @@ -224,13 +225,13 @@ def test_template_loader(template_name, template_dirs=None): raise template.TemplateDoesNotExist, template_name def run_tests(verbosity=0, standalone=False): - template_loader.load_template_source, old_template_loader = test_template_loader, template_loader.load_template_source + loader.load_template_source, old_template_loader = test_template_loader, loader.load_template_source failed_tests = [] tests = TEMPLATE_TESTS.items() tests.sort() for name, vals in tests: try: - output = template_loader.get_template(name).render(template.Context(vals[1])) + output = loader.get_template(name).render(template.Context(vals[1])) except Exception, e: if e.__class__ == vals[2]: if verbosity: @@ -247,7 +248,7 @@ def run_tests(verbosity=0, standalone=False): if verbosity: print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output) failed_tests.append(name) - template_loader.load_template_source = old_template_loader + loader.load_template_source = old_template_loader if failed_tests and not standalone: msg = "Template tests %s failed." % failed_tests -- cgit v1.3 From 083b4f9001f3968138c4d0c34ab741cf7be97f43 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Fri, 14 Oct 2005 20:12:16 +0000 Subject: Redid [865], which got lost in [867] git-svn-id: http://code.djangoproject.com/svn/django/trunk@868 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/core/template/defaultfilters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django/core/template/defaultfilters.py b/django/core/template/defaultfilters.py index 9ef367ee52..746fa9b873 100644 --- a/django/core/template/defaultfilters.py +++ b/django/core/template/defaultfilters.py @@ -423,6 +423,7 @@ register_filter('center', center, True) register_filter('cut', cut, True) register_filter('date', date, True) register_filter('default', default, True) +register_filter('default_if_none', default_if_none, True) register_filter('dictsort', dictsort, True) register_filter('dictsortreversed', dictsortreversed, True) register_filter('divisibleby', divisibleby, True) -- cgit v1.3