mirror of
https://github.com/django/django.git
synced 2025-07-04 17:59:13 +00:00
sqlalchemy: Merged revisions 3679 to 3723 from trunk.
git-svn-id: http://code.djangoproject.com/svn/django/branches/sqlalchemy@3724 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
083ebb3468
commit
70e0581791
5
AUTHORS
5
AUTHORS
@ -104,7 +104,6 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
mattycakes@gmail.com
|
mattycakes@gmail.com
|
||||||
Jason McBrayer <http://www.carcosa.net/jason/>
|
Jason McBrayer <http://www.carcosa.net/jason/>
|
||||||
michael.mcewan@gmail.com
|
michael.mcewan@gmail.com
|
||||||
mir@noris.de
|
|
||||||
mmarshall
|
mmarshall
|
||||||
Eric Moritz <http://eric.themoritzfamily.com/>
|
Eric Moritz <http://eric.themoritzfamily.com/>
|
||||||
Robin Munn <http://www.geekforgod.com/>
|
Robin Munn <http://www.geekforgod.com/>
|
||||||
@ -121,11 +120,14 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
plisk
|
plisk
|
||||||
Daniel Poelzleithner <http://poelzi.org/>
|
Daniel Poelzleithner <http://poelzi.org/>
|
||||||
J. Rademaker
|
J. Rademaker
|
||||||
|
Michael Radziej <mir@noris.de>
|
||||||
Brian Ray <http://brianray.chipy.org/>
|
Brian Ray <http://brianray.chipy.org/>
|
||||||
rhettg@gmail.com
|
rhettg@gmail.com
|
||||||
Oliver Rutherfurd <http://rutherfurd.net/>
|
Oliver Rutherfurd <http://rutherfurd.net/>
|
||||||
Ivan Sagalaev (Maniac) <http://www.softwaremaniacs.org/>
|
Ivan Sagalaev (Maniac) <http://www.softwaremaniacs.org/>
|
||||||
David Schein
|
David Schein
|
||||||
|
Pete Shinners <pete@shinners.org>
|
||||||
|
SmileyChris <smileychris@gmail.com>
|
||||||
sopel
|
sopel
|
||||||
Thomas Steinacher <tom@eggdrop.ch>
|
Thomas Steinacher <tom@eggdrop.ch>
|
||||||
Radek Švarz <http://www.svarz.cz/translate/>
|
Radek Švarz <http://www.svarz.cz/translate/>
|
||||||
@ -138,6 +140,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Amit Upadhyay
|
Amit Upadhyay
|
||||||
Geert Vanderkelen
|
Geert Vanderkelen
|
||||||
Milton Waddams
|
Milton Waddams
|
||||||
|
Dan Watson <http://theidioteque.net/>
|
||||||
Rachel Willmer <http://www.willmer.com/kb/>
|
Rachel Willmer <http://www.willmer.com/kb/>
|
||||||
wojtek
|
wojtek
|
||||||
ye7cakf02@sneakemail.com
|
ye7cakf02@sneakemail.com
|
||||||
|
@ -301,4 +301,9 @@ AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
|
|||||||
# TESTING #
|
# TESTING #
|
||||||
###########
|
###########
|
||||||
|
|
||||||
TEST_RUNNER='django.test.simple.run_tests'
|
# The name of the method to use to invoke the test suite
|
||||||
|
TEST_RUNNER = 'django.test.simple.run_tests'
|
||||||
|
|
||||||
|
# The name of the database to use for testing purposes.
|
||||||
|
# If None, a name of 'test_' + DATABASE_NAME will be assumed
|
||||||
|
TEST_DATABASE_NAME = None
|
||||||
|
@ -328,13 +328,17 @@ def extract_views_from_urlpatterns(urlpatterns, base=''):
|
|||||||
"""
|
"""
|
||||||
views = []
|
views = []
|
||||||
for p in urlpatterns:
|
for p in urlpatterns:
|
||||||
if hasattr(p, 'get_callback'):
|
if hasattr(p, '_get_callback'):
|
||||||
try:
|
try:
|
||||||
views.append((p.get_callback(), base + p.regex.pattern))
|
views.append((p._get_callback(), base + p.regex.pattern))
|
||||||
except ViewDoesNotExist:
|
except ViewDoesNotExist:
|
||||||
continue
|
continue
|
||||||
elif hasattr(p, '_get_url_patterns'):
|
elif hasattr(p, '_get_url_patterns'):
|
||||||
views.extend(extract_views_from_urlpatterns(p.url_patterns, base + p.regex.pattern))
|
try:
|
||||||
|
patterns = p.url_patterns
|
||||||
|
except ImportError:
|
||||||
|
continue
|
||||||
|
views.extend(extract_views_from_urlpatterns(patterns, base + p.regex.pattern))
|
||||||
else:
|
else:
|
||||||
raise TypeError, _("%s does not appear to be a urlpattern object") % p
|
raise TypeError, _("%s does not appear to be a urlpattern object") % p
|
||||||
return views
|
return views
|
||||||
|
@ -33,7 +33,7 @@ class Permission(models.Model):
|
|||||||
|
|
||||||
Permissions are set globally per type of object, not per specific object instance. It is possible to say "Mary may change news stories," but it's not currently possible to say "Mary may change news stories, but only the ones she created herself" or "Mary may only change news stories that have a certain status or publication date."
|
Permissions are set globally per type of object, not per specific object instance. It is possible to say "Mary may change news stories," but it's not currently possible to say "Mary may change news stories, but only the ones she created herself" or "Mary may only change news stories that have a certain status or publication date."
|
||||||
|
|
||||||
Three basic permissions -- add, create and delete -- are automatically created for each Django model.
|
Three basic permissions -- add, change and delete -- are automatically created for each Django model.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(_('name'), maxlength=50)
|
name = models.CharField(_('name'), maxlength=50)
|
||||||
content_type = models.ForeignKey(ContentType)
|
content_type = models.ForeignKey(ContentType)
|
||||||
|
90
django/contrib/sitemaps/__init__.py
Normal file
90
django/contrib/sitemaps/__init__.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from django.core import urlresolvers
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
PING_URL = "http://www.google.com/webmasters/sitemaps/ping"
|
||||||
|
|
||||||
|
class SitemapNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ping_google(sitemap_url=None, ping_url=PING_URL):
|
||||||
|
"""
|
||||||
|
Alerts Google that the sitemap for the current site has been updated.
|
||||||
|
If sitemap_url is provided, it should be an absolute path to the sitemap
|
||||||
|
for this site -- e.g., '/sitemap.xml'. If sitemap_url is not provided, this
|
||||||
|
function will attempt to deduce it by using urlresolvers.reverse().
|
||||||
|
"""
|
||||||
|
if sitemap_url is None:
|
||||||
|
try:
|
||||||
|
# First, try to get the "index" sitemap URL.
|
||||||
|
sitemap_url = urlresolvers.reverse('django.contrib.sitemaps.views.index')
|
||||||
|
except urlresolvers.NoReverseMatch:
|
||||||
|
try:
|
||||||
|
# Next, try for the "global" sitemap URL.
|
||||||
|
sitemap_url = urlresolvers.reverse('django.contrib.sitemaps.views.sitemap')
|
||||||
|
except urlresolvers.NoReverseMatch:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if sitemap_url is None:
|
||||||
|
raise SitemapNotFound("You didn't provide a sitemap_url, and the sitemap URL couldn't be auto-detected.")
|
||||||
|
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
current_site = Site.objects.get_current()
|
||||||
|
url = "%s%s" % (current_site.domain, sitemap)
|
||||||
|
params = urllib.urlencode({'sitemap':url})
|
||||||
|
urllib.urlopen("%s?%s" % (ping_url, params))
|
||||||
|
|
||||||
|
class Sitemap:
|
||||||
|
def __get(self, name, obj, default=None):
|
||||||
|
try:
|
||||||
|
attr = getattr(self, name)
|
||||||
|
except AttributeError:
|
||||||
|
return default
|
||||||
|
if callable(attr):
|
||||||
|
return attr(obj)
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def location(self, obj):
|
||||||
|
return obj.get_absolute_url()
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
current_site = Site.objects.get_current()
|
||||||
|
urls = []
|
||||||
|
for item in self.items():
|
||||||
|
loc = "http://%s%s" % (current_site.domain, self.__get('location', item))
|
||||||
|
url_info = {
|
||||||
|
'location': loc,
|
||||||
|
'lastmod': self.__get('lastmod', item, None),
|
||||||
|
'changefreq': self.__get('changefreq', item, None),
|
||||||
|
'priority': self.__get('priority', item, None)
|
||||||
|
}
|
||||||
|
urls.append(url_info)
|
||||||
|
return urls
|
||||||
|
|
||||||
|
class FlatPageSitemap(Sitemap):
|
||||||
|
def items(self):
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
current_site = Site.objects.get_current()
|
||||||
|
return current_site.flatpage_set.all()
|
||||||
|
|
||||||
|
class GenericSitemap(Sitemap):
|
||||||
|
priority = None
|
||||||
|
changefreq = None
|
||||||
|
|
||||||
|
def __init__(self, info_dict, priority=None, changefreq=None):
|
||||||
|
self.queryset = info_dict['queryset']
|
||||||
|
self.date_field = info_dict.get('date_field', None)
|
||||||
|
self.priority = priority
|
||||||
|
self.changefreq = changefreq
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
# Make sure to return a clone; we don't want premature evaluation.
|
||||||
|
return self.queryset.filter()
|
||||||
|
|
||||||
|
def lastmod(self, item):
|
||||||
|
if self.date_field is not None:
|
||||||
|
return getattr(item, self.date_field)
|
||||||
|
return None
|
11
django/contrib/sitemaps/templates/sitemap.xml
Normal file
11
django/contrib/sitemaps/templates/sitemap.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.google.com/schemas/sitemap/0.84">
|
||||||
|
{% for url in urlset %}
|
||||||
|
<url>
|
||||||
|
<loc>{{ url.location|escape }}</loc>
|
||||||
|
{% if url.lastmod %}<lastmod>{{ url.lastmod|date:"Y-m-d" }}</lastmod>{% endif %}
|
||||||
|
{% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %}
|
||||||
|
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %}
|
||||||
|
</url>
|
||||||
|
{% endfor %}
|
||||||
|
</urlset>
|
8
django/contrib/sitemaps/templates/sitemap_index.xml
Normal file
8
django/contrib/sitemaps/templates/sitemap_index.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<sitemapindex xmlns="http://www.google.com/schemas/sitemap/0.84">
|
||||||
|
{% for location in sitemaps %}
|
||||||
|
<sitemap>
|
||||||
|
<loc>{{ location|escape }}</loc>
|
||||||
|
</sitemap>
|
||||||
|
{% endfor %}
|
||||||
|
</sitemapindex>
|
30
django/contrib/sitemaps/views.py
Normal file
30
django/contrib/sitemaps/views.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from django.http import HttpResponse, Http404
|
||||||
|
from django.template import loader
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
|
from django.core import urlresolvers
|
||||||
|
|
||||||
|
def index(request, sitemaps):
|
||||||
|
current_site = Site.objects.get_current()
|
||||||
|
sites = []
|
||||||
|
protocol = request.is_secure() and 'https' or 'http'
|
||||||
|
for section in sitemaps.keys():
|
||||||
|
sitemap_url = urlresolvers.reverse('django.contrib.sitemaps.views.sitemap', kwargs={'section': section})
|
||||||
|
sites.append('%s://%s%s' % (protocol, current_site.domain, sitemap_url))
|
||||||
|
xml = loader.render_to_string('sitemap_index.xml', {'sitemaps': sites})
|
||||||
|
return HttpResponse(xml, mimetype='application/xml')
|
||||||
|
|
||||||
|
def sitemap(request, sitemaps, section=None):
|
||||||
|
maps, urls = [], []
|
||||||
|
if section is not None:
|
||||||
|
if not sitemaps.has_key(section):
|
||||||
|
raise Http404("No sitemap available for section: %r" % section)
|
||||||
|
maps.append(sitemaps[section])
|
||||||
|
else:
|
||||||
|
maps = sitemaps.values()
|
||||||
|
for site in maps:
|
||||||
|
if callable(site):
|
||||||
|
urls.extend(site().get_urls())
|
||||||
|
else:
|
||||||
|
urls.extend(site.get_urls())
|
||||||
|
xml = loader.render_to_string('sitemap.xml', {'urlset': urls})
|
||||||
|
return HttpResponse(xml, mimetype='application/xml')
|
@ -50,7 +50,11 @@ class Serializer(object):
|
|||||||
Convert a field's value to a string.
|
Convert a field's value to a string.
|
||||||
"""
|
"""
|
||||||
if isinstance(field, models.DateTimeField):
|
if isinstance(field, models.DateTimeField):
|
||||||
value = getattr(obj, field.name).strftime("%Y-%m-%d %H:%M:%S")
|
value = getattr(obj, field.name)
|
||||||
|
if value is None:
|
||||||
|
value = ''
|
||||||
|
else:
|
||||||
|
value = value.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
elif isinstance(field, models.FileField):
|
elif isinstance(field, models.FileField):
|
||||||
value = getattr(obj, "get_%s_url" % field.name, lambda: None)()
|
value = getattr(obj, "get_%s_url" % field.name, lambda: None)()
|
||||||
else:
|
else:
|
||||||
|
@ -62,7 +62,10 @@ class DatabaseWrapper(local):
|
|||||||
self.connection.rollback()
|
self.connection.rollback()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self.connection is not None:
|
from django.conf import settings
|
||||||
|
# If database is in memory, closing the connection destroys the database.
|
||||||
|
# To prevent accidental data loss, ignore close requests on an in-memory db.
|
||||||
|
if self.connection is not None and settings.DATABASE_NAME != ":memory:":
|
||||||
self.connection.close()
|
self.connection.close()
|
||||||
self.connection = None
|
self.connection = None
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ def rev_typecast_boolean(obj, d):
|
|||||||
|
|
||||||
def _dict_helper(desc, row):
|
def _dict_helper(desc, row):
|
||||||
"Returns a dictionary for the given cursor.description and result row."
|
"Returns a dictionary for the given cursor.description and result row."
|
||||||
return dict([(desc[col[0]][0], col[1]) for col in enumerate(row)])
|
return dict(zip([col[0] for col in desc], row))
|
||||||
|
|
||||||
def dictfetchone(cursor):
|
def dictfetchone(cursor):
|
||||||
"Returns a row from the cursor as a dict"
|
"Returns a row from the cursor as a dict"
|
||||||
|
@ -137,13 +137,14 @@ class StringOrigin(Origin):
|
|||||||
return self.source
|
return self.source
|
||||||
|
|
||||||
class Template(object):
|
class Template(object):
|
||||||
def __init__(self, template_string, origin=None):
|
def __init__(self, template_string, origin=None, name='<Unknown Template>'):
|
||||||
"Compilation stage"
|
"Compilation stage"
|
||||||
if settings.TEMPLATE_DEBUG and origin == None:
|
if settings.TEMPLATE_DEBUG and origin == None:
|
||||||
origin = StringOrigin(template_string)
|
origin = StringOrigin(template_string)
|
||||||
# Could do some crazy stack-frame stuff to record where this string
|
# Could do some crazy stack-frame stuff to record where this string
|
||||||
# came from...
|
# came from...
|
||||||
self.nodelist = compile_string(template_string, origin)
|
self.nodelist = compile_string(template_string, origin)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for node in self.nodelist:
|
for node in self.nodelist:
|
||||||
@ -434,7 +435,7 @@ class TokenParser(object):
|
|||||||
while i < len(subject) and subject[i] != subject[p]:
|
while i < len(subject) and subject[i] != subject[p]:
|
||||||
i += 1
|
i += 1
|
||||||
if i >= len(subject):
|
if i >= len(subject):
|
||||||
raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject
|
raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % (i, subject)
|
||||||
i += 1
|
i += 1
|
||||||
res = subject[p:i]
|
res = subject[p:i]
|
||||||
while i < len(subject) and subject[i] in (' ', '\t'):
|
while i < len(subject) and subject[i] in (' ', '\t'):
|
||||||
@ -548,9 +549,12 @@ class FilterExpression(object):
|
|||||||
obj = resolve_variable(self.var, context)
|
obj = resolve_variable(self.var, context)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
if ignore_failures:
|
if ignore_failures:
|
||||||
return None
|
obj = None
|
||||||
else:
|
else:
|
||||||
return settings.TEMPLATE_STRING_IF_INVALID
|
if settings.TEMPLATE_STRING_IF_INVALID:
|
||||||
|
return settings.TEMPLATE_STRING_IF_INVALID
|
||||||
|
else:
|
||||||
|
obj = settings.TEMPLATE_STRING_IF_INVALID
|
||||||
for func, args in self.filters:
|
for func, args in self.filters:
|
||||||
arg_vals = []
|
arg_vals = []
|
||||||
for lookup, arg in args:
|
for lookup, arg in args:
|
||||||
@ -614,11 +618,7 @@ def resolve_variable(path, context):
|
|||||||
|
|
||||||
(The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
|
(The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
|
||||||
"""
|
"""
|
||||||
if path == 'False':
|
if path[0].isdigit():
|
||||||
current = False
|
|
||||||
elif path == 'True':
|
|
||||||
current = True
|
|
||||||
elif path[0].isdigit():
|
|
||||||
number_type = '.' in path and float or int
|
number_type = '.' in path and float or int
|
||||||
try:
|
try:
|
||||||
current = number_type(path)
|
current = number_type(path)
|
||||||
|
@ -86,7 +86,7 @@ class ForNode(Node):
|
|||||||
parentloop = {}
|
parentloop = {}
|
||||||
context.push()
|
context.push()
|
||||||
try:
|
try:
|
||||||
values = self.sequence.resolve(context)
|
values = self.sequence.resolve(context, True)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
values = []
|
values = []
|
||||||
if values is None:
|
if values is None:
|
||||||
@ -212,13 +212,13 @@ class RegroupNode(Node):
|
|||||||
self.var_name = var_name
|
self.var_name = var_name
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
obj_list = self.target.resolve(context)
|
obj_list = self.target.resolve(context, True)
|
||||||
if obj_list == '': # target_var wasn't found in context; fail silently
|
if obj_list == None: # target_var wasn't found in context; fail silently
|
||||||
context[self.var_name] = []
|
context[self.var_name] = []
|
||||||
return ''
|
return ''
|
||||||
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
|
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
|
||||||
for obj in obj_list:
|
for obj in obj_list:
|
||||||
grouper = self.expression.resolve(Context({'var': obj}))
|
grouper = self.expression.resolve(Context({'var': obj}), True)
|
||||||
# TODO: Is this a sensible way to determine equality?
|
# TODO: Is this a sensible way to determine equality?
|
||||||
if output and repr(output[-1]['grouper']) == repr(grouper):
|
if output and repr(output[-1]['grouper']) == repr(grouper):
|
||||||
output[-1]['list'].append(obj)
|
output[-1]['list'].append(obj)
|
||||||
@ -251,7 +251,7 @@ class SsiNode(Node):
|
|||||||
output = ''
|
output = ''
|
||||||
if self.parsed:
|
if self.parsed:
|
||||||
try:
|
try:
|
||||||
t = Template(output)
|
t = Template(output, name=self.filepath)
|
||||||
return t.render(context)
|
return t.render(context)
|
||||||
except TemplateSyntaxError, e:
|
except TemplateSyntaxError, e:
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
@ -76,14 +76,16 @@ def get_template(template_name):
|
|||||||
Returns a compiled Template object for the given template name,
|
Returns a compiled Template object for the given template name,
|
||||||
handling template inheritance recursively.
|
handling template inheritance recursively.
|
||||||
"""
|
"""
|
||||||
return get_template_from_string(*find_template_source(template_name))
|
source, origin = find_template_source(template_name)
|
||||||
|
template = get_template_from_string(source, origin, template_name)
|
||||||
|
return template
|
||||||
|
|
||||||
def get_template_from_string(source, origin=None):
|
def get_template_from_string(source, origin=None, name=None):
|
||||||
"""
|
"""
|
||||||
Returns a compiled Template object for the given template code,
|
Returns a compiled Template object for the given template code,
|
||||||
handling template inheritance recursively.
|
handling template inheritance recursively.
|
||||||
"""
|
"""
|
||||||
return Template(source, origin)
|
return Template(source, origin, name)
|
||||||
|
|
||||||
def render_to_string(template_name, dictionary=None, context_instance=None):
|
def render_to_string(template_name, dictionary=None, context_instance=None):
|
||||||
"""
|
"""
|
||||||
|
@ -51,13 +51,13 @@ class ExtendsNode(Node):
|
|||||||
error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr.
|
error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr.
|
||||||
raise TemplateSyntaxError, error_msg
|
raise TemplateSyntaxError, error_msg
|
||||||
if hasattr(parent, 'render'):
|
if hasattr(parent, 'render'):
|
||||||
return parent
|
return parent # parent is a Template object
|
||||||
try:
|
try:
|
||||||
source, origin = find_template_source(parent, self.template_dirs)
|
source, origin = find_template_source(parent, self.template_dirs)
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
|
raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
|
||||||
else:
|
else:
|
||||||
return get_template_from_string(source, origin)
|
return get_template_from_string(source, origin, parent)
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
compiled_parent = self.get_parent(context)
|
compiled_parent = self.get_parent(context)
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
from django.contrib.admin.views.decorators import LOGIN_FORM_KEY, _encode_post_data
|
|
||||||
from django.core.handlers.base import BaseHandler
|
from django.core.handlers.base import BaseHandler
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.dispatch import dispatcher
|
from django.dispatch import dispatcher
|
||||||
from django.http import urlencode, SimpleCookie
|
from django.http import urlencode, SimpleCookie
|
||||||
from django.template import signals
|
from django.test import signals
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
|
|
||||||
class ClientHandler(BaseHandler):
|
class ClientHandler(BaseHandler):
|
||||||
@ -96,7 +95,7 @@ class Client:
|
|||||||
HTML rendered to the end-user.
|
HTML rendered to the end-user.
|
||||||
"""
|
"""
|
||||||
def __init__(self, **defaults):
|
def __init__(self, **defaults):
|
||||||
self.handler = TestHandler()
|
self.handler = ClientHandler()
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
self.cookie = SimpleCookie()
|
self.cookie = SimpleCookie()
|
||||||
|
|
||||||
@ -180,29 +179,38 @@ class Client:
|
|||||||
def login(self, path, username, password, **extra):
|
def login(self, path, username, password, **extra):
|
||||||
"""
|
"""
|
||||||
A specialized sequence of GET and POST to log into a view that
|
A specialized sequence of GET and POST to log into a view that
|
||||||
is protected by @login_required or a similar access decorator.
|
is protected by a @login_required access decorator.
|
||||||
|
|
||||||
path should be the URL of the login page, or of any page that
|
path should be the URL of the page that is login protected.
|
||||||
is login protected.
|
|
||||||
|
|
||||||
Returns True if login was successful; False if otherwise.
|
Returns the response from GETting the requested URL after
|
||||||
|
login is complete. Returns False if login process failed.
|
||||||
"""
|
"""
|
||||||
# First, GET the login page.
|
# First, GET the page that is login protected.
|
||||||
# This is required to establish the session.
|
# This page will redirect to the login page.
|
||||||
response = self.get(path)
|
response = self.get(path)
|
||||||
|
if response.status_code != 302:
|
||||||
|
return False
|
||||||
|
|
||||||
|
login_path, data = response['Location'].split('?')
|
||||||
|
next = data.split('=')[1]
|
||||||
|
|
||||||
|
# Second, GET the login page; required to set up cookies
|
||||||
|
response = self.get(login_path, **extra)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Set up the block of form data required by the login page.
|
# Last, POST the login data.
|
||||||
form_data = {
|
form_data = {
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'this_is_the_login_form': 1,
|
'next' : next,
|
||||||
'post_data': _encode_post_data({LOGIN_FORM_KEY: 1})
|
|
||||||
}
|
}
|
||||||
response = self.post(path, data=form_data, **extra)
|
response = self.post(login_path, data=form_data, **extra)
|
||||||
|
|
||||||
# login page should give response 200 (if you requested the login
|
# Login page should 302 redirect to the originally requested page
|
||||||
# page specifically), or 302 (if you requested a login
|
if response.status_code != 302 or response['Location'] != path:
|
||||||
# protected page, to which the login can redirect).
|
return False
|
||||||
return response.status_code in (200,302)
|
|
||||||
|
# Since we are logged in, request the actual page again
|
||||||
|
return self.get(path)
|
||||||
|
1
django/test/signals.py
Normal file
1
django/test/signals.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
template_rendered = object()
|
@ -1,6 +1,7 @@
|
|||||||
import unittest, doctest
|
import unittest, doctest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import management
|
from django.core import management
|
||||||
|
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||||
from django.test.utils import create_test_db, destroy_test_db
|
from django.test.utils import create_test_db, destroy_test_db
|
||||||
from django.test.testcases import OutputChecker, DocTestRunner
|
from django.test.testcases import OutputChecker, DocTestRunner
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
|
|||||||
the module. A list of 'extra' tests may also be provided; these tests
|
the module. A list of 'extra' tests may also be provided; these tests
|
||||||
will be added to the test suite.
|
will be added to the test suite.
|
||||||
"""
|
"""
|
||||||
|
setup_test_environment()
|
||||||
|
|
||||||
settings.DEBUG = False
|
settings.DEBUG = False
|
||||||
suite = unittest.TestSuite()
|
suite = unittest.TestSuite()
|
||||||
@ -61,7 +63,10 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
|
|||||||
for test in extra_tests:
|
for test in extra_tests:
|
||||||
suite.addTest(test)
|
suite.addTest(test)
|
||||||
|
|
||||||
old_name = create_test_db(verbosity)
|
old_name = settings.DATABASE_NAME
|
||||||
|
create_test_db(verbosity)
|
||||||
management.syncdb(verbosity, interactive=False)
|
management.syncdb(verbosity, interactive=False)
|
||||||
unittest.TextTestRunner(verbosity=verbosity).run(suite)
|
unittest.TextTestRunner(verbosity=verbosity).run(suite)
|
||||||
destroy_test_db(old_name, verbosity)
|
destroy_test_db(old_name, verbosity)
|
||||||
|
|
||||||
|
teardown_test_environment()
|
||||||
|
@ -1,11 +1,40 @@
|
|||||||
import sys, time
|
import sys, time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection, transaction, backend
|
from django.db import connection, transaction, backend
|
||||||
|
from django.dispatch import dispatcher
|
||||||
|
from django.test import signals
|
||||||
|
from django.template import Template
|
||||||
|
|
||||||
# The prefix to put on the default database name when creating
|
# The prefix to put on the default database name when creating
|
||||||
# the test database.
|
# the test database.
|
||||||
TEST_DATABASE_PREFIX = 'test_'
|
TEST_DATABASE_PREFIX = 'test_'
|
||||||
|
|
||||||
|
def instrumented_test_render(self, context):
|
||||||
|
"""An instrumented Template render method, providing a signal
|
||||||
|
that can be intercepted by the test system Client
|
||||||
|
|
||||||
|
"""
|
||||||
|
dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
|
||||||
|
return self.nodelist.render(context)
|
||||||
|
|
||||||
|
def setup_test_environment():
|
||||||
|
"""Perform any global pre-test setup. This involves:
|
||||||
|
|
||||||
|
- Installing the instrumented test renderer
|
||||||
|
|
||||||
|
"""
|
||||||
|
Template.original_render = Template.render
|
||||||
|
Template.render = instrumented_test_render
|
||||||
|
|
||||||
|
def teardown_test_environment():
|
||||||
|
"""Perform any global post-test teardown. This involves:
|
||||||
|
|
||||||
|
- Restoring the original test renderer
|
||||||
|
|
||||||
|
"""
|
||||||
|
Template.render = Template.original_render
|
||||||
|
del Template.original_render
|
||||||
|
|
||||||
def _set_autocommit(connection):
|
def _set_autocommit(connection):
|
||||||
"Make sure a connection is in autocommit mode."
|
"Make sure a connection is in autocommit mode."
|
||||||
if hasattr(connection.connection, "autocommit"):
|
if hasattr(connection.connection, "autocommit"):
|
||||||
@ -21,7 +50,10 @@ def create_test_db(verbosity=1, autoclobber=False):
|
|||||||
if settings.DATABASE_ENGINE == "sqlite3":
|
if settings.DATABASE_ENGINE == "sqlite3":
|
||||||
TEST_DATABASE_NAME = ":memory:"
|
TEST_DATABASE_NAME = ":memory:"
|
||||||
else:
|
else:
|
||||||
TEST_DATABASE_NAME = TEST_DATABASE_PREFIX + settings.DATABASE_NAME
|
if settings.TEST_DATABASE_NAME:
|
||||||
|
TEST_DATABASE_NAME = settings.TEST_DATABASE_NAME
|
||||||
|
else:
|
||||||
|
TEST_DATABASE_NAME = TEST_DATABASE_PREFIX + settings.DATABASE_NAME
|
||||||
|
|
||||||
# Create the test database and connect to it. We need to autocommit
|
# Create the test database and connect to it. We need to autocommit
|
||||||
# if the database supports it because PostgreSQL doesn't allow
|
# if the database supports it because PostgreSQL doesn't allow
|
||||||
@ -50,15 +82,12 @@ def create_test_db(verbosity=1, autoclobber=False):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
connection.close()
|
connection.close()
|
||||||
old_database_name = settings.DATABASE_NAME
|
|
||||||
settings.DATABASE_NAME = TEST_DATABASE_NAME
|
settings.DATABASE_NAME = TEST_DATABASE_NAME
|
||||||
|
|
||||||
# Get a cursor (even though we don't need one yet). This has
|
# Get a cursor (even though we don't need one yet). This has
|
||||||
# the side effect of initializing the test database.
|
# the side effect of initializing the test database.
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
return old_database_name
|
|
||||||
|
|
||||||
def destroy_test_db(old_database_name, verbosity=1):
|
def destroy_test_db(old_database_name, verbosity=1):
|
||||||
# Unless we're using SQLite, remove the test database to clean up after
|
# Unless we're using SQLite, remove the test database to clean up after
|
||||||
# ourselves. Connect to the previous database (not the test database)
|
# ourselves. Connect to the previous database (not the test database)
|
||||||
@ -66,13 +95,13 @@ def destroy_test_db(old_database_name, verbosity=1):
|
|||||||
# connected to it.
|
# connected to it.
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
print "Destroying test database..."
|
print "Destroying test database..."
|
||||||
|
connection.close()
|
||||||
|
TEST_DATABASE_NAME = settings.DATABASE_NAME
|
||||||
|
settings.DATABASE_NAME = old_database_name
|
||||||
|
|
||||||
if settings.DATABASE_ENGINE != "sqlite3":
|
if settings.DATABASE_ENGINE != "sqlite3":
|
||||||
connection.close()
|
|
||||||
TEST_DATABASE_NAME = settings.DATABASE_NAME
|
|
||||||
settings.DATABASE_NAME = old_database_name
|
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
_set_autocommit(connection)
|
_set_autocommit(connection)
|
||||||
time.sleep(1) # To avoid "database is being accessed by other users" errors.
|
time.sleep(1) # To avoid "database is being accessed by other users" errors.
|
||||||
cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
|
cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
|
@ -187,17 +187,23 @@ class MultiValueDict(dict):
|
|||||||
"Returns a copy of this object."
|
"Returns a copy of this object."
|
||||||
return self.__deepcopy__()
|
return self.__deepcopy__()
|
||||||
|
|
||||||
def update(self, other_dict):
|
def update(self, *args, **kwargs):
|
||||||
"update() extends rather than replaces existing key lists."
|
"update() extends rather than replaces existing key lists. Also accepts keyword args."
|
||||||
if isinstance(other_dict, MultiValueDict):
|
if len(args) > 1:
|
||||||
for key, value_list in other_dict.lists():
|
raise TypeError, "update expected at most 1 arguments, got %d", len(args)
|
||||||
self.setlistdefault(key, []).extend(value_list)
|
if args:
|
||||||
else:
|
other_dict = args[0]
|
||||||
try:
|
if isinstance(other_dict, MultiValueDict):
|
||||||
for key, value in other_dict.items():
|
for key, value_list in other_dict.lists():
|
||||||
self.setlistdefault(key, []).append(value)
|
self.setlistdefault(key, []).extend(value_list)
|
||||||
except TypeError:
|
else:
|
||||||
raise ValueError, "MultiValueDict.update() takes either a MultiValueDict or dictionary"
|
try:
|
||||||
|
for key, value in other_dict.items():
|
||||||
|
self.setlistdefault(key, []).append(value)
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError, "MultiValueDict.update() takes either a MultiValueDict or dictionary"
|
||||||
|
for key, value in kwargs.iteritems():
|
||||||
|
self.setlistdefault(key, []).append(value)
|
||||||
|
|
||||||
class DotExpandedDict(dict):
|
class DotExpandedDict(dict):
|
||||||
"""
|
"""
|
||||||
|
@ -115,7 +115,7 @@ def technical_500_response(request, exc_type, exc_value, tb):
|
|||||||
'function': '?',
|
'function': '?',
|
||||||
'lineno': '?',
|
'lineno': '?',
|
||||||
}]
|
}]
|
||||||
t = Template(TECHNICAL_500_TEMPLATE)
|
t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
|
||||||
c = Context({
|
c = Context({
|
||||||
'exception_type': exc_type.__name__,
|
'exception_type': exc_type.__name__,
|
||||||
'exception_value': exc_value,
|
'exception_value': exc_value,
|
||||||
@ -141,7 +141,7 @@ def technical_404_response(request, exception):
|
|||||||
# tried exists but is an empty list. The URLconf must've been empty.
|
# tried exists but is an empty list. The URLconf must've been empty.
|
||||||
return empty_urlconf(request)
|
return empty_urlconf(request)
|
||||||
|
|
||||||
t = Template(TECHNICAL_404_TEMPLATE)
|
t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 template')
|
||||||
c = Context({
|
c = Context({
|
||||||
'root_urlconf': settings.ROOT_URLCONF,
|
'root_urlconf': settings.ROOT_URLCONF,
|
||||||
'urlpatterns': tried,
|
'urlpatterns': tried,
|
||||||
@ -154,7 +154,7 @@ def technical_404_response(request, exception):
|
|||||||
|
|
||||||
def empty_urlconf(request):
|
def empty_urlconf(request):
|
||||||
"Create an empty URLconf 404 error response."
|
"Create an empty URLconf 404 error response."
|
||||||
t = Template(EMPTY_URLCONF_TEMPLATE)
|
t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf template')
|
||||||
c = Context({
|
c = Context({
|
||||||
'project_name': settings.SETTINGS_MODULE.split('.')[0]
|
'project_name': settings.SETTINGS_MODULE.split('.')[0]
|
||||||
})
|
})
|
||||||
@ -189,7 +189,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||||
<title>{{ exception_type }} at {{ request.path }}</title>
|
<title>{{ exception_type }} at {{ request.path|escape }}</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
html * { padding:0; margin:0; }
|
html * { padding:0; margin:0; }
|
||||||
body * { padding:10px 20px; }
|
body * { padding:10px 20px; }
|
||||||
@ -292,7 +292,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="summary">
|
<div id="summary">
|
||||||
<h1>{{ exception_type }} at {{ request.path }}</h1>
|
<h1>{{ exception_type }} at {{ request.path|escape }}</h1>
|
||||||
<h2>{{ exception_value|escape }}</h2>
|
<h2>{{ exception_value|escape }}</h2>
|
||||||
<table class="meta">
|
<table class="meta">
|
||||||
<tr>
|
<tr>
|
||||||
@ -301,7 +301,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Request URL:</th>
|
<th>Request URL:</th>
|
||||||
<td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path }}</td>
|
<td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path|escape }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exception Type:</th>
|
<th>Exception Type:</th>
|
||||||
@ -309,7 +309,7 @@ TECHNICAL_500_TEMPLATE = """
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exception Value:</th>
|
<th>Exception Value:</th>
|
||||||
<td>{{ exception_value }}</td>
|
<td>{{ exception_value|escape }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Exception Location:</th>
|
<th>Exception Location:</th>
|
||||||
@ -412,7 +412,7 @@ Traceback (most recent call last):<br/>
|
|||||||
{{ frame.lineno }}. {{ frame.context_line|escape }}<br/>
|
{{ frame.lineno }}. {{ frame.context_line|escape }}<br/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}<br/>
|
{% endfor %}<br/>
|
||||||
{{ exception_type }} at {{ request.path }}<br/>
|
{{ exception_type }} at {{ request.path|escape }}<br/>
|
||||||
{{ exception_value|escape }}</code>
|
{{ exception_value|escape }}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -546,7 +546,7 @@ TECHNICAL_404_TEMPLATE = """
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
<title>Page not found at {{ request.path }}</title>
|
<title>Page not found at {{ request.path|escape }}</title>
|
||||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
html * { padding:0; margin:0; }
|
html * { padding:0; margin:0; }
|
||||||
@ -576,7 +576,7 @@ TECHNICAL_404_TEMPLATE = """
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Request URL:</th>
|
<th>Request URL:</th>
|
||||||
<td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path }}</td>
|
<td>{{ request_protocol }}://{{ request.META.HTTP_HOST }}{{ request.path|escape }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -591,7 +591,7 @@ TECHNICAL_404_TEMPLATE = """
|
|||||||
<li>{{ pattern|escape }}</li>
|
<li>{{ pattern|escape }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
<p>The current URL, <code>{{ request.path }}</code>, didn't match any of these.</p>
|
<p>The current URL, <code>{{ request.path|escape }}</code>, didn't match any of these.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ reason|escape }}</p>
|
<p>{{ reason|escape }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -81,7 +81,7 @@ def directory_index(path, fullpath):
|
|||||||
try:
|
try:
|
||||||
t = loader.get_template('static/directory_index')
|
t = loader.get_template('static/directory_index')
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE)
|
t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
|
||||||
files = []
|
files = []
|
||||||
for f in os.listdir(fullpath):
|
for f in os.listdir(fullpath):
|
||||||
if not f.startswith('.'):
|
if not f.startswith('.'):
|
||||||
|
@ -153,6 +153,15 @@ See the `sites documentation`_.
|
|||||||
|
|
||||||
.. _sites documentation: http://www.djangoproject.com/documentation/sites/
|
.. _sites documentation: http://www.djangoproject.com/documentation/sites/
|
||||||
|
|
||||||
|
sitemaps
|
||||||
|
========
|
||||||
|
|
||||||
|
A framework for generating Google sitemap XML files.
|
||||||
|
|
||||||
|
See the `sitemaps documentation`_.
|
||||||
|
|
||||||
|
.. _sitemaps documentation: http://www.djangoproject.com/documentation/sitemaps/
|
||||||
|
|
||||||
syndication
|
syndication
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
@ -168,6 +168,19 @@ Please follow these coding standards when writing code for inclusion in Django:
|
|||||||
|
|
||||||
{{foo}}
|
{{foo}}
|
||||||
|
|
||||||
|
* In Django views, the first parameter in a view function should be called
|
||||||
|
``request``.
|
||||||
|
|
||||||
|
Do this::
|
||||||
|
|
||||||
|
def my_view(request, foo):
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Don't do this::
|
||||||
|
|
||||||
|
def my_view(req, foo):
|
||||||
|
# ...
|
||||||
|
|
||||||
* Please don't put your name in the code. While we appreciate all
|
* Please don't put your name in the code. While we appreciate all
|
||||||
contributions to Django, our policy is not to publish individual
|
contributions to Django, our policy is not to publish individual
|
||||||
developer names in code -- for instance, at the top of Python modules.
|
developer names in code -- for instance, at the top of Python modules.
|
||||||
|
@ -345,6 +345,17 @@ setting the Python path for you.
|
|||||||
Displays a help message that includes a terse list of all available actions and
|
Displays a help message that includes a terse list of all available actions and
|
||||||
options.
|
options.
|
||||||
|
|
||||||
|
--noinput
|
||||||
|
---------
|
||||||
|
|
||||||
|
Inform django-admin that the user should NOT be prompted for any input. Useful if
|
||||||
|
the django-admin script will be executed as an unattended, automated script.
|
||||||
|
|
||||||
|
--noreload
|
||||||
|
----------
|
||||||
|
|
||||||
|
Disable the use of the auto-reloader when running the development server.
|
||||||
|
|
||||||
--version
|
--version
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@ -355,6 +366,17 @@ Example output::
|
|||||||
0.9.1
|
0.9.1
|
||||||
0.9.1 (SVN)
|
0.9.1 (SVN)
|
||||||
|
|
||||||
|
--verbosity
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
django-admin.py syncdb --verbosity=2
|
||||||
|
|
||||||
|
Verbosity determines the amount of notification and debug information that
|
||||||
|
will be printed to the console. '0' is no output, '1' is normal output,
|
||||||
|
and `2` is verbose output.
|
||||||
|
|
||||||
Extra niceties
|
Extra niceties
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
@ -641,7 +641,7 @@ How can I get started contributing code to Django?
|
|||||||
Thanks for asking! We've written an entire document devoted to this question.
|
Thanks for asking! We've written an entire document devoted to this question.
|
||||||
It's titled `Contributing to Django`_.
|
It's titled `Contributing to Django`_.
|
||||||
|
|
||||||
.. _Contributing do Django: http://www.djangoproject.com/documentation/contributing/
|
.. _Contributing to Django: http://www.djangoproject.com/documentation/contributing/
|
||||||
|
|
||||||
I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch?
|
I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch?
|
||||||
--------------------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------
|
||||||
|
@ -754,6 +754,30 @@ misspelled) variables. See `How invalid variables are handled`_.
|
|||||||
|
|
||||||
.. _How invalid variables are handled: http://www.djangoproject.com/documentation/templates_python/#how-invalid-variables-are-handled
|
.. _How invalid variables are handled: http://www.djangoproject.com/documentation/templates_python/#how-invalid-variables-are-handled
|
||||||
|
|
||||||
|
TEST_RUNNER
|
||||||
|
-----------
|
||||||
|
|
||||||
|
**New in Django development version**
|
||||||
|
|
||||||
|
Default: ``'django.test.simple.run_tests'``
|
||||||
|
|
||||||
|
The name of the method to use for starting the test suite. See
|
||||||
|
`Testing Django Applications`_.
|
||||||
|
|
||||||
|
.. _Testing Django Applications: ../testing/
|
||||||
|
|
||||||
|
TEST_DATABASE_NAME
|
||||||
|
------------------
|
||||||
|
|
||||||
|
**New in Django development version**
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
The name of database to use when running the test suite. If a value of
|
||||||
|
``None`` is specified, the test database will use the name ``'test_' + settings.DATABASE_NAME``. See `Testing Django Applications`_.
|
||||||
|
|
||||||
|
.. _Testing Django Applications: ../testing/
|
||||||
|
|
||||||
TIME_FORMAT
|
TIME_FORMAT
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
320
docs/sitemaps.txt
Normal file
320
docs/sitemaps.txt
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
=====================
|
||||||
|
The sitemap framework
|
||||||
|
=====================
|
||||||
|
|
||||||
|
**New in Django development version**.
|
||||||
|
|
||||||
|
Django comes with a high-level sitemap-generating framework that makes
|
||||||
|
creating `Google Sitemap`_ XML files easy.
|
||||||
|
|
||||||
|
.. _Google Sitemap: http://www.google.com/webmasters/sitemaps/docs/en/protocol.html
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
A sitemap is an XML file on your Web site that tells search-engine indexers how
|
||||||
|
frequently your pages change and how "important" certain pages are in relation
|
||||||
|
to other pages on your site. This information helps search engines index your
|
||||||
|
site.
|
||||||
|
|
||||||
|
The Django sitemap framework automates the creation of this XML file by letting
|
||||||
|
you express this information in Python code.
|
||||||
|
|
||||||
|
It works much like Django's `syndication framework`_. To create a sitemap, just
|
||||||
|
write a ``Sitemap`` class and point to it in your URLconf_.
|
||||||
|
|
||||||
|
.. _syndication framework: http://www.djangoproject.com/documentation/syndication/
|
||||||
|
.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
To install the sitemap app, follow these steps:
|
||||||
|
|
||||||
|
1. Add ``'django.contrib.sitemaps'`` to your INSTALLED_APPS_ setting.
|
||||||
|
2. Make sure ``'django.template.loaders.app_directories.load_template_source'``
|
||||||
|
is in your TEMPLATE_LOADERS_ setting. It's in there by default, so
|
||||||
|
you'll only need to change this if you've changed that setting.
|
||||||
|
3. Make sure you've installed the `sites framework`_.
|
||||||
|
|
||||||
|
(Note: The sitemap application doesn't install any database tables. The only
|
||||||
|
reason it needs to go into ``INSTALLED_APPS`` is so that the
|
||||||
|
``load_template_source`` template loader can find the default templates.)
|
||||||
|
|
||||||
|
.. _INSTALLED_APPS: http://www.djangoproject.com/documentation/settings/#installed-apps
|
||||||
|
.. _TEMPLATE_LOADERS: http://www.djangoproject.com/documentation/settings/#template-loaders
|
||||||
|
.. _sites framework: http://www.djangoproject.com/documentation/sites/
|
||||||
|
|
||||||
|
Initialization
|
||||||
|
==============
|
||||||
|
|
||||||
|
To activate sitemap generation on your Django site, add this line to your
|
||||||
|
URLconf_:
|
||||||
|
|
||||||
|
(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps})
|
||||||
|
|
||||||
|
This tells Django to build a sitemap when a client accesses ``/sitemap.xml``.
|
||||||
|
|
||||||
|
The name of the sitemap file is not important, but the location is. Google will
|
||||||
|
only index links in your sitemap for the current URL level and below. For
|
||||||
|
instance, if ``sitemap.xml`` lives in your root directory, it may reference any
|
||||||
|
URL in your site. However, if your sitemap lives at ``/content/sitemap.xml``,
|
||||||
|
it may only reference URLs that begin with ``/content/``.
|
||||||
|
|
||||||
|
The sitemap view takes an extra, required argument: ``{'sitemaps': sitemaps}``.
|
||||||
|
``sitemaps`` should be a dictionary that maps a short section label (e.g.,
|
||||||
|
``blog`` or ``news``) to its ``Sitemap`` class (e.g., ``BlogSitemap`` or
|
||||||
|
``NewsSitemap``). It may also map to an *instance* of a ``Sitemap`` class
|
||||||
|
(e.g., ``BlogSitemap(some_var)``).
|
||||||
|
|
||||||
|
.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
|
||||||
|
|
||||||
|
Sitemap classes
|
||||||
|
===============
|
||||||
|
|
||||||
|
A ``Sitemap`` class is a simple Python class that represents a "section" of
|
||||||
|
entries in your sitemap. For example, one ``Sitemap`` class could represent all
|
||||||
|
the entries of your weblog, while another could represent all of the events in
|
||||||
|
your events calendar.
|
||||||
|
|
||||||
|
In the simplest case, all these sections get lumped together into one
|
||||||
|
``sitemap.xml``, but it's also possible to use the framework to generate a
|
||||||
|
sitemap index that references individual sitemap files, one per section. (See
|
||||||
|
`Creating a sitemap index`_ below.)
|
||||||
|
|
||||||
|
``Sitemap`` classes must subclass ``django.contrib.sitemaps.Sitemap``. They can
|
||||||
|
live anywhere in your codebase.
|
||||||
|
|
||||||
|
A simple example
|
||||||
|
================
|
||||||
|
|
||||||
|
Let's assume you have a blog system, with an ``Entry`` model, and you want your
|
||||||
|
sitemap to include all the links to your individual blog entries. Here's how
|
||||||
|
your sitemap class might look::
|
||||||
|
|
||||||
|
from django.contrib.sitemaps import Sitemap
|
||||||
|
from mysite.blog.models import Entry
|
||||||
|
|
||||||
|
class BlogSitemap(Sitemap):
|
||||||
|
changefreq = "never"
|
||||||
|
priority = 0.5
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return Entry.objects.filter(is_draft=False)
|
||||||
|
|
||||||
|
def lastmod(self, obj):
|
||||||
|
return obj.pub_date
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
* ``changefreq`` and ``priority`` are class attributes corresponding to
|
||||||
|
``<changefreq>`` and ``<priority>`` elements, respectively. They can be
|
||||||
|
made callable as functions, as ``lastmod`` was in the example.
|
||||||
|
* ``items()`` is simply a method that returns a list of objects. The objects
|
||||||
|
returned will get passed to any callable methods corresponding to a
|
||||||
|
sitemap property (``location``, ``lastmod``, ``changefreq``, and
|
||||||
|
``priority``).
|
||||||
|
* ``lastmod`` should return a Python ``datetime`` object.
|
||||||
|
* There is no ``location`` method in this example, but you can provide it
|
||||||
|
in order to specify the URL for your object. By default, ``location()``
|
||||||
|
calls ``get_absolute_url()`` on each object and returns the result.
|
||||||
|
|
||||||
|
Sitemap class reference
|
||||||
|
=======================
|
||||||
|
|
||||||
|
A ``Sitemap`` class can define the following methods/attributes:
|
||||||
|
|
||||||
|
``items``
|
||||||
|
---------
|
||||||
|
|
||||||
|
**Required.** A method that returns a list of objects. The framework doesn't
|
||||||
|
care what *type* of objects they are; all that matters is that these objects
|
||||||
|
get passed to the ``location()``, ``lastmod()``, ``changefreq()`` and
|
||||||
|
``priority()`` methods.
|
||||||
|
|
||||||
|
``location``
|
||||||
|
------------
|
||||||
|
|
||||||
|
**Optional.** Either a method or attribute.
|
||||||
|
|
||||||
|
If it's a method, it should return the absolute URL for a given object as
|
||||||
|
returned by ``items()``.
|
||||||
|
|
||||||
|
If it's an attribute, its value should be a string representing an absolute URL
|
||||||
|
to use for *every* object returned by ``items()``.
|
||||||
|
|
||||||
|
In both cases, "absolute URL" means a URL that doesn't include the protocol or
|
||||||
|
domain. Examples:
|
||||||
|
|
||||||
|
* Good: ``'/foo/bar/'``
|
||||||
|
* Bad: ``'example.com/foo/bar/'``
|
||||||
|
* Bad: ``'http://example.com/foo/bar/'``
|
||||||
|
|
||||||
|
If ``location`` isn't provided, the framework will call the
|
||||||
|
``get_absolute_url()`` method on each object as returned by ``items()``.
|
||||||
|
|
||||||
|
``lastmod``
|
||||||
|
-----------
|
||||||
|
|
||||||
|
**Optional.** Either a method or attribute.
|
||||||
|
|
||||||
|
If it's a method, it should take one argument -- an object as returned by
|
||||||
|
``items()`` -- and return that object's last-modified date/time, as a Python
|
||||||
|
``datetime.datetime`` object.
|
||||||
|
|
||||||
|
If it's an attribute, its value should be a Python ``datetime.datetime`` object
|
||||||
|
representing the last-modified date/time for *every* object returned by
|
||||||
|
``items()``.
|
||||||
|
|
||||||
|
``changefreq``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
**Optional.** Either a method or attribute.
|
||||||
|
|
||||||
|
If it's a method, it should take one argument -- an object as returned by
|
||||||
|
``items()`` -- and return that object's change frequency, as a Python string.
|
||||||
|
|
||||||
|
If it's an attribute, its value should be a string representing the change
|
||||||
|
frequency of *every* object returned by ``items()``.
|
||||||
|
|
||||||
|
Possible values for ``changefreq``, whether you use a method or attribute, are:
|
||||||
|
|
||||||
|
* ``'always'``
|
||||||
|
* ``'hourly'``
|
||||||
|
* ``'daily'``
|
||||||
|
* ``'weekly'``
|
||||||
|
* ``'monthly'``
|
||||||
|
* ``'yearly'``
|
||||||
|
* ``'never'``
|
||||||
|
|
||||||
|
``priority``
|
||||||
|
------------
|
||||||
|
|
||||||
|
**Optional.** Either a method or attribute.
|
||||||
|
|
||||||
|
If it's a method, it should take one argument -- an object as returned by
|
||||||
|
``items()`` -- and return that object's priority, as either a string or float.
|
||||||
|
|
||||||
|
If it's an attribute, its value should be either a string or float representing
|
||||||
|
the priority of *every* object returned by ``items()``.
|
||||||
|
|
||||||
|
Example values for ``priority``: ``0.4``, ``1.0``. The default priority of a
|
||||||
|
page is ``0.5``. See Google's documentation for more documentation.
|
||||||
|
|
||||||
|
.. _Google's documentation: http://www.google.com/webmasters/sitemaps/docs/en/protocol.html
|
||||||
|
|
||||||
|
Shortcuts
|
||||||
|
=========
|
||||||
|
|
||||||
|
The sitemap framework provides a couple convenience classes for common cases:
|
||||||
|
|
||||||
|
``FlatPageSitemap``
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The ``django.contrib.sitemaps.FlatPageSitemap`` class looks at all flatpages_
|
||||||
|
defined for the current ``SITE_ID`` (see the `sites documentation`_) and
|
||||||
|
creates an entry in the sitemap. These entries include only the ``location``
|
||||||
|
attribute -- not ``lastmod``, ``changefreq`` or ``priority``.
|
||||||
|
|
||||||
|
.. _flatpages: http://www.djangoproject.com/documentation/flatpages/
|
||||||
|
.. _sites documentation: http://www.djangoproject.com/documentation/sites/
|
||||||
|
|
||||||
|
``GenericSitemap``
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The ``GenericSitemap`` class works with any `generic views`_ you already have.
|
||||||
|
To use it, create an instance, passing in the same ``info_dict`` you pass to
|
||||||
|
the generic views. The only requirement is that the dictionary have a
|
||||||
|
``queryset`` entry. It may also have a ``date_field`` entry that specifies a
|
||||||
|
date field for objects retrieved from the ``queryset``. This will be used for
|
||||||
|
the ``lastmod`` attribute in the generated sitemap. You may also pass
|
||||||
|
``priority`` and ``changefreq`` keyword arguments to the ``GenericSitemap``
|
||||||
|
constructor to specify these attributes for all URLs.
|
||||||
|
|
||||||
|
.. _generic views: http://www.djangoproject.com/documentation/generic_views/
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
Here's an example of a URLconf_ using both::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from django.contrib.sitemaps import FlatPageSitemap, GenericSitemap
|
||||||
|
from mysite.blog.models import Entry
|
||||||
|
|
||||||
|
info_dict = {
|
||||||
|
'queryset': Entry.objects.all(),
|
||||||
|
'date_field': 'pub_date',
|
||||||
|
}
|
||||||
|
|
||||||
|
sitemaps = {
|
||||||
|
'flatpages': FlatPageSitemap,
|
||||||
|
'blog': GenericSitemap(info_dict, priority=0.6),
|
||||||
|
}
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
# some generic view using info_dict
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# the sitemap
|
||||||
|
(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps})
|
||||||
|
)
|
||||||
|
|
||||||
|
.. _URLconf: http://www.djangoproject.com/documentation/url_dispatch/
|
||||||
|
|
||||||
|
Creating a sitemap index
|
||||||
|
========================
|
||||||
|
|
||||||
|
The sitemap framework also has the ability to create a sitemap index that
|
||||||
|
references individual sitemap files, one per each section defined in your
|
||||||
|
``sitemaps`` dictionary. The only differences in usage are:
|
||||||
|
|
||||||
|
* You use two views in your URLconf: ``django.contrib.sitemaps.views.index``
|
||||||
|
and ``django.contrib.sitemaps.views.sitemap``.
|
||||||
|
* The ``django.contrib.sitemaps.views.sitemap`` view should take a
|
||||||
|
``section`` keyword argument.
|
||||||
|
|
||||||
|
Here is what the relevant URLconf lines would look like for the example above::
|
||||||
|
|
||||||
|
(r'^sitemap.xml$', 'django.contrib.sitemaps.views.index', {'sitemaps': sitemaps})
|
||||||
|
(r'^sitemap-(?P<section>.+).xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps})
|
||||||
|
|
||||||
|
This will automatically generate a ``sitemap.xml`` file that references
|
||||||
|
both ``sitemap-flatpages.xml`` and ``sitemap-blog.xml``. The ``Sitemap``
|
||||||
|
classes and the ``sitemaps`` dict don't change at all.
|
||||||
|
|
||||||
|
Pinging Google
|
||||||
|
==============
|
||||||
|
|
||||||
|
You may want to "ping" Google when your sitemap changes, to let it know to
|
||||||
|
reindex your site. The framework provides a function to do just that:
|
||||||
|
``django.contrib.sitemaps.ping_google()``.
|
||||||
|
|
||||||
|
``ping_google()`` takes an optional argument, ``sitemap_url``, which should be
|
||||||
|
the absolute URL of your site's sitemap (e.g., ``'/sitemap.xml'``). If this
|
||||||
|
argument isn't provided, ``ping_google()`` will attempt to figure out your
|
||||||
|
sitemap by performing a reverse looking in your URLconf.
|
||||||
|
|
||||||
|
``ping_google()`` raises the exception
|
||||||
|
``django.contrib.sitemaps.SitemapNotFound`` if it cannot determine your sitemap
|
||||||
|
URL.
|
||||||
|
|
||||||
|
One useful way to call ``ping_google()`` is from a model's ``save()`` method::
|
||||||
|
|
||||||
|
from django.contrib.sitemaps import ping_google
|
||||||
|
|
||||||
|
class Entry(models.Model):
|
||||||
|
# ...
|
||||||
|
def save(self):
|
||||||
|
super(Entry, self).save()
|
||||||
|
try:
|
||||||
|
ping_google()
|
||||||
|
except Exception:
|
||||||
|
# Bare 'except' because we could get a variety
|
||||||
|
# of HTTP-related exceptions.
|
||||||
|
pass
|
||||||
|
|
||||||
|
A more efficient solution, however, would be to call ``ping_google()`` from a
|
||||||
|
cron script, or some other scheduled task. The function makes an HTTP request
|
||||||
|
to Google's servers, so you may not want to introduce that network overhead
|
||||||
|
each time you call ``save()``.
|
@ -266,7 +266,18 @@ this::
|
|||||||
If you attempt to use ``CurrentSiteManager`` and pass a field name that doesn't
|
If you attempt to use ``CurrentSiteManager`` and pass a field name that doesn't
|
||||||
exist, Django will raise a ``ValueError``.
|
exist, Django will raise a ``ValueError``.
|
||||||
|
|
||||||
|
Finally, note that you'll probably want to keep a normal (non-site-specific)
|
||||||
|
``Manager`` on your model, even if you use ``CurrentSiteManager``. As explained
|
||||||
|
in the `manager documentation`_, if you define a manager manually, then Django
|
||||||
|
won't create the automatic ``objects = models.Manager()`` manager for you.
|
||||||
|
Also, note that certain parts of Django -- namely, the Django admin site and
|
||||||
|
generic views -- use whichever manager is defined *first* in the model, so if
|
||||||
|
you want your admin site to have access to all objects (not just site-specific
|
||||||
|
ones), put ``objects = models.Manager()`` in your model, before you define
|
||||||
|
``CurrentSiteManager``.
|
||||||
|
|
||||||
.. _manager: http://www.djangoproject.com/documentation/model_api/#managers
|
.. _manager: http://www.djangoproject.com/documentation/model_api/#managers
|
||||||
|
.. _manager documentation: http://www.djangoproject.com/documentation/model_api/#managers
|
||||||
|
|
||||||
How Django uses the sites framework
|
How Django uses the sites framework
|
||||||
===================================
|
===================================
|
||||||
|
@ -707,7 +707,7 @@ This example creates an Atom 1.0 feed and prints it to standard output::
|
|||||||
... title=u"My Weblog",
|
... title=u"My Weblog",
|
||||||
... link=u"http://www.example.com/",
|
... link=u"http://www.example.com/",
|
||||||
... description=u"In which I write about what I ate today.",
|
... description=u"In which I write about what I ate today.",
|
||||||
... language=u"en"),
|
... language=u"en")
|
||||||
>>> f.add_item(title=u"Hot dog today",
|
>>> f.add_item(title=u"Hot dog today",
|
||||||
... link=u"http://www.example.com/entries/1/",
|
... link=u"http://www.example.com/entries/1/",
|
||||||
... description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
|
... description=u"<p>Today I had a Vienna Beef hot dog. It was pink, plump and perfect.</p>")
|
||||||
|
@ -198,9 +198,19 @@ some things to keep in mind:
|
|||||||
How invalid variables are handled
|
How invalid variables are handled
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If a variable doesn't exist, the template system inserts the value of the
|
Generally, if a variable doesn't exist, the template system inserts the
|
||||||
``TEMPLATE_STRING_IF_INVALID`` setting, which is set to ``''`` (the empty
|
value of the ``TEMPLATE_STRING_IF_INVALID`` setting, which is set to ``''``
|
||||||
string) by default.
|
(the empty string) by default.
|
||||||
|
|
||||||
|
Filters that are applied to an invalid variable will only be applied if
|
||||||
|
``TEMPLATE_STRING_IF_INVALID`` is set to ``''`` (the empty string). If
|
||||||
|
``TEMPLATE_STRING_IF_INVALID`` is set to any other value, variable
|
||||||
|
filters will be ignored.
|
||||||
|
|
||||||
|
This behavior is slightly different for the ``if``, ``for`` and ``regroup``
|
||||||
|
template tags. If an invalid variable is provided to one of these template
|
||||||
|
tags, the variable will be interpreted as ``None``. Filters are always
|
||||||
|
applied to invalid variables within these template tags.
|
||||||
|
|
||||||
Playing with Context objects
|
Playing with Context objects
|
||||||
----------------------------
|
----------------------------
|
||||||
@ -301,11 +311,13 @@ If ``TEMPLATE_CONTEXT_PROCESSORS`` contains this processor, every
|
|||||||
logged-in user (or an ``AnonymousUser`` instance, if the client isn't
|
logged-in user (or an ``AnonymousUser`` instance, if the client isn't
|
||||||
logged in). See the `user authentication docs`.
|
logged in). See the `user authentication docs`.
|
||||||
|
|
||||||
* ``messages`` -- Returns a list of messages for the currently logged-in
|
* ``messages`` -- A list of messages (as strings) for the currently
|
||||||
user (set with ``user.add_message()``). Behind the scenes, this calls
|
logged-in user. Behind the scenes, this calls
|
||||||
``request.user.get_and_delete_messages()``, which iterates over user's
|
``request.user.get_and_delete_messages()`` for every request. That method
|
||||||
messages and returns the actual message before deleting the ``Message``
|
collects the user's messages and deletes them from the database.
|
||||||
object.
|
|
||||||
|
Note that messages are set with ``user.add_message()``. See the
|
||||||
|
`message docs`_ for more.
|
||||||
|
|
||||||
* ``perms`` -- An instance of
|
* ``perms`` -- An instance of
|
||||||
``django.core.context_processors.PermWrapper``, representing the
|
``django.core.context_processors.PermWrapper``, representing the
|
||||||
@ -313,6 +325,7 @@ If ``TEMPLATE_CONTEXT_PROCESSORS`` contains this processor, every
|
|||||||
docs`_.
|
docs`_.
|
||||||
|
|
||||||
.. _user authentication docs: http://www.djangoproject.com/documentation/authentication/#users
|
.. _user authentication docs: http://www.djangoproject.com/documentation/authentication/#users
|
||||||
|
.. _message docs: http://www.djangoproject.com/documentation/authentication/#messages
|
||||||
.. _permissions docs: http://www.djangoproject.com/documentation/authentication/#permissions
|
.. _permissions docs: http://www.djangoproject.com/documentation/authentication/#permissions
|
||||||
|
|
||||||
django.core.context_processors.debug
|
django.core.context_processors.debug
|
||||||
|
275
docs/testing.txt
275
docs/testing.txt
@ -4,9 +4,17 @@ Testing Django applications
|
|||||||
|
|
||||||
**New in Django development version**.
|
**New in Django development version**.
|
||||||
|
|
||||||
.. XXX insert quick introduction to testing (and why you'd want to do it)
|
Automated testing is an extremely useful weapon in the bug-killing arsenal
|
||||||
|
of the modern developer. When initially writing code, a test suite can be
|
||||||
|
used to validate that code behaves as expected. When refactoring or
|
||||||
|
modifying code, tests serve as a guide to ensure that behavior hasn't
|
||||||
|
changed unexpectedly as a result of the refactor.
|
||||||
|
|
||||||
.. note::
|
Testing an web application is a complex task, as there are many
|
||||||
|
components of a web application that must be validated and tested. To
|
||||||
|
help you test your application, Django provides a test execution
|
||||||
|
framework, and range of utilities that can be used to stimulate and
|
||||||
|
inspect various facets of a web application.
|
||||||
|
|
||||||
This testing framework is currently under development, and may change
|
This testing framework is currently under development, and may change
|
||||||
slightly before the next official Django release.
|
slightly before the next official Django release.
|
||||||
@ -23,7 +31,10 @@ Writing doctests
|
|||||||
|
|
||||||
Doctests use Python's standard doctest_ module, which searches for tests in
|
Doctests use Python's standard doctest_ module, which searches for tests in
|
||||||
your docstrings. Django's test runner looks for doctests in your ``models.py``
|
your docstrings. Django's test runner looks for doctests in your ``models.py``
|
||||||
file, and executes any that it finds.
|
file, and executes any that it finds. Django will also search for a file
|
||||||
|
called ``tests.py`` in the application directory (i.e., the directory that
|
||||||
|
holds ``models.py``). If a ``tests.py`` is found, it will also be searched
|
||||||
|
for doctests.
|
||||||
|
|
||||||
.. admonition:: What's a **docstring**?
|
.. admonition:: What's a **docstring**?
|
||||||
|
|
||||||
@ -80,8 +91,8 @@ Writing unittests
|
|||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Like doctests, Django's unit tests use a standard library module: unittest_.
|
Like doctests, Django's unit tests use a standard library module: unittest_.
|
||||||
Django's test runner looks for unit test cases in a ``tests.py`` file in your
|
As with doctests, Django's test runner looks for any unit test cases defined
|
||||||
app (i.e. in the same directory as your ``models.py`` file).
|
in ``models.py``, or in a ``tests.py`` file in your application directory.
|
||||||
|
|
||||||
An equivalent unittest test case for the above example would look like::
|
An equivalent unittest test case for the above example would look like::
|
||||||
|
|
||||||
@ -148,6 +159,164 @@ Again, remember that you can use both systems side-by-side (even in the same
|
|||||||
app). In the end, most projects will eventually end up using both; each shines
|
app). In the end, most projects will eventually end up using both; each shines
|
||||||
in different circumstances.
|
in different circumstances.
|
||||||
|
|
||||||
|
Testing Tools
|
||||||
|
=============
|
||||||
|
|
||||||
|
To assist in testing various features of your application, Django provides
|
||||||
|
tools that can be used to establish tests and test conditions.
|
||||||
|
|
||||||
|
* `Test Client`_
|
||||||
|
* Fixtures_
|
||||||
|
|
||||||
|
Test Client
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The Test Client is a simple dummy browser. It allows you to simulate
|
||||||
|
GET and POST requests on a URL, and observe the response that is received.
|
||||||
|
This allows you to test that the correct view is executed for a given URL,
|
||||||
|
and that the view constructs the correct response.
|
||||||
|
|
||||||
|
As the response is generated, the Test Client gathers details on the
|
||||||
|
Template and Context objects that were used to generate the response. These
|
||||||
|
Templates and Contexts are then provided as part of the response, and can be
|
||||||
|
used as test conditions.
|
||||||
|
|
||||||
|
.. admonition:: Test Client vs Browser Automation?
|
||||||
|
|
||||||
|
The Test Client is not intended as a replacement for Twill_, Selenium_,
|
||||||
|
or other browser automation frameworks - it is intended to allow
|
||||||
|
testing of the contexts and templates produced by a view,
|
||||||
|
rather than the HTML rendered to the end-user.
|
||||||
|
|
||||||
|
A comprehensive test suite should use a combination of both: Test Client
|
||||||
|
tests to establish that the correct view is being called and that
|
||||||
|
the view is collecting the correct context data, and Browser Automation
|
||||||
|
tests to check that user interface behaves as expected.
|
||||||
|
|
||||||
|
.. _Twill: http://twill.idyll.org/
|
||||||
|
.. _Selenium: http://www.openqa.org/selenium/
|
||||||
|
|
||||||
|
The Test Client is stateful; if a cookie is returned as part of a response,
|
||||||
|
that cookie is provided as part of the next request. Expiry policies for these
|
||||||
|
cookies are not followed; if you want a cookie to expire, either delete it
|
||||||
|
manually from ``client.cookies``, or create a new Client instance (which will
|
||||||
|
effectively delete all cookies).
|
||||||
|
|
||||||
|
Making requests
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Creating an instance of ``Client`` (``django.test.client.Client``) requires
|
||||||
|
no arguments at time of construction. Once constructed, the following methods
|
||||||
|
can be invoked on the ``Client`` instance.
|
||||||
|
|
||||||
|
``get(path, data={})``
|
||||||
|
|
||||||
|
Make a GET request on the provided ``path``. The key-value pairs in the
|
||||||
|
data dictionary will be used to create a GET data payload. For example::
|
||||||
|
|
||||||
|
c = Client()
|
||||||
|
c.get('/customers/details/', {'name':'fred', 'age':7})
|
||||||
|
|
||||||
|
will result in the evaluation of a GET request equivalent to::
|
||||||
|
|
||||||
|
http://yoursite.com/customers/details/?name='fred'&age=7
|
||||||
|
|
||||||
|
``post(path, data={})``
|
||||||
|
|
||||||
|
Make a POST request on the provided ``path``. The key-value pairs in the
|
||||||
|
data dictionary will be used to create the POST data payload. This payload
|
||||||
|
will be transmitted with the mimetype ``multipart/form-data``.
|
||||||
|
|
||||||
|
However submitting files is a special case. To POST a file, you need only
|
||||||
|
provide the file field name as a key, and a file handle to the file you wish to
|
||||||
|
upload as a value. The Test Client will populate the two POST fields (i.e.,
|
||||||
|
``field`` and ``field_file``) required by FileField. For example::
|
||||||
|
|
||||||
|
c = Client()
|
||||||
|
f = open('wishlist.doc')
|
||||||
|
c.post('/customers/wishes/', {'name':'fred', 'attachment':f})
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
will result in the evaluation of a POST request on ``/customers/wishes/``,
|
||||||
|
with a POST dictionary that contains `name`, `attachment` (containing the
|
||||||
|
file name), and `attachment_file` (containing the file data). Note that you
|
||||||
|
need to manually close the file after it has been provided to the POST.
|
||||||
|
|
||||||
|
``login(path, username, password)``
|
||||||
|
|
||||||
|
In a production site, it is likely that some views will be protected with
|
||||||
|
the @login_required URL provided by ``django.contrib.auth``. Interacting
|
||||||
|
with a URL that has been login protected is a slightly complex operation,
|
||||||
|
so the Test Client provides a simple URL to automate the login process. A
|
||||||
|
call to ``login()`` stimulates the series of GET and POST calls required
|
||||||
|
to log a user into a @login_required protected URL.
|
||||||
|
|
||||||
|
If login is possible, the final return value of ``login()`` is the response
|
||||||
|
that is generated by issuing a GET request on the protected URL. If login
|
||||||
|
is not possible, ``login()`` returns False.
|
||||||
|
|
||||||
|
Note that since the test suite will be executed using the test database,
|
||||||
|
which contains no users by default. As a result, logins for your production
|
||||||
|
site will not work. You will need to create users as part of the test suite
|
||||||
|
to be able to test logins to your application.
|
||||||
|
|
||||||
|
Testing Responses
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``get()``, ``post()`` and ``login()`` methods all return a Response
|
||||||
|
object. This Response object has the following properties that can be used
|
||||||
|
for testing purposes:
|
||||||
|
|
||||||
|
=============== ==========================================================
|
||||||
|
Property Description
|
||||||
|
=============== ==========================================================
|
||||||
|
``status_code`` The HTTP status of the response. See RFC2616_ for a
|
||||||
|
full list of HTTP status codes.
|
||||||
|
|
||||||
|
``content`` The body of the response. The is the final page
|
||||||
|
content as rendered by the view, or any error message
|
||||||
|
(such as the URL for a 302 redirect).
|
||||||
|
|
||||||
|
``template`` The Template instance that was used to render the final
|
||||||
|
content. Testing ``template.name`` can be particularly
|
||||||
|
useful; if the template was loaded from a file,
|
||||||
|
``template.name`` will be the file name that was loaded.
|
||||||
|
|
||||||
|
If multiple templates were rendered, (e.g., if one
|
||||||
|
template includes another template),``template`` will
|
||||||
|
be a list of Template objects, in the order in which
|
||||||
|
they were rendered.
|
||||||
|
|
||||||
|
``context`` The Context that was used to render the template that
|
||||||
|
produced the response content.
|
||||||
|
|
||||||
|
As with ``template``, if multiple templates were rendered
|
||||||
|
``context`` will be a list of Context objects, stored in
|
||||||
|
the order in which they were rendered.
|
||||||
|
=============== ==========================================================
|
||||||
|
|
||||||
|
.. _RFC2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
|
||||||
|
|
||||||
|
The following is a simple unit test using the Test Client::
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from django.test.client import Client
|
||||||
|
|
||||||
|
class SimpleTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
# Every test needs a client
|
||||||
|
self.client = Client()
|
||||||
|
def test_details(self):
|
||||||
|
response = self.client.get('/customer/details/')
|
||||||
|
|
||||||
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
|
self.failUnlessEqual(len(response.context['customers']), 5)
|
||||||
|
|
||||||
|
Fixtures
|
||||||
|
--------
|
||||||
|
|
||||||
|
Feature still to come...
|
||||||
|
|
||||||
Running tests
|
Running tests
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@ -155,9 +324,25 @@ Run your tests using your project's ``manage.py`` utility::
|
|||||||
|
|
||||||
$ ./manage.py test
|
$ ./manage.py test
|
||||||
|
|
||||||
You'll see a bunch of text flow by as the test database is created, models are
|
If you only want to run tests for a particular application, add the
|
||||||
initialized, and your tests are run. If everything goes well, at the end
|
application name to the command line. For example, if your
|
||||||
you'll see::
|
``INSTALLED_APPS`` contains ``myproject.polls`` and ``myproject.animals``,
|
||||||
|
but you only want to run the animals unit tests, run::
|
||||||
|
|
||||||
|
$ ./manage.py test animals
|
||||||
|
|
||||||
|
When you run your tests, you'll see a bunch of text flow by as the test
|
||||||
|
database is created and models are initialized. This test database is
|
||||||
|
created from scratch every time you run your tests.
|
||||||
|
|
||||||
|
By default, the test database gets its name by prepending ``test_`` to
|
||||||
|
the database name specified by the ``DATABASE_NAME`` setting; all other
|
||||||
|
database settings will the same as they would be for the project normally.
|
||||||
|
If you wish to use a name other than the default for the test database,
|
||||||
|
you can use the ``TEST_DATABASE_NAME`` setting to provide a name.
|
||||||
|
|
||||||
|
Once the test database has been established, Django will run your tests.
|
||||||
|
If everything goes well, at the end you'll see::
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
Ran 22 tests in 0.221s
|
Ran 22 tests in 0.221s
|
||||||
@ -190,3 +375,77 @@ failed::
|
|||||||
|
|
||||||
FAILED (failures=1)
|
FAILED (failures=1)
|
||||||
|
|
||||||
|
When the tests have all been executed, the test database is destroyed.
|
||||||
|
|
||||||
|
Using a different testing framework
|
||||||
|
===================================
|
||||||
|
|
||||||
|
Doctest and Unittest are not the only Python testing frameworks. While
|
||||||
|
Django doesn't provide explicit support these alternative frameworks,
|
||||||
|
it does provide a mechanism to allow you to invoke tests constructed for
|
||||||
|
an alternative framework as if they were normal Django tests.
|
||||||
|
|
||||||
|
When you run ``./manage.py test``, Django looks at the ``TEST_RUNNER``
|
||||||
|
setting to determine what to do. By default, ``TEST_RUNNER`` points to ``django.test.simple.run_tests``. This method defines the default Django
|
||||||
|
testing behaviour. This behaviour involves:
|
||||||
|
|
||||||
|
#. Performing global pre-test setup
|
||||||
|
#. Creating the test database
|
||||||
|
#. Running ``syncdb`` to install models and initial data into the test database
|
||||||
|
#. Looking for Unit Tests and Doctests in ``models.py`` and ``tests.py`` file for each installed application
|
||||||
|
#. Running the Unit Tests and Doctests that are found
|
||||||
|
#. Destroying the test database.
|
||||||
|
#. Performing global post-test teardown
|
||||||
|
|
||||||
|
If you define your own test runner method and point ``TEST_RUNNER``
|
||||||
|
at that method, Django will execute your test runner whenever you run
|
||||||
|
``./manage.py test``. In this way, it is possible to use any test
|
||||||
|
framework that can be executed from Python code.
|
||||||
|
|
||||||
|
Defining a test runner
|
||||||
|
----------------------
|
||||||
|
By convention, a test runner should be called ``run_tests``; however, you
|
||||||
|
can call it anything you want. The only requirement is that it accept two
|
||||||
|
arguments:
|
||||||
|
|
||||||
|
``run_tests(module_list, verbosity=1)``
|
||||||
|
The module list is the list of Python modules that contain the models to be
|
||||||
|
tested. This is the same format returned by ``django.db.models.get_apps()``
|
||||||
|
|
||||||
|
Verbosity determines the amount of notification and debug information that
|
||||||
|
will be printed to the console; '0' is no output, '1' is normal output,
|
||||||
|
and `2` is verbose output.
|
||||||
|
|
||||||
|
Testing utilities
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
To assist in the creation of your own test runner, Django provides
|
||||||
|
a number of utility methods in the ``django.test.utils`` module.
|
||||||
|
|
||||||
|
``setup_test_environment()``
|
||||||
|
Performs any global pre-test setup, such as the installing the
|
||||||
|
instrumentation of the template rendering system.
|
||||||
|
|
||||||
|
``teardown_test_environment()``
|
||||||
|
Performs any global post-test teardown, such as removing the instrumentation
|
||||||
|
of the template rendering system.
|
||||||
|
|
||||||
|
``create_test_db(verbosity=1, autoclobber=False)``
|
||||||
|
Creates a new test database, and run ``syncdb`` against it.
|
||||||
|
|
||||||
|
``verbosity`` has the same behaviour as in the test runner.
|
||||||
|
|
||||||
|
``Autoclobber`` describes the behavior that will occur if a database with
|
||||||
|
the same name as the test database is discovered. If ``autoclobber`` is False,
|
||||||
|
the user will be asked to approve destroying the existing database. ``sys.exit``
|
||||||
|
is called if the user does not approve. If autoclobber is ``True``, the database
|
||||||
|
will be destroyed without consulting the user.
|
||||||
|
|
||||||
|
``create_test_db()`` has the side effect of modifying
|
||||||
|
``settings.DATABASE_NAME`` to match the name of the test database.
|
||||||
|
|
||||||
|
``destroy_test_db(old_database_name, verbosity=1)``
|
||||||
|
Destroys the database with the name ``settings.DATABASE_NAME`` matching,
|
||||||
|
and restores the value of ``settings.DATABASE_NAME`` to the provided name.
|
||||||
|
|
||||||
|
``verbosity`` has the same behaviour as in the test runner.
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
Models can have a ``get_latest_by`` attribute, which should be set to the name
|
Models can have a ``get_latest_by`` attribute, which should be set to the name
|
||||||
of a DateField or DateTimeField. If ``get_latest_by`` exists, the model's
|
of a DateField or DateTimeField. If ``get_latest_by`` exists, the model's
|
||||||
module will get a ``get_latest()`` function, which will return the latest
|
manager will get a ``latest()`` method, which will return the latest object in
|
||||||
object in the database according to that field. "Latest" means "having the
|
the database according to that field. "Latest" means "having the date farthest
|
||||||
date farthest into the future."
|
into the future."
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -30,7 +30,7 @@ class Person(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
__test__ = {'API_TESTS':"""
|
__test__ = {'API_TESTS':"""
|
||||||
# Because no Articles exist yet, get_latest() raises ArticleDoesNotExist.
|
# Because no Articles exist yet, latest() raises ArticleDoesNotExist.
|
||||||
>>> Article.objects.latest()
|
>>> Article.objects.latest()
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
|
0
tests/modeltests/test_client/__init__.py
Normal file
0
tests/modeltests/test_client/__init__.py
Normal file
10
tests/modeltests/test_client/management.py
Normal file
10
tests/modeltests/test_client/management.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.dispatch import dispatcher
|
||||||
|
from django.db.models import signals
|
||||||
|
import models as test_client_app
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
def setup_test(app, created_models, verbosity):
|
||||||
|
# Create a user account for the login-based tests
|
||||||
|
User.objects.create_user('testclient','testclient@example.com', 'password')
|
||||||
|
|
||||||
|
dispatcher.connect(setup_test, sender=test_client_app, signal=signals.post_syncdb)
|
101
tests/modeltests/test_client/models.py
Normal file
101
tests/modeltests/test_client/models.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
39. Testing using the Test Client
|
||||||
|
|
||||||
|
The test client is a class that can act like a simple
|
||||||
|
browser for testing purposes.
|
||||||
|
|
||||||
|
It allows the user to compose GET and POST requests, and
|
||||||
|
obtain the response that the server gave to those requests.
|
||||||
|
The server Response objects are annotated with the details
|
||||||
|
of the contexts and templates that were rendered during the
|
||||||
|
process of serving the request.
|
||||||
|
|
||||||
|
Client objects are stateful - they will retain cookie (and
|
||||||
|
thus session) details for the lifetime of the Client instance.
|
||||||
|
|
||||||
|
This is not intended as a replacement for Twill,Selenium, or
|
||||||
|
other browser automation frameworks - it is here to allow
|
||||||
|
testing against the contexts and templates produced by a view,
|
||||||
|
rather than the HTML rendered to the end-user.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.test.client import Client
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class ClientTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"Set up test environment"
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_get_view(self):
|
||||||
|
"GET a view"
|
||||||
|
response = self.client.get('/test_client/get_view/')
|
||||||
|
|
||||||
|
# Check some response details
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.context['var'], 42)
|
||||||
|
self.assertEqual(response.template.name, 'GET Template')
|
||||||
|
self.failUnless('This is a test.' in response.content)
|
||||||
|
|
||||||
|
def test_get_post_view(self):
|
||||||
|
"GET a view that normally expects POSTs"
|
||||||
|
response = self.client.get('/test_client/post_view/', {})
|
||||||
|
|
||||||
|
# Check some response details
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.template.name, 'Empty POST Template')
|
||||||
|
|
||||||
|
def test_empty_post(self):
|
||||||
|
"POST an empty dictionary to a view"
|
||||||
|
response = self.client.post('/test_client/post_view/', {})
|
||||||
|
|
||||||
|
# Check some response details
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.template.name, 'Empty POST Template')
|
||||||
|
|
||||||
|
def test_post_view(self):
|
||||||
|
"POST some data to a view"
|
||||||
|
post_data = {
|
||||||
|
'value': 37
|
||||||
|
}
|
||||||
|
response = self.client.post('/test_client/post_view/', post_data)
|
||||||
|
|
||||||
|
# Check some response details
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.context['data'], '37')
|
||||||
|
self.assertEqual(response.template.name, 'POST Template')
|
||||||
|
self.failUnless('Data received' in response.content)
|
||||||
|
|
||||||
|
def test_redirect(self):
|
||||||
|
"GET a URL that redirects elsewhere"
|
||||||
|
response = self.client.get('/test_client/redirect_view/')
|
||||||
|
|
||||||
|
# Check that the response was a 302 (redirect)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_unknown_page(self):
|
||||||
|
"GET an invalid URL"
|
||||||
|
response = self.client.get('/test_client/unknown_view/')
|
||||||
|
|
||||||
|
# Check that the response was a 404
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_view_with_login(self):
|
||||||
|
"Request a page that is protected with @login_required"
|
||||||
|
|
||||||
|
# Get the page without logging in. Should result in 302.
|
||||||
|
response = self.client.get('/test_client/login_protected_view/')
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Request a page that requires a login
|
||||||
|
response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password')
|
||||||
|
self.assertTrue(response)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.context['user'].username, 'testclient')
|
||||||
|
self.assertEqual(response.template.name, 'Login Template')
|
||||||
|
|
||||||
|
def test_view_with_bad_login(self):
|
||||||
|
"Request a page that is protected with @login, but use bad credentials"
|
||||||
|
|
||||||
|
response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword')
|
||||||
|
self.assertFalse(response)
|
9
tests/modeltests/test_client/urls.py
Normal file
9
tests/modeltests/test_client/urls.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.conf.urls.defaults import *
|
||||||
|
import views
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^get_view/$', views.get_view),
|
||||||
|
(r'^post_view/$', views.post_view),
|
||||||
|
(r'^redirect_view/$', views.redirect_view),
|
||||||
|
(r'^login_protected_view/$', views.login_protected_view),
|
||||||
|
)
|
35
tests/modeltests/test_client/views.py
Normal file
35
tests/modeltests/test_client/views.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.template import Context, Template
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
def get_view(request):
|
||||||
|
"A simple view that expects a GET request, and returns a rendered template"
|
||||||
|
t = Template('This is a test. {{ var }} is the value.', name='GET Template')
|
||||||
|
c = Context({'var': 42})
|
||||||
|
|
||||||
|
return HttpResponse(t.render(c))
|
||||||
|
|
||||||
|
def post_view(request):
|
||||||
|
"""A view that expects a POST, and returns a different template depending
|
||||||
|
on whether any POST data is available
|
||||||
|
"""
|
||||||
|
if request.POST:
|
||||||
|
t = Template('Data received: {{ data }} is the value.', name='POST Template')
|
||||||
|
c = Context({'data': request.POST['value']})
|
||||||
|
else:
|
||||||
|
t = Template('Viewing POST page.', name='Empty POST Template')
|
||||||
|
c = Context()
|
||||||
|
|
||||||
|
return HttpResponse(t.render(c))
|
||||||
|
|
||||||
|
def redirect_view(request):
|
||||||
|
"A view that redirects all requests to the GET view"
|
||||||
|
return HttpResponseRedirect('/test_client/get_view/')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def login_protected_view(request):
|
||||||
|
"A simple view that is login protected."
|
||||||
|
t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template')
|
||||||
|
c = Context({'user': request.user})
|
||||||
|
|
||||||
|
return HttpResponse(t.render(c))
|
@ -84,7 +84,7 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"),
|
'basic-syntax03': ("{{ first }} --- {{ second }}", {"first" : 1, "second" : 2}, "1 --- 2"),
|
||||||
|
|
||||||
# Fail silently when a variable is not found in the current context
|
# Fail silently when a variable is not found in the current context
|
||||||
'basic-syntax04': ("as{{ missing }}df", {}, "asINVALIDdf"),
|
'basic-syntax04': ("as{{ missing }}df", {}, ("asdf","asINVALIDdf")),
|
||||||
|
|
||||||
# A variable may not contain more than one word
|
# A variable may not contain more than one word
|
||||||
'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError),
|
'basic-syntax06': ("{{ multi word variable }}", {}, template.TemplateSyntaxError),
|
||||||
@ -100,7 +100,7 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"),
|
'basic-syntax10': ("{{ var.otherclass.method }}", {"var": SomeClass()}, "OtherClass.method"),
|
||||||
|
|
||||||
# Fail silently when a variable's attribute isn't found
|
# Fail silently when a variable's attribute isn't found
|
||||||
'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, "INVALID"),
|
'basic-syntax11': ("{{ var.blech }}", {"var": SomeClass()}, ("","INVALID")),
|
||||||
|
|
||||||
# Raise TemplateSyntaxError when trying to access a variable beginning with an underscore
|
# Raise TemplateSyntaxError when trying to access a variable beginning with an underscore
|
||||||
'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError),
|
'basic-syntax12': ("{{ var.__dict__ }}", {"var": SomeClass()}, template.TemplateSyntaxError),
|
||||||
@ -116,10 +116,10 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"),
|
'basic-syntax18': ("{{ foo.bar }}", {"foo" : {"bar" : "baz"}}, "baz"),
|
||||||
|
|
||||||
# Fail silently when a variable's dictionary key isn't found
|
# Fail silently when a variable's dictionary key isn't found
|
||||||
'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, "INVALID"),
|
'basic-syntax19': ("{{ foo.spam }}", {"foo" : {"bar" : "baz"}}, ("","INVALID")),
|
||||||
|
|
||||||
# Fail silently when accessing a non-simple method
|
# Fail silently when accessing a non-simple method
|
||||||
'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, "INVALID"),
|
'basic-syntax20': ("{{ var.method2 }}", {"var": SomeClass()}, ("","INVALID")),
|
||||||
|
|
||||||
# Basic filter usage
|
# Basic filter usage
|
||||||
'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"),
|
'basic-syntax21': ("{{ var|upper }}", {"var": "Django is the greatest!"}, "DJANGO IS THE GREATEST!"),
|
||||||
@ -158,7 +158,7 @@ class Templates(unittest.TestCase):
|
|||||||
'basic-syntax32': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'),
|
'basic-syntax32': (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'),
|
||||||
|
|
||||||
# Fail silently for methods that raise an exception with a "silent_variable_failure" attribute
|
# Fail silently for methods that raise an exception with a "silent_variable_failure" attribute
|
||||||
'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, "1INVALID2"),
|
'basic-syntax33': (r'1{{ var.method3 }}2', {"var": SomeClass()}, ("12", "1INVALID2")),
|
||||||
|
|
||||||
# In methods that raise an exception without a "silent_variable_attribute" set to True,
|
# In methods that raise an exception without a "silent_variable_attribute" set to True,
|
||||||
# the exception propogates
|
# the exception propogates
|
||||||
@ -464,6 +464,14 @@ class Templates(unittest.TestCase):
|
|||||||
# translation of a constant string
|
# translation of a constant string
|
||||||
'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'),
|
'i18n13': ('{{ _("Page not found") }}', {'LANGUAGE_CODE': 'de'}, 'Seite nicht gefunden'),
|
||||||
|
|
||||||
|
### HANDLING OF TEMPLATE_TAG_IF_INVALID ###################################
|
||||||
|
|
||||||
|
'invalidstr01': ('{{ var|default:"Foo" }}', {}, ('Foo','INVALID')),
|
||||||
|
'invalidstr02': ('{{ var|default_if_none:"Foo" }}', {}, ('','INVALID')),
|
||||||
|
'invalidstr03': ('{% for v in var %}({{ v }}){% endfor %}', {}, ''),
|
||||||
|
'invalidstr04': ('{% if var %}Yes{% else %}No{% endif %}', {}, 'No'),
|
||||||
|
'invalidstr04': ('{% if var|default:"Foo" %}Yes{% else %}No{% endif %}', {}, 'Yes'),
|
||||||
|
|
||||||
### MULTILINE #############################################################
|
### MULTILINE #############################################################
|
||||||
|
|
||||||
'multiline01': ("""
|
'multiline01': ("""
|
||||||
@ -507,7 +515,7 @@ class Templates(unittest.TestCase):
|
|||||||
'{{ item.foo }}' + \
|
'{{ item.foo }}' + \
|
||||||
'{% endfor %},' + \
|
'{% endfor %},' + \
|
||||||
'{% endfor %}',
|
'{% endfor %}',
|
||||||
{}, 'INVALID:INVALIDINVALIDINVALIDINVALIDINVALIDINVALIDINVALID,'),
|
{}, ''),
|
||||||
|
|
||||||
### TEMPLATETAG TAG #######################################################
|
### TEMPLATETAG TAG #######################################################
|
||||||
'templatetag01': ('{% templatetag openblock %}', {}, '{%'),
|
'templatetag01': ('{% templatetag openblock %}', {}, '{%'),
|
||||||
@ -592,30 +600,44 @@ class Templates(unittest.TestCase):
|
|||||||
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
|
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
|
||||||
|
|
||||||
# Set TEMPLATE_STRING_IF_INVALID to a known string
|
# Set TEMPLATE_STRING_IF_INVALID to a known string
|
||||||
old_invalid, settings.TEMPLATE_STRING_IF_INVALID = settings.TEMPLATE_STRING_IF_INVALID, 'INVALID'
|
old_invalid = settings.TEMPLATE_STRING_IF_INVALID
|
||||||
|
|
||||||
for name, vals in tests:
|
for name, vals in tests:
|
||||||
install()
|
install()
|
||||||
|
|
||||||
|
if isinstance(vals[2], tuple):
|
||||||
|
normal_string_result = vals[2][0]
|
||||||
|
invalid_string_result = vals[2][1]
|
||||||
|
else:
|
||||||
|
normal_string_result = vals[2]
|
||||||
|
invalid_string_result = vals[2]
|
||||||
|
|
||||||
if 'LANGUAGE_CODE' in vals[1]:
|
if 'LANGUAGE_CODE' in vals[1]:
|
||||||
activate(vals[1]['LANGUAGE_CODE'])
|
activate(vals[1]['LANGUAGE_CODE'])
|
||||||
else:
|
else:
|
||||||
activate('en-us')
|
activate('en-us')
|
||||||
try:
|
|
||||||
output = loader.get_template(name).render(template.Context(vals[1]))
|
for invalid_str, result in [('', normal_string_result),
|
||||||
except Exception, e:
|
('INVALID', invalid_string_result)]:
|
||||||
if e.__class__ != vals[2]:
|
settings.TEMPLATE_STRING_IF_INVALID = invalid_str
|
||||||
failures.append("Template test: %s -- FAILED. Got %s, exception: %s" % (name, e.__class__, e))
|
try:
|
||||||
continue
|
output = loader.get_template(name).render(template.Context(vals[1]))
|
||||||
|
except Exception, e:
|
||||||
|
if e.__class__ != result:
|
||||||
|
failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e))
|
||||||
|
continue
|
||||||
|
if output != result:
|
||||||
|
failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output))
|
||||||
|
|
||||||
if 'LANGUAGE_CODE' in vals[1]:
|
if 'LANGUAGE_CODE' in vals[1]:
|
||||||
deactivate()
|
deactivate()
|
||||||
if output != vals[2]:
|
|
||||||
failures.append("Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output))
|
|
||||||
loader.template_source_loaders = old_template_loaders
|
loader.template_source_loaders = old_template_loaders
|
||||||
deactivate()
|
deactivate()
|
||||||
settings.TEMPLATE_DEBUG = old_td
|
settings.TEMPLATE_DEBUG = old_td
|
||||||
settings.TEMPLATE_STRING_IF_INVALID = old_invalid
|
settings.TEMPLATE_STRING_IF_INVALID = old_invalid
|
||||||
|
|
||||||
self.assertEqual(failures, [])
|
self.assertEqual(failures, [], '\n'.join(failures))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -5,6 +5,8 @@ import unittest
|
|||||||
|
|
||||||
MODEL_TESTS_DIR_NAME = 'modeltests'
|
MODEL_TESTS_DIR_NAME = 'modeltests'
|
||||||
REGRESSION_TESTS_DIR_NAME = 'regressiontests'
|
REGRESSION_TESTS_DIR_NAME = 'regressiontests'
|
||||||
|
TEST_DATABASE_NAME = 'django_test_db'
|
||||||
|
TEST_TEMPLATE_DIR = 'templates'
|
||||||
|
|
||||||
MODEL_TEST_DIR = os.path.join(os.path.dirname(__file__), MODEL_TESTS_DIR_NAME)
|
MODEL_TEST_DIR = os.path.join(os.path.dirname(__file__), MODEL_TESTS_DIR_NAME)
|
||||||
REGRESSION_TEST_DIR = os.path.join(os.path.dirname(__file__), REGRESSION_TESTS_DIR_NAME)
|
REGRESSION_TEST_DIR = os.path.join(os.path.dirname(__file__), REGRESSION_TESTS_DIR_NAME)
|
||||||
@ -70,14 +72,23 @@ class InvalidModelTestCase(unittest.TestCase):
|
|||||||
def django_tests(verbosity, tests_to_run):
|
def django_tests(verbosity, tests_to_run):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.loading import get_apps, load_app
|
from django.db.models.loading import get_apps, load_app
|
||||||
|
|
||||||
old_installed_apps = settings.INSTALLED_APPS
|
old_installed_apps = settings.INSTALLED_APPS
|
||||||
|
old_test_database_name = settings.TEST_DATABASE_NAME
|
||||||
|
old_root_urlconf = settings.ROOT_URLCONF
|
||||||
|
old_template_dirs = settings.TEMPLATE_DIRS
|
||||||
|
|
||||||
|
# Redirect some settings for the duration of these tests
|
||||||
|
settings.TEST_DATABASE_NAME = TEST_DATABASE_NAME
|
||||||
|
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
|
||||||
|
settings.ROOT_URLCONF = 'urls'
|
||||||
|
settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),)
|
||||||
|
|
||||||
# load all the ALWAYS_INSTALLED_APPS
|
# load all the ALWAYS_INSTALLED_APPS
|
||||||
settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS
|
|
||||||
get_apps()
|
get_apps()
|
||||||
|
|
||||||
test_models = []
|
|
||||||
# Load all the test model apps
|
# Load all the test model apps
|
||||||
|
test_models = []
|
||||||
for model_dir, model_name in get_test_models():
|
for model_dir, model_name in get_test_models():
|
||||||
model_label = '.'.join([model_dir, model_name])
|
model_label = '.'.join([model_dir, model_name])
|
||||||
try:
|
try:
|
||||||
@ -105,8 +116,11 @@ def django_tests(verbosity, tests_to_run):
|
|||||||
from django.test.simple import run_tests
|
from django.test.simple import run_tests
|
||||||
run_tests(test_models, verbosity, extra_tests=extra_tests)
|
run_tests(test_models, verbosity, extra_tests=extra_tests)
|
||||||
|
|
||||||
# Restore the old INSTALLED_APPS setting
|
# Restore the old settings
|
||||||
settings.INSTALLED_APPS = old_installed_apps
|
settings.INSTALLED_APPS = old_installed_apps
|
||||||
|
settings.TESTS_DATABASE_NAME = old_test_database_name
|
||||||
|
settings.ROOT_URLCONF = old_root_urlconf
|
||||||
|
settings.TEMPLATE_DIRS = old_template_dirs
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
1
tests/templates/404.html
Normal file
1
tests/templates/404.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
Django Internal Tests: 404 Error
|
1
tests/templates/500.html
Normal file
1
tests/templates/500.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
Django Internal Tests: 500 Error
|
19
tests/templates/login.html
Normal file
19
tests/templates/login.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<h1>Django Internal Tests: Login</h1>
|
||||||
|
{% if form.has_errors %}
|
||||||
|
<p>Your username and password didn't match. Please try again.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action=".">
|
||||||
|
<table>
|
||||||
|
<tr><td><label for="id_username">Username:</label></td><td>{{ form.username }}</td></tr>
|
||||||
|
<tr><td><label for="id_password">Password:</label></td><td>{{ form.password }}</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="submit" value="login" />
|
||||||
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
tests/urls.py
Normal file
10
tests/urls.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
# test_client modeltest urls
|
||||||
|
(r'^test_client/', include('modeltests.test_client.urls')),
|
||||||
|
|
||||||
|
# Always provide the auth system login and logout views
|
||||||
|
(r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}),
|
||||||
|
(r'^accounts/logout/$', 'django.contrib.auth.views.login'),
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user