1
0
mirror of https://github.com/django/django.git synced 2025-07-04 17:59:13 +00:00

[multi-db] Merged trunk to [4050]. Some tests still failing.

git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@4157 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jason Pellerin 2006-12-04 20:52:14 +00:00
parent b7a897eebb
commit 478ebadec7
34 changed files with 334 additions and 102 deletions

View File

@ -101,6 +101,7 @@ 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.
DATABASE_OPTIONS = {} # Set to empy dictionary for default.
# Optional named database connections in addition to the default.
OTHER_DATABASES = {}
@ -231,6 +232,10 @@ MONTH_DAY_FORMAT = 'F j'
# Hint: you really don't!
TRANSACTIONS_MANAGED = False
# The User-Agent string to use when checking for URL validity through the
# isExistingURL validator.
URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)"
##############
# MIDDLEWARE #
##############

View File

@ -169,8 +169,8 @@ var dateParsePatterns = [
handler: function(bits) {
var d = new Date();
d.setYear(parseInt(bits[1]));
d.setDate(parseInt(bits[3], 10));
d.setMonth(parseInt(bits[2], 10) - 1);
d.setDate(parseInt(bits[3], 10));
return d;
}
},

View File

@ -177,8 +177,8 @@ def output_all(form_fields):
output_all = register.simple_tag(output_all)
def auto_populated_field_script(auto_pop_fields, change = False):
t = []
for field in auto_pop_fields:
t = []
if change:
t.append('document.getElementById("id_%s")._changed = true;' % field.name)
else:

View File

@ -34,7 +34,7 @@ class CommentManager(models.Manager):
"""
Given a rating_string, this returns a tuple of (rating_range, options).
>>> s = "scale:1-10|First_category|Second_category"
>>> get_rating_options(s)
>>> Comment.objects.get_rating_options(s)
([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category'])
"""
rating_range, options = rating_string.split('|', 1)

View File

@ -186,6 +186,33 @@ def get_sql_reset(app):
get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given app name(s)."
get_sql_reset.args = APP_ARGS
def get_sql_initial_data_for_model(model):
from django.db import models
from django.conf import settings
opts = model._meta
app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))
output = []
# Some backends can't execute more than one SQL statement at a time,
# so split into separate statements.
statements = re.compile(r";[ \t]*$", re.M)
# Find custom SQL, if it's available.
sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.object_name.lower(), settings.DATABASE_ENGINE)),
os.path.join(app_dir, "%s.sql" % opts.object_name.lower())]
for sql_file in sql_files:
if os.path.exists(sql_file):
fp = open(sql_file, 'U')
for statement in statements.split(fp.read()):
# Remove any comments from the file
statement = re.sub(r"--.*[\n\Z]", "", statement)
if statement.strip():
output.append(statement + ";")
fp.close()
return output
def get_sql_initial_data(app):
"Returns a list of the initial INSERT SQL statements for the given app."
from django.db import model_connection_name

View File

@ -1,54 +1,46 @@
from math import ceil
class InvalidPage(Exception):
pass
class ObjectPaginator(object):
"""
This class makes pagination easy. Feed it a QuerySet, plus the number of
objects you want on each page. Then read the hits and pages properties to
This class makes pagination easy. Feed it a QuerySet or list, plus the number
of objects you want on each page. Then read the hits and pages properties to
see how many pages it involves. Call get_page with a page number (starting
at 0) to get back a list of objects for that page.
Finally, check if a page number has a next/prev page using
has_next_page(page_number) and has_previous_page(page_number).
Use orphans to avoid small final pages. For example:
13 records, num_per_page=10, orphans=2 --> pages==2, len(self.get_page(0))==10
12 records, num_per_page=10, orphans=2 --> pages==1, len(self.get_page(0))==12
"""
def __init__(self, query_set, num_per_page):
def __init__(self, query_set, num_per_page, orphans=0):
self.query_set = query_set
self.num_per_page = num_per_page
self._hits, self._pages = None, None
self._has_next = {} # Caches page_number -> has_next_boolean
self.orphans = orphans
self._hits = self._pages = None
def get_page(self, page_number):
def validate_page_number(self, page_number):
try:
page_number = int(page_number)
except ValueError:
raise InvalidPage
if page_number < 0:
if page_number < 0 or page_number > self.pages - 1:
raise InvalidPage
return page_number
# Retrieve one extra record, and check for the existence of that extra
# record to determine whether there's a next page.
limit = self.num_per_page + 1
offset = page_number * self.num_per_page
object_list = list(self.query_set[offset:offset+limit])
if not object_list:
raise InvalidPage
self._has_next[page_number] = (len(object_list) > self.num_per_page)
return object_list[:self.num_per_page]
def get_page(self, page_number):
page_number = self.validate_page_number(page_number)
bottom = page_number * self.num_per_page
top = bottom + self.num_per_page
if top + self.orphans >= self.hits:
top = self.hits
return self.query_set[bottom:top]
def has_next_page(self, page_number):
"Does page $page_number have a 'next' page?"
if not self._has_next.has_key(page_number):
if self._pages is None:
offset = (page_number + 1) * self.num_per_page
self._has_next[page_number] = len(self.query_set[offset:offset+1]) > 0
else:
self._has_next[page_number] = page_number < (self.pages - 1)
return self._has_next[page_number]
return page_number < self.pages - 1
def has_previous_page(self, page_number):
return page_number > 0
@ -58,8 +50,7 @@ class ObjectPaginator(object):
Returns the 1-based index of the first object on the given page,
relative to total objects found (hits).
"""
if page_number == 0:
return 1
page_number = self.validate_page_number(page_number)
return (self.num_per_page * page_number) + 1
def last_on_page(self, page_number):
@ -67,20 +58,30 @@ class ObjectPaginator(object):
Returns the 1-based index of the last object on the given page,
relative to total objects found (hits).
"""
if page_number == 0 and self.num_per_page >= self._hits:
return self._hits
elif page_number == (self._pages - 1) and (page_number + 1) * self.num_per_page > self._hits:
return self._hits
return (page_number + 1) * self.num_per_page
page_number = self.validate_page_number(page_number)
page_number += 1 # 1-base
if page_number == self.pages:
return self.hits
return page_number * self.num_per_page
def _get_hits(self):
if self._hits is None:
self._hits = self.query_set.count()
# Try .count() or fall back to len().
try:
self._hits = int(self.query_set.count())
except (AttributeError, TypeError, ValueError):
# AttributeError if query_set has no object count.
# TypeError if query_set.count() required arguments.
# ValueError if int() fails.
self._hits = len(self.query_set)
return self._hits
def _get_pages(self):
if self._pages is None:
self._pages = int(ceil(self.hits / float(self.num_per_page)))
hits = (self.hits - 1 - self.orphans)
if hits < 1:
hits = 0
self._pages = hits // self.num_per_page + 1
return self._pages
hits = property(_get_hits)

View File

@ -28,6 +28,7 @@ class Serializer(object):
self.options = options
self.stream = options.get("stream", StringIO())
self.selected_fields = options.get("fields")
self.start_serialization()
for obj in queryset:
@ -36,11 +37,14 @@ class Serializer(object):
if field is obj._meta.pk:
continue
elif field.rel is None:
self.handle_field(obj, field)
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_field(obj, field)
else:
self.handle_fk_field(obj, field)
if self.selected_fields is None or field.attname[:-3] in self.selected_fields:
self.handle_fk_field(obj, field)
for field in obj._meta.many_to_many:
self.handle_m2m_field(obj, field)
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_m2m_field(obj, field)
self.end_object(obj)
self.end_serialization()
return self.getvalue()

View File

@ -76,7 +76,7 @@ def Deserializer(object_list, **options):
m2m_data[field.name] = field.rel.to._default_manager.in_bulk(field_value).values()
# Handle FK fields
elif field.rel and isinstance(field.rel, models.ManyToOneRel):
elif field.rel and isinstance(field.rel, models.ManyToOneRel) and field_value is not None:
try:
data[field.name] = field.rel.to._default_manager.get(pk=field_value)
except field.rel.to.DoesNotExist:

View File

@ -166,7 +166,11 @@ class Deserializer(base.Deserializer):
# If it doesn't exist, set the field to None (which might trigger
# validation error, but that's expected).
RelatedModel = self._get_model_from_node(node, "to")
return RelatedModel.objects.get(pk=getInnerText(node).strip().encode(self.encoding))
# Check if there is a child node named 'None', returning None if so.
if len(node.childNodes) == 1 and node.childNodes[0].nodeName == 'None':
return None
else:
return RelatedModel.objects.get(pk=getInnerText(node).strip().encode(self.encoding))
def _handle_m2m_field_node(self, node):
"""

View File

@ -33,9 +33,9 @@ Optional Fcgi settings: (setting=value)
method=IMPL prefork or threaded (default prefork)
maxrequests=NUMBER number of requests a child handles before it is
killed and a new child is forked (0 = no limit).
maxspare=NUMBER max number of spare processes to keep running.
minspare=NUMBER min number of spare processes to prefork.
maxchildren=NUMBER hard limit number of processes in prefork mode.
maxspare=NUMBER max number of spare processes / threads
minspare=NUMBER min number of spare processes / threads.
maxchildren=NUMBER hard limit number of processes / threads
daemonize=BOOL whether to detach from terminal.
pidfile=FILE write the spawned process-id to this file.
workdir=DIRECTORY change to this directory when daemonizing
@ -110,7 +110,11 @@ def runfastcgi(argset=[], **kwargs):
}
elif options['method'] in ('thread', 'threaded'):
from flup.server.fcgi import WSGIServer
wsgi_opts = {}
wsgi_opts = {
'maxSpare': int(options["maxspare"]),
'minSpare': int(options["minspare"]),
'maxThreads': int(options["maxchildren"]),
}
else:
return fastcgi_help("ERROR: Implementation must be one of prefork or thread.")

View File

@ -21,7 +21,10 @@ class NoReverseMatch(Exception):
def get_mod_func(callback):
# Converts 'django.views.news.stories.story_detail' to
# ['django.views.news.stories', 'story_detail']
dot = callback.rindex('.')
try:
dot = callback.rindex('.')
except ValueError:
return callback, ''
return callback[:dot], callback[dot+1:]
def reverse_helper(regex, *args, **kwargs):

View File

@ -8,6 +8,7 @@ validator will *always* be run, regardless of whether its associated
form field is required.
"""
import urllib2
from django.conf import settings
from django.utils.translation import gettext, gettext_lazy, ngettext
from django.utils.functional import Promise, lazy
@ -223,17 +224,25 @@ def isWellFormedXmlFragment(field_data, all_data):
isWellFormedXml('<root>%s</root>' % field_data, all_data)
def isExistingURL(field_data, all_data):
import urllib2
try:
u = urllib2.urlopen(field_data)
headers = {
"Accept" : "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
"Accept-Language" : "en-us,en;q=0.5",
"Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
"Connection" : "close",
"User-Agent": settings.URL_VALIDATOR_USER_AGENT
}
req = urllib2.Request(field_data,None, headers)
u = urllib2.urlopen(req)
except ValueError:
raise ValidationError, gettext("Invalid URL: %s") % field_data
raise ValidationError, _("Invalid URL: %s") % field_data
except urllib2.HTTPError, e:
# 401s are valid; they just mean authorization is required.
if e.code not in ('401',):
raise ValidationError, gettext("The URL %s is a broken link.") % field_data
# 301 and 302 are redirects; they just mean look somewhere else.
if str(e.code) not in ('401','301','302'):
raise ValidationError, _("The URL %s is a broken link.") % field_data
except: # urllib2.URLError, httplib.InvalidURL, etc.
raise ValidationError, gettext("The URL %s is a broken link.") % field_data
raise ValidationError, _("The URL %s is a broken link.") % field_data
def isValidUSState(field_data, all_data):
"Checks that the given string is a valid two-letter U.S. state abbreviation"
@ -344,6 +353,38 @@ class UniqueAmongstFieldsWithPrefix(object):
if field_name != self.field_name and value == field_data:
raise ValidationError, self.error_message
class NumberIsInRange(object):
"""
Validator that tests if a value is in a range (inclusive).
"""
def __init__(self, lower=None, upper=None, error_message=''):
self.lower, self.upper = lower, upper
if not error_message:
if lower and upper:
self.error_message = gettext("This value must be between %s and %s.") % (lower, upper)
elif lower:
self.error_message = gettext("This value must be at least %s.") % lower
elif upper:
self.error_message = gettext("This value must be no more than %s.") % upper
else:
self.error_message = error_message
def __call__(self, field_data, all_data):
# Try to make the value numeric. If this fails, we assume another
# validator will catch the problem.
try:
val = float(field_data)
except ValueError:
return
# Now validate
if self.lower and self.upper and (val < self.lower or val > self.upper):
raise ValidationError(self.error_message)
elif self.lower and val < self.lower:
raise ValidationError(self.error_message)
elif self.upper and val > self.upper:
raise ValidationError(self.error_message)
class IsAPowerOf(object):
"""
>>> v = IsAPowerOf(2)

View File

@ -48,6 +48,8 @@ class ConnectionInfo(object):
super(ConnectionInfo, self).__init__(**kw)
if settings is None:
from django.conf import settings
if not settings.DATABASE_OPTIONS:
settings.DATABASE_OPTIONS = {}
self.settings = settings
self.backend = self.load_backend()
self.connection = self.backend.DatabaseWrapper(settings)

View File

@ -50,6 +50,7 @@ Database.convertVariantToPython = variantToPython
class DatabaseWrapper(object):
def __init__(self, settings):
self.settings = settings
self.options = settings.DATABASE_OPTIONS
self.connection = None
self.queries = []

View File

@ -291,6 +291,8 @@ class SchemaBuilder(object):
output.append(BoundStatement(fp.read(), db.connection))
else:
for statement in statements.split(fp.read()):
# Remove any comments from the file
statement = re.sub(r"--.*[\n\Z]", "", statement)
if statement.strip():
output.append(BoundStatement(statement + ";",
db.connection))

View File

@ -64,6 +64,7 @@ class DatabaseWrapper(object):
self.connection = None
self.queries = []
self.server_version = None
self.options = settings.DATABASE_OPTIONS
def _valid_connection(self):
if self.connection is not None:
@ -90,6 +91,7 @@ class DatabaseWrapper(object):
kwargs['host'] = settings.DATABASE_HOST
if settings.DATABASE_PORT:
kwargs['port'] = int(settings.DATABASE_PORT)
kwargs.update(self.options)
self.connection = Database.connect(**kwargs)
cursor = self.connection.cursor()
if self.connection.get_server_info() >= '4.1':

View File

@ -18,6 +18,7 @@ class DatabaseWrapper(object):
self.settings = settings
self.connection = None
self.queries = []
self.options = settings.DATABASE_OPTIONS
def _valid_connection(self):
return self.connection is not None
@ -29,10 +30,10 @@ class DatabaseWrapper(object):
settings.DATABASE_HOST = 'localhost'
if len(settings.DATABASE_PORT.strip()) != 0:
dsn = Database.makedsn(settings.DATABASE_HOST, int(settings.DATABASE_PORT), settings.DATABASE_NAME)
self.connection = Database.connect(settings.DATABASE_USER, settings.DATABASE_PASSWORD, dsn)
self.connection = Database.connect(settings.DATABASE_USER, settings.DATABASE_PASSWORD, dsn, **self.options)
else:
conn_string = "%s/%s@%s" % (settings.DATABASE_USER, settings.DATABASE_PASSWORD, settings.DATABASE_NAME)
self.connection = Database.connect(conn_string)
self.connection = Database.connect(conn_string, **self.options)
return FormatStylePlaceholderCursor(self.connection)
def _commit(self):

View File

@ -18,6 +18,7 @@ class DatabaseWrapper(object):
self.settings = settings
self.connection = None
self.queries = []
self.options = settings.DATABASE_OPTIONS
def cursor(self):
settings = self.settings
@ -34,7 +35,7 @@ class DatabaseWrapper(object):
conn_string += " host=%s" % settings.DATABASE_HOST
if settings.DATABASE_PORT:
conn_string += " port=%s" % settings.DATABASE_PORT
self.connection = Database.connect(conn_string)
self.connection = Database.connect(conn_string, **self.options)
self.connection.set_isolation_level(1) # make transactions transparent to all cursors
cursor = self.connection.cursor()
cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE])

View File

@ -18,6 +18,7 @@ class DatabaseWrapper(object):
self.settings = settings
self.connection = None
self.queries = []
self.options = settings.DATABASE_OPTIONS
def cursor(self):
settings = self.settings
@ -34,7 +35,7 @@ class DatabaseWrapper(object):
conn_string += " host=%s" % settings.DATABASE_HOST
if settings.DATABASE_PORT:
conn_string += " port=%s" % settings.DATABASE_PORT
self.connection = Database.connect(conn_string)
self.connection = Database.connect(conn_string, **self.options)
self.connection.set_isolation_level(1) # make transactions transparent to all cursors
cursor = self.connection.cursor()
cursor.tzinfo_factory = None

View File

@ -46,13 +46,17 @@ class DatabaseWrapper(local):
self.settings = settings
self.connection = None
self.queries = []
self.options = settings.DATABASE_OPTIONS
def cursor(self):
settings = self.settings
if self.connection is None:
self.connection = Database.connect(settings.DATABASE_NAME,
detect_types=Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES)
kwargs = {
'database': settings.DATABASE_NAME,
'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
}
kwargs.update(self.options)
self.connection = Database.connect(**kwargs)
# Register extract and date_trunc functions.
self.connection.create_function("django_extract", 2, _sqlite_extract)
self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc)

View File

@ -17,7 +17,7 @@ class CursorDebugWrapper(object):
if not isinstance(params, (tuple, dict)):
params = tuple(params)
self.db.queries.append({
'sql': sql % tuple(params),
'sql': sql % params,
'time': "%.3f" % (stop - start),
})

View File

@ -591,7 +591,7 @@ class FileField(Field):
# If the raw path is passed in, validate it's under the MEDIA_ROOT.
def isWithinMediaRoot(field_data, all_data):
f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
if not f.startswith(os.path.normpath(settings.MEDIA_ROOT)):
if not f.startswith(os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))):
raise validators.ValidationError, _("Enter a valid filename.")
field_list[1].validator_list.append(isWithinMediaRoot)
return field_list

View File

@ -286,7 +286,7 @@ def manipulator_validator_unique_together(field_name_list, opts, self, field_dat
# This is really not going to work for fields that have different
# form fields, e.g. DateTime.
# This validation needs to occur after html2python to be effective.
field_val = all_data.get(f.attname, None)
field_val = all_data.get(f.name, None)
if field_val is None:
# This will be caught by another validator, assuming the field
# doesn't have blank=True.

View File

@ -108,8 +108,13 @@ class FormWrapper(object):
This allows dictionary-style lookups of formfields. It also handles feeding
prepopulated data and validation error messages to the formfield objects.
"""
def __init__(self, manipulator, data, error_dict, edit_inline=True):
self.manipulator, self.data = manipulator, data
def __init__(self, manipulator, data=None, error_dict=None, edit_inline=True):
self.manipulator = manipulator
if data is None:
data = {}
if error_dict is None:
error_dict = {}
self.data = data
self.error_dict = error_dict
self._inline_collections = None
self.edit_inline = edit_inline

View File

@ -188,17 +188,35 @@ url_re = re.compile(
r'(?::\d+)?' # optional port
r'(?:/?|/\S+)$', re.IGNORECASE)
try:
from django.conf import settings
URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT
except ImportError:
# It's OK if Django settings aren't configured.
URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)'
class URLField(RegexField):
def __init__(self, required=True, verify_exists=False, widget=None):
def __init__(self, required=True, verify_exists=False, widget=None,
validator_user_agent=URL_VALIDATOR_USER_AGENT):
RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget)
self.verify_exists = verify_exists
self.user_agent = validator_user_agent
def clean(self, value):
value = RegexField.clean(self, value)
if self.verify_exists:
import urllib2
from django.conf import settings
headers = {
"Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
"Accept-Language": "en-us,en;q=0.5",
"Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
"Connection": "close",
"User-Agent": self.user_agent,
}
try:
u = urllib2.urlopen(value)
req = urllib2.Request(field_data, None, headers)
u = urllib2.urlopen(req)
except ValueError:
raise ValidationError(u'Enter a valid URL.')
except: # urllib2.URLError, httplib.InvalidURL, etc.

View File

@ -868,8 +868,11 @@ class Library(object):
dict = func(*args)
if not getattr(self, 'nodelist', False):
from django.template.loader import get_template
t = get_template(file_name)
from django.template.loader import get_template, select_template
if hasattr(file_name, '__iter__'):
t = select_template(file_name)
else:
t = get_template(file_name)
self.nodelist = t.nodelist
return self.nodelist.render(context_class(dict))

View File

@ -421,7 +421,11 @@ 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)
try:
bytes = float(bytes)
except TypeError:
return "0 bytes"
if bytes < 1024:
return "%d byte%s" % (bytes, bytes != 1 and 's' or '')
if bytes < 1024 * 1024:

View File

@ -124,17 +124,27 @@ class ForNode(Node):
return nodelist.render(context)
class IfChangedNode(Node):
def __init__(self, nodelist):
def __init__(self, nodelist, *varlist):
self.nodelist = nodelist
self._last_seen = None
self._varlist = varlist
def render(self, context):
if context.has_key('forloop') and context['forloop']['first']:
self._last_seen = None
content = self.nodelist.render(context)
if content != self._last_seen:
try:
if self._varlist:
# Consider multiple parameters.
# This automatically behaves like a OR evaluation of the multiple variables.
compare_to = [resolve_variable(var, context) for var in self._varlist]
else:
compare_to = self.nodelist.render(context)
except VariableDoesNotExist:
compare_to = None
if compare_to != self._last_seen:
firstloop = (self._last_seen == None)
self._last_seen = content
self._last_seen = compare_to
context.push()
context['ifchanged'] = {'firstloop': firstloop}
content = self.nodelist.render(context)
@ -634,23 +644,34 @@ def 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::
The 'ifchanged' block tag is used within a loop. It has two possible uses.
<h1>Archive for {{ year }}</h1>
1. Checks its own rendered contents against its previous state and only
displays the content if it has changed. For example, this displays a list of
days, only displaying the month if it changes::
{% for date in days %}
{% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
<a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
{% endfor %}
<h1>Archive for {{ year }}</h1>
{% for date in days %}
{% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
<a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
{% endfor %}
2. If given a variable, check if that variable has changed. For example, the
following shows the date every time it changes, but only shows the hour if both
the hour and the date has changed::
{% for date in days %}
{% ifchanged date.date %} {{date.date}} {% endifchanged %}
{% ifchanged date.hour date.date %}
{{date.hour}}
{% endifchanged %}
{% 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)
return IfChangedNode(nodelist, *bits[1:])
ifchanged = register.tag(ifchanged)
#@register.tag

View File

@ -610,6 +610,15 @@ fails. If no message is passed in, a default message is used.
string "123" is less than the string "2", for example. If you don't want
string comparison here, you will need to write your own validator.
``NumberIsInRange``
Takes two boundary number, ``lower`` and ``upper`` and checks that the
field is greater than ``lower`` (if given) and less than ``upper`` (if
given).
Both checks are inclusive; that is, ``NumberIsInRange(10, 20)`` will allow
values of both 10 and 20. This validator only checks numeric fields
(i.e. floats and integer fields).
``IsAPowerOf``
Takes an integer argument and when called as a validator, checks that the
field being validated is a power of the integer.

View File

@ -265,6 +265,14 @@ Default: ``''`` (Empty string)
The name of the database to use. For SQLite, it's the full path to the database
file.
DATABASE_OPTIONS
----------------
Default: ``{}`` (Empty dictionary)
Extra parameters to use when connecting to the database. Consult backend
module's document for available keywords.
DATABASE_PASSWORD
-----------------
@ -821,6 +829,16 @@ manual configuration option (see below), Django will *not* touch the ``TZ``
environment variable, and it'll be up to you to ensure your processes are
running in the correct environment.
URL_VALIDATOR_USER_AGENT
------------------------
Default: ``Django/<version> (http://www.djangoproject.com/)``
The string to use as the ``User-Agent`` header when checking to see if URLs
exist (see the ``verify_exists`` option on URLField_).
.. URLField: ../model_api/#urlfield
USE_ETAGS
---------

View File

@ -95,7 +95,7 @@ latest five news items::
from django.contrib.syndication.feeds import Feed
from chicagocrime.models import NewsItem
class SiteNewsFeed(Feed):
class LatestEntries(Feed):
title = "Chicagocrime.org site news"
link = "/sitenews/"
description = "Updates on changes and additions to chicagocrime.org."
@ -120,8 +120,8 @@ One thing's left to do. In an RSS feed, each ``<item>`` has a ``<title>``,
put into those elements.
* To specify the contents of ``<title>`` and ``<description>``, create
`Django templates`_ called ``feeds/sitenews_title.html`` and
``feeds/sitenews_description.html``, where ``sitenews`` is the ``slug``
`Django templates`_ called ``feeds/latest_title.html`` and
``feeds/latest_description.html``, where ``latest`` is the ``slug``
specified in the URLconf for the given feed. Note the ``.html`` extension
is required. The RSS system renders that template for each item, passing
it two template context variables:
@ -145,6 +145,16 @@ put into those elements.
Both ``get_absolute_url()`` and ``item_link()`` should return the item's
URL as a normal Python string.
* For the LatestEntries example above, we could have very simple feed templates:
* latest_title.html::
{{ obj.title }}
* latest_description.html::
{{ obj.description }}
.. _chicagocrime.org: http://www.chicagocrime.org/
.. _object-relational mapper: http://www.djangoproject.com/documentation/db_api/
.. _Django templates: http://www.djangoproject.com/documentation/templates/

View File

@ -525,16 +525,29 @@ ifchanged
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::
The 'ifchanged' block tag is used within a loop. It has two possible uses.
<h1>Archive for {{ year }}</h1>
1. Checks its own rendered contents against its previous state and only
displays the content if it has changed. For example, this displays a list of
days, only displaying the month if it changes::
{% for day in days %}
{% ifchanged %}<h3>{{ day|date:"F" }}</h3>{% endifchanged %}
<a href="{{ day|date:"M/d"|lower }}/">{{ day|date:"j" }}</a>
{% endfor %}
<h1>Archive for {{ year }}</h1>
{% for date in days %}
{% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %}
<a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a>
{% endfor %}
2. If given a variable, check if that variable has changed. For example, the
following shows the date every time it changes, but only shows the hour if both
the hour and the date has changed::
{% for date in days %}
{% ifchanged date.date %} {{date.date}} {% endifchanged %}
{% ifchanged date.hour date.date %}
{{date.hour}}
{% endifchanged %}
{% endfor %}
ifequal
~~~~~~~

View File

@ -64,4 +64,17 @@ True
>>> paginator.last_on_page(1)
9
# Add a few more records to test out the orphans feature.
>>> for x in range(10, 13):
... Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save()
# With orphans set to 3 and 10 items per page, we should get all 12 items on a single page:
>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=3)
>>> paginator.pages
1
# With orphans only set to 1, we should get two pages:
>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1)
>>> paginator.pages
2
"""}

View File

@ -329,6 +329,21 @@ class Templates(unittest.TestCase):
'ifchanged06': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2)}, '1222'),
'ifchanged07': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% for y in numy %}{% ifchanged %}{{ y }}{% endifchanged %}{% endfor %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2), 'numy': (3, 3, 3)}, '1233323332333'),
# Test one parameter given to ifchanged.
'ifchanged-param01': ('{% for n in num %}{% ifchanged n %}..{% endifchanged %}{{ n }}{% endfor %}', { 'num': (1,2,3) }, '..1..2..3'),
'ifchanged-param02': ('{% for n in num %}{% for x in numx %}{% ifchanged n %}..{% endifchanged %}{{ x }}{% endfor %}{% endfor %}', { 'num': (1,2,3), 'numx': (5,6,7) }, '..567..567..567'),
# Test multiple parameters to ifchanged.
'ifchanged-param03': ('{% for n in num %}{{ n }}{% for x in numx %}{% ifchanged x n %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1,1,2), 'numx': (5,6,6) }, '156156256'),
# Test a date+hour like construct, where the hour of the last day
# is the same but the date had changed, so print the hour anyway.
'ifchanged-param04': ('{% for d in days %}{% ifchanged %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'),
# Logically the same as above, just written with explicit
# ifchanged for the day.
'ifchanged-param04': ('{% for d in days %}{% ifchanged d.day %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d.day h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'),
### IFEQUAL TAG ###########################################################
'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""),
'ifequal02': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 1}, "yes"),