mirror of
https://github.com/django/django.git
synced 2025-07-07 11:19:12 +00:00
[soc2009/model-validation] merged to trunk at r11032
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/model-validation@11038 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
3a331970b4
commit
98e6ab0afc
@ -37,6 +37,9 @@ def post_comment(request, next=None):
|
|||||||
if not data.get('email', ''):
|
if not data.get('email', ''):
|
||||||
data["email"] = request.user.email
|
data["email"] = request.user.email
|
||||||
|
|
||||||
|
# Check to see if the POST data overrides the view's next argument.
|
||||||
|
next = data.get("next", next)
|
||||||
|
|
||||||
# Look up the object we're trying to comment about
|
# Look up the object we're trying to comment about
|
||||||
ctype = data.get("content_type")
|
ctype = data.get("content_type")
|
||||||
object_pk = data.get("object_pk")
|
object_pk = data.get("object_pk")
|
||||||
|
@ -321,7 +321,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Avoid a circular import.
|
# Avoid a circular import.
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
if self.instance is None:
|
if self.instance is None or self.instance.pk is None:
|
||||||
return self.model._default_manager.none()
|
return self.model._default_manager.none()
|
||||||
return self.model._default_manager.filter(**{
|
return self.model._default_manager.filter(**{
|
||||||
self.ct_field.name: ContentType.objects.get_for_model(self.instance),
|
self.ct_field.name: ContentType.objects.get_for_model(self.instance),
|
||||||
|
@ -14,7 +14,7 @@ if lib_path:
|
|||||||
lib_names = None
|
lib_names = None
|
||||||
elif os.name == 'nt':
|
elif os.name == 'nt':
|
||||||
# Windows NT shared library
|
# Windows NT shared library
|
||||||
lib_names = ['gdal15']
|
lib_names = ['gdal16', 'gdal15']
|
||||||
elif os.name == 'posix':
|
elif os.name == 'posix':
|
||||||
# *NIX library names.
|
# *NIX library names.
|
||||||
lib_names = ['gdal', 'GDAL', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0']
|
lib_names = ['gdal', 'GDAL', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0']
|
||||||
|
@ -84,16 +84,15 @@ class GeoIPTest(unittest.TestCase):
|
|||||||
self.assertEqual('USA', d['country_code3'])
|
self.assertEqual('USA', d['country_code3'])
|
||||||
self.assertEqual('Houston', d['city'])
|
self.assertEqual('Houston', d['city'])
|
||||||
self.assertEqual('TX', d['region'])
|
self.assertEqual('TX', d['region'])
|
||||||
self.assertEqual('77002', d['postal_code'])
|
|
||||||
self.assertEqual(713, d['area_code'])
|
self.assertEqual(713, d['area_code'])
|
||||||
geom = g.geos(query)
|
geom = g.geos(query)
|
||||||
self.failIf(not isinstance(geom, GEOSGeometry))
|
self.failIf(not isinstance(geom, GEOSGeometry))
|
||||||
lon, lat = (-95.366996765, 29.752300262)
|
lon, lat = (-95.4152, 29.7755)
|
||||||
lat_lon = g.lat_lon(query)
|
lat_lon = g.lat_lon(query)
|
||||||
lat_lon = (lat_lon[1], lat_lon[0])
|
lat_lon = (lat_lon[1], lat_lon[0])
|
||||||
for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon):
|
for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon):
|
||||||
self.assertAlmostEqual(lon, tup[0], 9)
|
self.assertAlmostEqual(lon, tup[0], 4)
|
||||||
self.assertAlmostEqual(lat, tup[1], 9)
|
self.assertAlmostEqual(lat, tup[1], 4)
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
|
@ -83,8 +83,17 @@ class GeoIPRecord(Structure):
|
|||||||
('postal_code', c_char_p),
|
('postal_code', c_char_p),
|
||||||
('latitude', c_float),
|
('latitude', c_float),
|
||||||
('longitude', c_float),
|
('longitude', c_float),
|
||||||
|
# TODO: In 1.4.6 this changed from `int dma_code;` to
|
||||||
|
# `union {int metro_code; int dma_code;};`. Change
|
||||||
|
# to a `ctypes.Union` in to accomodate in future when
|
||||||
|
# pre-1.4.6 versions are no longer distributed.
|
||||||
('dma_code', c_int),
|
('dma_code', c_int),
|
||||||
('area_code', c_int),
|
('area_code', c_int),
|
||||||
|
# TODO: The following structure fields were added in 1.4.3 --
|
||||||
|
# uncomment these fields when sure previous versions are no
|
||||||
|
# longer distributed by package maintainers.
|
||||||
|
#('charset', c_int),
|
||||||
|
#('continent_code', c_char_p),
|
||||||
]
|
]
|
||||||
class GeoIPTag(Structure): pass
|
class GeoIPTag(Structure): pass
|
||||||
|
|
||||||
@ -99,9 +108,12 @@ def record_output(func):
|
|||||||
rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr)
|
rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr)
|
||||||
rec_by_name = record_output(lgeoip.GeoIP_record_by_name)
|
rec_by_name = record_output(lgeoip.GeoIP_record_by_name)
|
||||||
|
|
||||||
# For opening up GeoIP databases.
|
# For opening & closing GeoIP database files.
|
||||||
geoip_open = lgeoip.GeoIP_open
|
geoip_open = lgeoip.GeoIP_open
|
||||||
geoip_open.restype = DBTYPE
|
geoip_open.restype = DBTYPE
|
||||||
|
geoip_close = lgeoip.GeoIP_delete
|
||||||
|
geoip_close.argtypes = [DBTYPE]
|
||||||
|
geoip_close.restype = None
|
||||||
|
|
||||||
# String output routines.
|
# String output routines.
|
||||||
def string_output(func):
|
def string_output(func):
|
||||||
@ -136,6 +148,12 @@ class GeoIP(object):
|
|||||||
GEOIP_CHECK_CACHE = 2
|
GEOIP_CHECK_CACHE = 2
|
||||||
GEOIP_INDEX_CACHE = 4
|
GEOIP_INDEX_CACHE = 4
|
||||||
cache_options = dict((opt, None) for opt in (0, 1, 2, 4))
|
cache_options = dict((opt, None) for opt in (0, 1, 2, 4))
|
||||||
|
_city_file = ''
|
||||||
|
_country_file = ''
|
||||||
|
|
||||||
|
# Initially, pointers to GeoIP file references are NULL.
|
||||||
|
_city = None
|
||||||
|
_country = None
|
||||||
|
|
||||||
def __init__(self, path=None, cache=0, country=None, city=None):
|
def __init__(self, path=None, cache=0, country=None, city=None):
|
||||||
"""
|
"""
|
||||||
@ -174,13 +192,19 @@ class GeoIP(object):
|
|||||||
if not isinstance(path, basestring):
|
if not isinstance(path, basestring):
|
||||||
raise TypeError('Invalid path type: %s' % type(path).__name__)
|
raise TypeError('Invalid path type: %s' % type(path).__name__)
|
||||||
|
|
||||||
cntry_ptr, city_ptr = (None, None)
|
|
||||||
if os.path.isdir(path):
|
if os.path.isdir(path):
|
||||||
# Getting the country and city files using the settings
|
# Constructing the GeoIP database filenames using the settings
|
||||||
# dictionary. If no settings are provided, default names
|
# dictionary. If the database files for the GeoLite country
|
||||||
# are assigned.
|
# and/or city datasets exist, then try and open them.
|
||||||
country = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat'))
|
country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.dat'))
|
||||||
city = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
|
if os.path.isfile(country_db):
|
||||||
|
self._country = geoip_open(country_db, cache)
|
||||||
|
self._country_file = country_db
|
||||||
|
|
||||||
|
city_db = os.path.join(path, city or GEOIP_SETTINGS.get('GEOIP_CITY', 'GeoLiteCity.dat'))
|
||||||
|
if os.path.isfile(city_db):
|
||||||
|
self._city = geoip_open(city_db, cache)
|
||||||
|
self._city_file = city_db
|
||||||
elif os.path.isfile(path):
|
elif os.path.isfile(path):
|
||||||
# Otherwise, some detective work will be needed to figure
|
# Otherwise, some detective work will be needed to figure
|
||||||
# out whether the given database path is for the GeoIP country
|
# out whether the given database path is for the GeoIP country
|
||||||
@ -188,29 +212,22 @@ class GeoIP(object):
|
|||||||
ptr = geoip_open(path, cache)
|
ptr = geoip_open(path, cache)
|
||||||
info = geoip_dbinfo(ptr)
|
info = geoip_dbinfo(ptr)
|
||||||
if lite_regex.match(info):
|
if lite_regex.match(info):
|
||||||
# GeoLite City database.
|
# GeoLite City database detected.
|
||||||
city, city_ptr = path, ptr
|
self._city = ptr
|
||||||
|
self._city_file = path
|
||||||
elif free_regex.match(info):
|
elif free_regex.match(info):
|
||||||
# GeoIP Country database.
|
# GeoIP Country database detected.
|
||||||
country, cntry_ptr = path, ptr
|
self._country = ptr
|
||||||
|
self._country_file = path
|
||||||
else:
|
else:
|
||||||
raise GeoIPException('Unable to recognize database edition: %s' % info)
|
raise GeoIPException('Unable to recognize database edition: %s' % info)
|
||||||
else:
|
else:
|
||||||
raise GeoIPException('GeoIP path must be a valid file or directory.')
|
raise GeoIPException('GeoIP path must be a valid file or directory.')
|
||||||
|
|
||||||
# `_init_db` does the dirty work.
|
def __del__(self):
|
||||||
self._init_db(country, cache, '_country', cntry_ptr)
|
# Cleaning any GeoIP file handles lying around.
|
||||||
self._init_db(city, cache, '_city', city_ptr)
|
if self._country: geoip_close(self._country)
|
||||||
|
if self._city: geoip_close(self._city)
|
||||||
def _init_db(self, db_file, cache, attname, ptr=None):
|
|
||||||
"Helper routine for setting GeoIP ctypes database properties."
|
|
||||||
if ptr:
|
|
||||||
# Pointer already retrieved.
|
|
||||||
pass
|
|
||||||
elif os.path.isfile(db_file or ''):
|
|
||||||
ptr = geoip_open(db_file, cache)
|
|
||||||
setattr(self, attname, ptr)
|
|
||||||
setattr(self, '%s_file' % attname, db_file)
|
|
||||||
|
|
||||||
def _check_query(self, query, country=False, city=False, city_or_country=False):
|
def _check_query(self, query, country=False, city=False, city_or_country=False):
|
||||||
"Helper routine for checking the query and database availability."
|
"Helper routine for checking the query and database availability."
|
||||||
@ -219,11 +236,11 @@ class GeoIP(object):
|
|||||||
raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
|
raise TypeError('GeoIP query must be a string, not type %s' % type(query).__name__)
|
||||||
|
|
||||||
# Extra checks for the existence of country and city databases.
|
# Extra checks for the existence of country and city databases.
|
||||||
if city_or_country and self._country is None and self._city is None:
|
if city_or_country and not (self._country or self._city):
|
||||||
raise GeoIPException('Invalid GeoIP country and city data files.')
|
raise GeoIPException('Invalid GeoIP country and city data files.')
|
||||||
elif country and self._country is None:
|
elif country and not self._country:
|
||||||
raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file)
|
raise GeoIPException('Invalid GeoIP country data file: %s' % self._country_file)
|
||||||
elif city and self._city is None:
|
elif city and not self._city:
|
||||||
raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file)
|
raise GeoIPException('Invalid GeoIP city data file: %s' % self._city_file)
|
||||||
|
|
||||||
def city(self, query):
|
def city(self, query):
|
||||||
|
@ -195,7 +195,7 @@ class EmailMessage(object):
|
|||||||
A container for email information.
|
A container for email information.
|
||||||
"""
|
"""
|
||||||
content_subtype = 'plain'
|
content_subtype = 'plain'
|
||||||
multipart_subtype = 'mixed'
|
mixed_subtype = 'mixed'
|
||||||
encoding = None # None => use settings default
|
encoding = None # None => use settings default
|
||||||
|
|
||||||
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
|
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
|
||||||
@ -234,16 +234,7 @@ class EmailMessage(object):
|
|||||||
encoding = self.encoding or settings.DEFAULT_CHARSET
|
encoding = self.encoding or settings.DEFAULT_CHARSET
|
||||||
msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
|
msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
|
||||||
self.content_subtype, encoding)
|
self.content_subtype, encoding)
|
||||||
if self.attachments:
|
msg = self._create_message(msg)
|
||||||
body_msg = msg
|
|
||||||
msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
|
|
||||||
if self.body:
|
|
||||||
msg.attach(body_msg)
|
|
||||||
for attachment in self.attachments:
|
|
||||||
if isinstance(attachment, MIMEBase):
|
|
||||||
msg.attach(attachment)
|
|
||||||
else:
|
|
||||||
msg.attach(self._create_attachment(*attachment))
|
|
||||||
msg['Subject'] = self.subject
|
msg['Subject'] = self.subject
|
||||||
msg['From'] = self.extra_headers.pop('From', self.from_email)
|
msg['From'] = self.extra_headers.pop('From', self.from_email)
|
||||||
msg['To'] = ', '.join(self.to)
|
msg['To'] = ', '.join(self.to)
|
||||||
@ -277,8 +268,7 @@ class EmailMessage(object):
|
|||||||
def attach(self, filename=None, content=None, mimetype=None):
|
def attach(self, filename=None, content=None, mimetype=None):
|
||||||
"""
|
"""
|
||||||
Attaches a file with the given filename and content. The filename can
|
Attaches a file with the given filename and content. The filename can
|
||||||
be omitted (useful for multipart/alternative messages) and the mimetype
|
be omitted and the mimetype is guessed, if not provided.
|
||||||
is guessed, if not provided.
|
|
||||||
|
|
||||||
If the first parameter is a MIMEBase subclass it is inserted directly
|
If the first parameter is a MIMEBase subclass it is inserted directly
|
||||||
into the resulting message attachments.
|
into the resulting message attachments.
|
||||||
@ -296,15 +286,26 @@ class EmailMessage(object):
|
|||||||
content = open(path, 'rb').read()
|
content = open(path, 'rb').read()
|
||||||
self.attach(filename, content, mimetype)
|
self.attach(filename, content, mimetype)
|
||||||
|
|
||||||
def _create_attachment(self, filename, content, mimetype=None):
|
def _create_message(self, msg):
|
||||||
|
return self._create_attachments(msg)
|
||||||
|
|
||||||
|
def _create_attachments(self, msg):
|
||||||
|
if self.attachments:
|
||||||
|
body_msg = msg
|
||||||
|
msg = SafeMIMEMultipart(_subtype=self.mixed_subtype)
|
||||||
|
if self.body:
|
||||||
|
msg.attach(body_msg)
|
||||||
|
for attachment in self.attachments:
|
||||||
|
if isinstance(attachment, MIMEBase):
|
||||||
|
msg.attach(attachment)
|
||||||
|
else:
|
||||||
|
msg.attach(self._create_attachment(*attachment))
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def _create_mime_attachment(self, content, mimetype):
|
||||||
"""
|
"""
|
||||||
Converts the filename, content, mimetype triple into a MIME attachment
|
Converts the content, mimetype pair into a MIME attachment object.
|
||||||
object.
|
|
||||||
"""
|
"""
|
||||||
if mimetype is None:
|
|
||||||
mimetype, _ = mimetypes.guess_type(filename)
|
|
||||||
if mimetype is None:
|
|
||||||
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
|
||||||
basetype, subtype = mimetype.split('/', 1)
|
basetype, subtype = mimetype.split('/', 1)
|
||||||
if basetype == 'text':
|
if basetype == 'text':
|
||||||
attachment = SafeMIMEText(smart_str(content,
|
attachment = SafeMIMEText(smart_str(content,
|
||||||
@ -314,6 +315,18 @@ class EmailMessage(object):
|
|||||||
attachment = MIMEBase(basetype, subtype)
|
attachment = MIMEBase(basetype, subtype)
|
||||||
attachment.set_payload(content)
|
attachment.set_payload(content)
|
||||||
Encoders.encode_base64(attachment)
|
Encoders.encode_base64(attachment)
|
||||||
|
return attachment
|
||||||
|
|
||||||
|
def _create_attachment(self, filename, content, mimetype=None):
|
||||||
|
"""
|
||||||
|
Converts the filename, content, mimetype triple into a MIME attachment
|
||||||
|
object.
|
||||||
|
"""
|
||||||
|
if mimetype is None:
|
||||||
|
mimetype, _ = mimetypes.guess_type(filename)
|
||||||
|
if mimetype is None:
|
||||||
|
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
||||||
|
attachment = self._create_mime_attachment(content, mimetype)
|
||||||
if filename:
|
if filename:
|
||||||
attachment.add_header('Content-Disposition', 'attachment',
|
attachment.add_header('Content-Disposition', 'attachment',
|
||||||
filename=filename)
|
filename=filename)
|
||||||
@ -325,11 +338,39 @@ class EmailMultiAlternatives(EmailMessage):
|
|||||||
messages. For example, including text and HTML versions of the text is
|
messages. For example, including text and HTML versions of the text is
|
||||||
made easier.
|
made easier.
|
||||||
"""
|
"""
|
||||||
multipart_subtype = 'alternative'
|
alternative_subtype = 'alternative'
|
||||||
|
|
||||||
def attach_alternative(self, content, mimetype=None):
|
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
|
||||||
|
connection=None, attachments=None, headers=None, alternatives=None):
|
||||||
|
"""
|
||||||
|
Initialize a single email message (which can be sent to multiple
|
||||||
|
recipients).
|
||||||
|
|
||||||
|
All strings used to create the message can be unicode strings (or UTF-8
|
||||||
|
bytestrings). The SafeMIMEText class will handle any necessary encoding
|
||||||
|
conversions.
|
||||||
|
"""
|
||||||
|
super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
|
||||||
|
self.alternatives=alternatives or []
|
||||||
|
|
||||||
|
def attach_alternative(self, content, mimetype):
|
||||||
"""Attach an alternative content representation."""
|
"""Attach an alternative content representation."""
|
||||||
self.attach(content=content, mimetype=mimetype)
|
assert content is not None
|
||||||
|
assert mimetype is not None
|
||||||
|
self.alternatives.append((content, mimetype))
|
||||||
|
|
||||||
|
def _create_message(self, msg):
|
||||||
|
return self._create_attachments(self._create_alternatives(msg))
|
||||||
|
|
||||||
|
def _create_alternatives(self, msg):
|
||||||
|
if self.alternatives:
|
||||||
|
body_msg = msg
|
||||||
|
msg = SafeMIMEMultipart(_subtype=self.alternative_subtype)
|
||||||
|
if self.body:
|
||||||
|
msg.attach(body_msg)
|
||||||
|
for alternative in self.alternatives:
|
||||||
|
msg.attach(self._create_mime_attachment(*alternative))
|
||||||
|
return msg
|
||||||
|
|
||||||
def send_mail(subject, message, from_email, recipient_list,
|
def send_mail(subject, message, from_email, recipient_list,
|
||||||
fail_silently=False, auth_user=None, auth_password=None):
|
fail_silently=False, auth_user=None, auth_password=None):
|
||||||
|
@ -73,7 +73,7 @@ class Command(BaseCommand):
|
|||||||
model_list = get_models(app)
|
model_list = get_models(app)
|
||||||
|
|
||||||
for model in model_list:
|
for model in model_list:
|
||||||
objects.extend(model.objects.all())
|
objects.extend(model._default_manager.all())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return serializers.serialize(format, objects, indent=indent)
|
return serializers.serialize(format, objects, indent=indent)
|
||||||
|
@ -25,6 +25,13 @@ class BaseDatabaseCreation(object):
|
|||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
|
||||||
|
def _digest(self, *args):
|
||||||
|
"""
|
||||||
|
Generates a 32-bit digest of a set of arguments that can be used to
|
||||||
|
shorten identifying names.
|
||||||
|
"""
|
||||||
|
return '%x' % (abs(hash(args)) % 4294967296L) # 2**32
|
||||||
|
|
||||||
def sql_create_model(self, model, style, known_models=set()):
|
def sql_create_model(self, model, style, known_models=set()):
|
||||||
"""
|
"""
|
||||||
Returns the SQL required to create a single model, as a tuple of:
|
Returns the SQL required to create a single model, as a tuple of:
|
||||||
@ -128,7 +135,7 @@ class BaseDatabaseCreation(object):
|
|||||||
col = opts.get_field(f.rel.field_name).column
|
col = opts.get_field(f.rel.field_name).column
|
||||||
# For MySQL, r_name must be unique in the first 64 characters.
|
# For MySQL, r_name must be unique in the first 64 characters.
|
||||||
# So we are careful with character usage here.
|
# So we are careful with character usage here.
|
||||||
r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table))))
|
r_name = '%s_refs_%s_%s' % (r_col, col, self._digest(r_table, table))
|
||||||
final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
|
final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \
|
||||||
(qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())),
|
(qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())),
|
||||||
qn(r_col), qn(table), qn(col),
|
qn(r_col), qn(table), qn(col),
|
||||||
@ -187,8 +194,7 @@ class BaseDatabaseCreation(object):
|
|||||||
output.append('\n'.join(table_output))
|
output.append('\n'.join(table_output))
|
||||||
|
|
||||||
for r_table, r_col, table, col in deferred:
|
for r_table, r_col, table, col in deferred:
|
||||||
r_name = '%s_refs_%s_%x' % (r_col, col,
|
r_name = '%s_refs_%s_%s' % (r_col, col, self._digest(r_table, table))
|
||||||
abs(hash((r_table, table))))
|
|
||||||
output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
|
output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' %
|
||||||
(qn(r_table),
|
(qn(r_table),
|
||||||
qn(truncate_name(r_name, self.connection.ops.max_name_length())),
|
qn(truncate_name(r_name, self.connection.ops.max_name_length())),
|
||||||
@ -289,7 +295,7 @@ class BaseDatabaseCreation(object):
|
|||||||
col = f.column
|
col = f.column
|
||||||
r_table = model._meta.db_table
|
r_table = model._meta.db_table
|
||||||
r_col = model._meta.get_field(f.rel.field_name).column
|
r_col = model._meta.get_field(f.rel.field_name).column
|
||||||
r_name = '%s_refs_%s_%x' % (col, r_col, abs(hash((table, r_table))))
|
r_name = '%s_refs_%s_%s' % (col, r_col, self._digest(table, r_table))
|
||||||
output.append('%s %s %s %s;' % \
|
output.append('%s %s %s %s;' % \
|
||||||
(style.SQL_KEYWORD('ALTER TABLE'),
|
(style.SQL_KEYWORD('ALTER TABLE'),
|
||||||
style.SQL_TABLE(qn(table)),
|
style.SQL_TABLE(qn(table)),
|
||||||
|
@ -413,29 +413,37 @@ class Model(object):
|
|||||||
|
|
||||||
save.alters_data = True
|
save.alters_data = True
|
||||||
|
|
||||||
def save_base(self, raw=False, cls=None, force_insert=False,
|
def save_base(self, raw=False, cls=None, origin=None,
|
||||||
force_update=False):
|
force_insert=False, force_update=False):
|
||||||
"""
|
"""
|
||||||
Does the heavy-lifting involved in saving. Subclasses shouldn't need to
|
Does the heavy-lifting involved in saving. Subclasses shouldn't need to
|
||||||
override this method. It's separate from save() in order to hide the
|
override this method. It's separate from save() in order to hide the
|
||||||
need for overrides of save() to pass around internal-only parameters
|
need for overrides of save() to pass around internal-only parameters
|
||||||
('raw' and 'cls').
|
('raw', 'cls', and 'origin').
|
||||||
"""
|
"""
|
||||||
assert not (force_insert and force_update)
|
assert not (force_insert and force_update)
|
||||||
if not cls:
|
if cls is None:
|
||||||
cls = self.__class__
|
cls = self.__class__
|
||||||
meta = self._meta
|
meta = cls._meta
|
||||||
signal = True
|
if not meta.proxy:
|
||||||
signals.pre_save.send(sender=self.__class__, instance=self, raw=raw)
|
origin = cls
|
||||||
else:
|
else:
|
||||||
meta = cls._meta
|
meta = cls._meta
|
||||||
signal = False
|
|
||||||
|
if origin:
|
||||||
|
signals.pre_save.send(sender=origin, instance=self, raw=raw)
|
||||||
|
|
||||||
# If we are in a raw save, save the object exactly as presented.
|
# If we are in a raw save, save the object exactly as presented.
|
||||||
# That means that we don't try to be smart about saving attributes
|
# That means that we don't try to be smart about saving attributes
|
||||||
# that might have come from the parent class - we just save the
|
# that might have come from the parent class - we just save the
|
||||||
# attributes we have been given to the class we have been given.
|
# attributes we have been given to the class we have been given.
|
||||||
if not raw:
|
# We also go through this process to defer the save of proxy objects
|
||||||
|
# to their actual underlying model.
|
||||||
|
if not raw or meta.proxy:
|
||||||
|
if meta.proxy:
|
||||||
|
org = cls
|
||||||
|
else:
|
||||||
|
org = None
|
||||||
for parent, field in meta.parents.items():
|
for parent, field in meta.parents.items():
|
||||||
# At this point, parent's primary key field may be unknown
|
# At this point, parent's primary key field may be unknown
|
||||||
# (for example, from administration form which doesn't fill
|
# (for example, from administration form which doesn't fill
|
||||||
@ -443,7 +451,8 @@ class Model(object):
|
|||||||
if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
|
if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None:
|
||||||
setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
|
setattr(self, parent._meta.pk.attname, getattr(self, field.attname))
|
||||||
|
|
||||||
self.save_base(cls=parent)
|
self.save_base(cls=parent, origin=org)
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
setattr(self, field.attname, self._get_pk_val(parent._meta))
|
setattr(self, field.attname, self._get_pk_val(parent._meta))
|
||||||
if meta.proxy:
|
if meta.proxy:
|
||||||
@ -494,8 +503,8 @@ class Model(object):
|
|||||||
setattr(self, meta.pk.attname, result)
|
setattr(self, meta.pk.attname, result)
|
||||||
transaction.commit_unless_managed()
|
transaction.commit_unless_managed()
|
||||||
|
|
||||||
if signal:
|
if origin:
|
||||||
signals.post_save.send(sender=self.__class__, instance=self,
|
signals.post_save.send(sender=origin, instance=self,
|
||||||
created=(not record_exists), raw=raw)
|
created=(not record_exists), raw=raw)
|
||||||
|
|
||||||
save_base.alters_data = True
|
save_base.alters_data = True
|
||||||
|
@ -112,9 +112,9 @@ class RelatedField(object):
|
|||||||
|
|
||||||
def do_related_class(self, other, cls):
|
def do_related_class(self, other, cls):
|
||||||
self.set_attributes_from_rel()
|
self.set_attributes_from_rel()
|
||||||
related = RelatedObject(other, cls, self)
|
self.related = RelatedObject(other, cls, self)
|
||||||
if not cls._meta.abstract:
|
if not cls._meta.abstract:
|
||||||
self.contribute_to_related_class(other, related)
|
self.contribute_to_related_class(other, self.related)
|
||||||
|
|
||||||
def get_db_prep_lookup(self, lookup_type, value):
|
def get_db_prep_lookup(self, lookup_type, value):
|
||||||
# If we are doing a lookup on a Related Field, we must be
|
# If we are doing a lookup on a Related Field, we must be
|
||||||
@ -132,6 +132,7 @@ class RelatedField(object):
|
|||||||
v, field = getattr(v, v._meta.pk.name), v._meta.pk
|
v, field = getattr(v, v._meta.pk.name), v._meta.pk
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if field:
|
if field:
|
||||||
if lookup_type in ('range', 'in'):
|
if lookup_type in ('range', 'in'):
|
||||||
v = [v]
|
v = [v]
|
||||||
@ -183,7 +184,6 @@ class SingleRelatedObjectDescriptor(object):
|
|||||||
def __get__(self, instance, instance_type=None):
|
def __get__(self, instance, instance_type=None):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return getattr(instance, self.cache_name)
|
return getattr(instance, self.cache_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -231,6 +231,7 @@ class ReverseSingleRelatedObjectDescriptor(object):
|
|||||||
def __get__(self, instance, instance_type=None):
|
def __get__(self, instance, instance_type=None):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
cache_name = self.field.get_cache_name()
|
cache_name = self.field.get_cache_name()
|
||||||
try:
|
try:
|
||||||
return getattr(instance, cache_name)
|
return getattr(instance, cache_name)
|
||||||
@ -271,6 +272,29 @@ class ReverseSingleRelatedObjectDescriptor(object):
|
|||||||
(value, instance._meta.object_name,
|
(value, instance._meta.object_name,
|
||||||
self.field.name, self.field.rel.to._meta.object_name))
|
self.field.name, self.field.rel.to._meta.object_name))
|
||||||
|
|
||||||
|
# If we're setting the value of a OneToOneField to None, we need to clear
|
||||||
|
# out the cache on any old related object. Otherwise, deleting the
|
||||||
|
# previously-related object will also cause this object to be deleted,
|
||||||
|
# which is wrong.
|
||||||
|
if value is None:
|
||||||
|
# Look up the previously-related object, which may still be available
|
||||||
|
# since we've not yet cleared out the related field.
|
||||||
|
# Use the cache directly, instead of the accessor; if we haven't
|
||||||
|
# populated the cache, then we don't care - we're only accessing
|
||||||
|
# the object to invalidate the accessor cache, so there's no
|
||||||
|
# need to populate the cache just to expire it again.
|
||||||
|
related = getattr(instance, self.field.get_cache_name(), None)
|
||||||
|
|
||||||
|
# If we've got an old related object, we need to clear out its
|
||||||
|
# cache. This cache also might not exist if the related object
|
||||||
|
# hasn't been accessed yet.
|
||||||
|
if related:
|
||||||
|
cache_name = '_%s_cache' % self.field.related.get_accessor_name()
|
||||||
|
try:
|
||||||
|
delattr(related, cache_name)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Set the value of the related field
|
# Set the value of the related field
|
||||||
try:
|
try:
|
||||||
val = getattr(value, self.field.rel.get_related_field().attname)
|
val = getattr(value, self.field.rel.get_related_field().attname)
|
||||||
@ -977,4 +1001,3 @@ class ManyToManyField(RelatedField, Field):
|
|||||||
# A ManyToManyField is not represented by a single column,
|
# A ManyToManyField is not represented by a single column,
|
||||||
# so return None.
|
# so return None.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
.. _index:
|
.. _index:
|
||||||
|
|
||||||
====================
|
====================
|
||||||
@ -99,8 +100,11 @@ The view layer
|
|||||||
:ref:`Managing files <topics-files>` |
|
:ref:`Managing files <topics-files>` |
|
||||||
:ref:`Custom storage <howto-custom-file-storage>`
|
:ref:`Custom storage <howto-custom-file-storage>`
|
||||||
|
|
||||||
|
* **Generic views:**
|
||||||
|
:ref:`Overview<topics-generic-views>` |
|
||||||
|
:ref:`Built-in generic views<ref-generic-views>`
|
||||||
|
|
||||||
* **Advanced:**
|
* **Advanced:**
|
||||||
:ref:`Generic views <ref-generic-views>` |
|
|
||||||
:ref:`Generating CSV <howto-outputting-csv>` |
|
:ref:`Generating CSV <howto-outputting-csv>` |
|
||||||
:ref:`Generating PDF <howto-outputting-pdf>`
|
:ref:`Generating PDF <howto-outputting-pdf>`
|
||||||
|
|
||||||
|
@ -130,11 +130,6 @@ TODO
|
|||||||
|
|
||||||
The work is mostly done, but here's what's left, in rough order of priority.
|
The work is mostly done, but here's what's left, in rough order of priority.
|
||||||
|
|
||||||
* Fix up generic view docs: adapt Chapter 9 of the Django Book (consider
|
|
||||||
this TODO item my permission and license) into
|
|
||||||
``topics/generic-views.txt``; remove the intro material from
|
|
||||||
``ref/generic-views.txt`` and just leave the function reference.
|
|
||||||
|
|
||||||
* Change the "Added/changed in development version" callouts to proper
|
* Change the "Added/changed in development version" callouts to proper
|
||||||
Sphinx ``.. versionadded::`` or ``.. versionchanged::`` directives.
|
Sphinx ``.. versionadded::`` or ``.. versionchanged::`` directives.
|
||||||
|
|
||||||
|
@ -82,6 +82,9 @@ Time for an example. Edit ``mysite/urls.py`` so it looks like this::
|
|||||||
|
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
admin.autodiscover()
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^polls/$', 'mysite.polls.views.index'),
|
(r'^polls/$', 'mysite.polls.views.index'),
|
||||||
(r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
|
(r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
|
||||||
|
@ -20,7 +20,7 @@ tutorial, so that the template contains an HTML ``<form>`` element:
|
|||||||
|
|
||||||
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
||||||
|
|
||||||
<form action="vote/" method="post">
|
<form action="/polls/{{ poll.id }}/vote/" method="post">
|
||||||
{% for choice in poll.choice_set.all %}
|
{% for choice in poll.choice_set.all %}
|
||||||
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
|
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
|
||||||
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
|
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
|
||||||
@ -36,12 +36,12 @@ A quick rundown:
|
|||||||
selects one of the radio buttons and submits the form, it'll send the
|
selects one of the radio buttons and submits the form, it'll send the
|
||||||
POST data ``choice=3``. This is HTML Forms 101.
|
POST data ``choice=3``. This is HTML Forms 101.
|
||||||
|
|
||||||
* We set the form's ``action`` to ``vote/``, and we set ``method="post"``.
|
* We set the form's ``action`` to ``/polls/{{ poll.id }}/vote/``, and we
|
||||||
Using ``method="post"`` (as opposed to ``method="get"``) is very
|
set ``method="post"``. Using ``method="post"`` (as opposed to
|
||||||
important, because the act of submitting this form will alter data
|
``method="get"``) is very important, because the act of submitting this
|
||||||
server-side. Whenever you create a form that alters data server-side, use
|
form will alter data server-side. Whenever you create a form that alters
|
||||||
``method="post"``. This tip isn't specific to Django; it's just good Web
|
data server-side, use ``method="post"``. This tip isn't specific to
|
||||||
development practice.
|
Django; it's just good Web development practice.
|
||||||
|
|
||||||
* ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone
|
* ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone
|
||||||
through its loop
|
through its loop
|
||||||
|
@ -43,8 +43,8 @@ modify the filename as necessary to get a unique name. The actual name of the
|
|||||||
stored file will be returned.
|
stored file will be returned.
|
||||||
|
|
||||||
The ``content`` argument must be an instance of
|
The ``content`` argument must be an instance of
|
||||||
:class:`django.db.files.File` or of a subclass of
|
:class:`django.core.files.File` or of a subclass of
|
||||||
:class:`~django.db.files.File`.
|
:class:`~django.core.files.File`.
|
||||||
|
|
||||||
``Storage.delete(name)``
|
``Storage.delete(name)``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -9,67 +9,18 @@ again and again. In Django, the most common of these patterns have been
|
|||||||
abstracted into "generic views" that let you quickly provide common views of
|
abstracted into "generic views" that let you quickly provide common views of
|
||||||
an object without actually needing to write any Python code.
|
an object without actually needing to write any Python code.
|
||||||
|
|
||||||
Django's generic views contain the following:
|
A general introduction to generic views can be found in the :ref:`topic guide
|
||||||
|
<topics-generic-views>`.
|
||||||
|
|
||||||
* A set of views for doing list/detail interfaces.
|
This reference contains details of Django's built-in generic views, along with
|
||||||
|
a list of all keyword arguments that a generic view expects. Remember that
|
||||||
* A set of views for year/month/day archive pages and associated
|
arguments may either come from the URL pattern or from the ``extra_context``
|
||||||
detail and "latest" pages (for example, the Django weblog's year_,
|
additional-information dictionary.
|
||||||
month_, day_, detail_, and latest_ pages).
|
|
||||||
|
|
||||||
* A set of views for creating, editing, and deleting objects.
|
|
||||||
|
|
||||||
.. _year: http://www.djangoproject.com/weblog/2005/
|
|
||||||
.. _month: http://www.djangoproject.com/weblog/2005/jul/
|
|
||||||
.. _day: http://www.djangoproject.com/weblog/2005/jul/20/
|
|
||||||
.. _detail: http://www.djangoproject.com/weblog/2005/jul/20/autoreload/
|
|
||||||
.. _latest: http://www.djangoproject.com/weblog/
|
|
||||||
|
|
||||||
All of these views are used by creating configuration dictionaries in
|
|
||||||
your URLconf files and passing those dictionaries as the third member of the
|
|
||||||
URLconf tuple for a given pattern. For example, here's the URLconf for the
|
|
||||||
simple weblog app that drives the blog on djangoproject.com::
|
|
||||||
|
|
||||||
from django.conf.urls.defaults import *
|
|
||||||
from django_website.apps.blog.models import Entry
|
|
||||||
|
|
||||||
info_dict = {
|
|
||||||
'queryset': Entry.objects.all(),
|
|
||||||
'date_field': 'pub_date',
|
|
||||||
}
|
|
||||||
|
|
||||||
urlpatterns = patterns('django.views.generic.date_based',
|
|
||||||
(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[-\w]+)/$', 'object_detail', info_dict),
|
|
||||||
(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$', 'archive_day', info_dict),
|
|
||||||
(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/$', 'archive_month', info_dict),
|
|
||||||
(r'^(?P<year>\d{4})/$', 'archive_year', info_dict),
|
|
||||||
(r'^$', 'archive_index', info_dict),
|
|
||||||
)
|
|
||||||
|
|
||||||
As you can see, this URLconf defines a few options in ``info_dict``.
|
|
||||||
``'queryset'`` gives the generic view a ``QuerySet`` of objects to use (in this
|
|
||||||
case, all of the ``Entry`` objects) and tells the generic view which model is
|
|
||||||
being used.
|
|
||||||
|
|
||||||
Documentation of each generic view follows, along with a list of all keyword
|
|
||||||
arguments that a generic view expects. Remember that as in the example above,
|
|
||||||
arguments may either come from the URL pattern (as ``month``, ``day``,
|
|
||||||
``year``, etc. do above) or from the additional-information dictionary (as for
|
|
||||||
``queryset``, ``date_field``, etc.).
|
|
||||||
|
|
||||||
Most generic views require the ``queryset`` key, which is a ``QuerySet``
|
Most generic views require the ``queryset`` key, which is a ``QuerySet``
|
||||||
instance; see :ref:`topics-db-queries` for more information about ``QuerySet``
|
instance; see :ref:`topics-db-queries` for more information about ``QuerySet``
|
||||||
objects.
|
objects.
|
||||||
|
|
||||||
Most views also take an optional ``extra_context`` dictionary that you can use
|
|
||||||
to pass any auxiliary information you wish to the view. The values in the
|
|
||||||
``extra_context`` dictionary can be either functions (or other callables) or
|
|
||||||
other objects. Functions are evaluated just before they are passed to the
|
|
||||||
template. However, note that QuerySets retrieve and cache their data when they
|
|
||||||
are first evaluated, so if you want to pass in a QuerySet via
|
|
||||||
``extra_context`` that is always fresh you need to wrap it in a function or
|
|
||||||
lambda that returns the QuerySet.
|
|
||||||
|
|
||||||
"Simple" generic views
|
"Simple" generic views
|
||||||
======================
|
======================
|
||||||
|
|
||||||
@ -1137,3 +1088,4 @@ In addition to ``extra_context``, the template's context will be:
|
|||||||
variable's name depends on the ``template_object_name`` parameter, which
|
variable's name depends on the ``template_object_name`` parameter, which
|
||||||
is ``'object'`` by default. If ``template_object_name`` is ``'foo'``,
|
is ``'object'`` by default. If ``template_object_name`` is ``'foo'``,
|
||||||
this variable's name will be ``foo``.
|
this variable's name will be ``foo``.
|
||||||
|
|
||||||
|
@ -20,3 +20,4 @@ API Reference
|
|||||||
signals
|
signals
|
||||||
templates/index
|
templates/index
|
||||||
unicode
|
unicode
|
||||||
|
|
||||||
|
@ -800,21 +800,22 @@ you can use the name of the model, rather than the model object itself::
|
|||||||
class Manufacturer(models.Model):
|
class Manufacturer(models.Model):
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
Note, however, that this only refers to models in the same ``models.py`` file --
|
.. versionadded:: 1.0
|
||||||
you cannot use a string to reference a model defined in another application or
|
|
||||||
imported from elsewhere.
|
|
||||||
|
|
||||||
.. versionchanged:: 1.0
|
To refer to models defined in another application, you can explicitly specify
|
||||||
Refering models in other applications must include the application label.
|
a model with the full application label. For example, if the ``Manufacturer``
|
||||||
|
model above is defined in another application called ``production``, you'd
|
||||||
To refer to models defined in another
|
need to use::
|
||||||
application, you must instead explicitly specify the application label. For
|
|
||||||
example, if the ``Manufacturer`` model above is defined in another application
|
|
||||||
called ``production``, you'd need to use::
|
|
||||||
|
|
||||||
class Car(models.Model):
|
class Car(models.Model):
|
||||||
manufacturer = models.ForeignKey('production.Manufacturer')
|
manufacturer = models.ForeignKey('production.Manufacturer')
|
||||||
|
|
||||||
|
This sort of reference can be useful when resolving circular import
|
||||||
|
dependencies between two applications.
|
||||||
|
|
||||||
|
Database Representation
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Behind the scenes, Django appends ``"_id"`` to the field name to create its
|
Behind the scenes, Django appends ``"_id"`` to the field name to create its
|
||||||
database column name. In the above example, the database table for the ``Car``
|
database column name. In the above example, the database table for the ``Car``
|
||||||
model will have a ``manufacturer_id`` column. (You can change this explicitly by
|
model will have a ``manufacturer_id`` column. (You can change this explicitly by
|
||||||
@ -824,6 +825,9 @@ deal with the field names of your model object.
|
|||||||
|
|
||||||
.. _foreign-key-arguments:
|
.. _foreign-key-arguments:
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
:class:`ForeignKey` accepts an extra set of arguments -- all optional -- that
|
:class:`ForeignKey` accepts an extra set of arguments -- all optional -- that
|
||||||
define the details of how the relation works.
|
define the details of how the relation works.
|
||||||
|
|
||||||
@ -871,6 +875,9 @@ the model is related. This works exactly the same as it does for
|
|||||||
:class:`ForeignKey`, including all the options regarding :ref:`recursive
|
:class:`ForeignKey`, including all the options regarding :ref:`recursive
|
||||||
<recursive-relationships>` and :ref:`lazy <lazy-relationships>` relationships.
|
<recursive-relationships>` and :ref:`lazy <lazy-relationships>` relationships.
|
||||||
|
|
||||||
|
Database Representation
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Behind the scenes, Django creates an intermediary join table to represent the
|
Behind the scenes, Django creates an intermediary join table to represent the
|
||||||
many-to-many relationship. By default, this table name is generated using the
|
many-to-many relationship. By default, this table name is generated using the
|
||||||
names of the two tables being joined. Since some databases don't support table
|
names of the two tables being joined. Since some databases don't support table
|
||||||
@ -882,6 +889,9 @@ You can manually provide the name of the join table using the
|
|||||||
|
|
||||||
.. _manytomany-arguments:
|
.. _manytomany-arguments:
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
:class:`ManyToManyField` accepts an extra set of arguments -- all optional --
|
:class:`ManyToManyField` accepts an extra set of arguments -- all optional --
|
||||||
that control how the relationship functions.
|
that control how the relationship functions.
|
||||||
|
|
||||||
|
@ -616,6 +616,8 @@ call, since they are conflicting options.
|
|||||||
Both the ``depth`` argument and the ability to specify field names in the call
|
Both the ``depth`` argument and the ability to specify field names in the call
|
||||||
to ``select_related()`` are new in Django version 1.0.
|
to ``select_related()`` are new in Django version 1.0.
|
||||||
|
|
||||||
|
.. _extra:
|
||||||
|
|
||||||
``extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)``
|
``extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -29,6 +29,45 @@ is required. For example::
|
|||||||
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
.. _transactions-and-raw-sql:
|
||||||
|
|
||||||
|
Transactions and raw SQL
|
||||||
|
------------------------
|
||||||
|
If you are using transaction decorators (such as ``commit_on_success``) to
|
||||||
|
wrap your views and provide transaction control, you don't have to make a
|
||||||
|
manual call to ``transaction.commit_unless_managed()`` -- you can manually
|
||||||
|
commit if you want to, but you aren't required to, since the decorator will
|
||||||
|
commit for you. However, if you don't manually commit your changes, you will
|
||||||
|
need to manually mark the transaction as dirty, using
|
||||||
|
``transaction.set_dirty()``::
|
||||||
|
|
||||||
|
@commit_on_success
|
||||||
|
def my_custom_sql_view(request, value):
|
||||||
|
from django.db import connection, transaction
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
# Data modifying operation
|
||||||
|
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [value])
|
||||||
|
|
||||||
|
# Since we modified data, mark the transaction as dirty
|
||||||
|
transaction.set_dirty()
|
||||||
|
|
||||||
|
# Data retrieval operation. This doesn't dirty the transaction,
|
||||||
|
# so no call to set_dirty() is required.
|
||||||
|
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [value])
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
return render_to_response('template.html', {'row': row})
|
||||||
|
|
||||||
|
The call to ``set_dirty()`` is made automatically when you use the Django ORM
|
||||||
|
to make data modifying database calls. However, when you use raw SQL, Django
|
||||||
|
has no way of knowing if your SQL modifies data or not. The manual call to
|
||||||
|
``set_dirty()`` ensures that Django knows that there are modifications that
|
||||||
|
must be committed.
|
||||||
|
|
||||||
|
Connections and cursors
|
||||||
|
-----------------------
|
||||||
|
|
||||||
``connection`` and ``cursor`` mostly implement the standard `Python DB-API`_
|
``connection`` and ``cursor`` mostly implement the standard `Python DB-API`_
|
||||||
(except when it comes to :ref:`transaction handling <topics-db-transactions>`).
|
(except when it comes to :ref:`transaction handling <topics-db-transactions>`).
|
||||||
If you're not familiar with the Python DB-API, note that the SQL statement in
|
If you're not familiar with the Python DB-API, note that the SQL statement in
|
||||||
@ -39,9 +78,12 @@ necessary. (Also note that Django expects the ``"%s"`` placeholder, *not* the
|
|||||||
``"?"`` placeholder, which is used by the SQLite Python bindings. This is for
|
``"?"`` placeholder, which is used by the SQLite Python bindings. This is for
|
||||||
the sake of consistency and sanity.)
|
the sake of consistency and sanity.)
|
||||||
|
|
||||||
|
An easier option?
|
||||||
|
-----------------
|
||||||
|
|
||||||
A final note: If all you want to do is a custom ``WHERE`` clause, you can just
|
A final note: If all you want to do is a custom ``WHERE`` clause, you can just
|
||||||
use the ``where``, ``tables`` and ``params`` arguments to the standard lookup
|
use the ``where``, ``tables`` and ``params`` arguments to the
|
||||||
API.
|
:ref:`extra clause <extra>` in the standard queryset API.
|
||||||
|
|
||||||
.. _Python DB-API: http://www.python.org/peps/pep-0249.html
|
.. _Python DB-API: http://www.python.org/peps/pep-0249.html
|
||||||
|
|
||||||
|
@ -323,16 +323,19 @@ Since the Author model has only 3 fields, 'name', 'title', and
|
|||||||
to be empty, and does not provide a default value for the missing fields,
|
to be empty, and does not provide a default value for the missing fields,
|
||||||
any attempt to ``save()`` a ``ModelForm`` with missing fields will fail.
|
any attempt to ``save()`` a ``ModelForm`` with missing fields will fail.
|
||||||
To avoid this failure, you must instantiate your model with initial values
|
To avoid this failure, you must instantiate your model with initial values
|
||||||
for the missing, but required fields, or use ``save(commit=False)`` and
|
for the missing, but required fields::
|
||||||
manually set any extra required fields::
|
|
||||||
|
|
||||||
instance = Instance(required_field='value')
|
author = Author(title='Mr')
|
||||||
form = InstanceForm(request.POST, instance=instance)
|
form = PartialAuthorForm(request.POST, instance=author)
|
||||||
new_instance = form.save()
|
form.save()
|
||||||
|
|
||||||
instance = form.save(commit=False)
|
Alternatively, you can use ``save(commit=False)`` and manually set
|
||||||
instance.required_field = 'new value'
|
any extra required fields::
|
||||||
new_instance = instance.save()
|
|
||||||
|
form = PartialAuthorForm(request.POST)
|
||||||
|
author = form.save(commit=False)
|
||||||
|
author.title = 'Mr'
|
||||||
|
author.save()
|
||||||
|
|
||||||
See the `section on saving forms`_ for more details on using
|
See the `section on saving forms`_ for more details on using
|
||||||
``save(commit=False)``.
|
``save(commit=False)``.
|
||||||
|
503
docs/topics/generic-views.txt
Normal file
503
docs/topics/generic-views.txt
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
.. _topics-generic-views:
|
||||||
|
|
||||||
|
=============
|
||||||
|
Generic views
|
||||||
|
=============
|
||||||
|
|
||||||
|
Writing Web applications can be monotonous, because we repeat certain patterns
|
||||||
|
again and again. Django tries to take away some of that monotony at the model
|
||||||
|
and template layers, but Web developers also experience this boredom at the view
|
||||||
|
level.
|
||||||
|
|
||||||
|
Django's *generic views* were developed to ease that pain. They take certain
|
||||||
|
common idioms and patterns found in view development and abstract them so that
|
||||||
|
you can quickly write common views of data without having to write too much
|
||||||
|
code.
|
||||||
|
|
||||||
|
We can recognize certain common tasks, like displaying a list of objects, and
|
||||||
|
write code that displays a list of *any* object. Then the model in question can
|
||||||
|
be passed as an extra argument to the URLconf.
|
||||||
|
|
||||||
|
Django ships with generic views to do the following:
|
||||||
|
|
||||||
|
* Perform common "simple" tasks: redirect to a different page and
|
||||||
|
render a given template.
|
||||||
|
|
||||||
|
* Display list and detail pages for a single object. If we were creating an
|
||||||
|
application to manage conferences then a ``talk_list`` view and a
|
||||||
|
``registered_user_list`` view would be examples of list views. A single
|
||||||
|
talk page is an example of what we call a "detail" view.
|
||||||
|
|
||||||
|
* Present date-based objects in year/month/day archive pages,
|
||||||
|
associated detail, and "latest" pages. The Django Weblog's
|
||||||
|
(http://www.djangoproject.com/weblog/) year, month, and
|
||||||
|
day archives are built with these, as would be a typical
|
||||||
|
newspaper's archives.
|
||||||
|
|
||||||
|
* Allow users to create, update, and delete objects -- with or
|
||||||
|
without authorization.
|
||||||
|
|
||||||
|
Taken together, these views provide easy interfaces to perform the most common
|
||||||
|
tasks developers encounter.
|
||||||
|
|
||||||
|
Using generic views
|
||||||
|
===================
|
||||||
|
|
||||||
|
All of these views are used by creating configuration dictionaries in
|
||||||
|
your URLconf files and passing those dictionaries as the third member of the
|
||||||
|
URLconf tuple for a given pattern.
|
||||||
|
|
||||||
|
For example, here's a simple URLconf you could use to present a static "about"
|
||||||
|
page::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from django.views.generic.simple import direct_to_template
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
('^about/$', direct_to_template, {
|
||||||
|
'template': 'about.html'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Though this might seem a bit "magical" at first glance -- look, a view with no
|
||||||
|
code! --, actually the ``direct_to_template`` view simply grabs information from
|
||||||
|
the extra-parameters dictionary and uses that information when rendering the
|
||||||
|
view.
|
||||||
|
|
||||||
|
Because this generic view -- and all the others -- is a regular view functions
|
||||||
|
like any other, we can reuse it inside our own views. As an example, let's
|
||||||
|
extend our "about" example to map URLs of the form ``/about/<whatever>/`` to
|
||||||
|
statically rendered ``about/<whatever>.html``. We'll do this by first modifying
|
||||||
|
the URLconf to point to a view function:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from django.views.generic.simple import direct_to_template
|
||||||
|
**from mysite.books.views import about_pages**
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
('^about/$', direct_to_template, {
|
||||||
|
'template': 'about.html'
|
||||||
|
}),
|
||||||
|
**('^about/(\w+)/$', about_pages),**
|
||||||
|
)
|
||||||
|
|
||||||
|
Next, we'll write the ``about_pages`` view::
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from django.template import TemplateDoesNotExist
|
||||||
|
from django.views.generic.simple import direct_to_template
|
||||||
|
|
||||||
|
def about_pages(request, page):
|
||||||
|
try:
|
||||||
|
return direct_to_template(request, template="about/%s.html" % page)
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
Here we're treating ``direct_to_template`` like any other function. Since it
|
||||||
|
returns an ``HttpResponse``, we can simply return it as-is. The only slightly
|
||||||
|
tricky business here is dealing with missing templates. We don't want a
|
||||||
|
nonexistent template to cause a server error, so we catch
|
||||||
|
``TemplateDoesNotExist`` exceptions and return 404 errors instead.
|
||||||
|
|
||||||
|
.. admonition:: Is there a security vulnerability here?
|
||||||
|
|
||||||
|
Sharp-eyed readers may have noticed a possible security hole: we're
|
||||||
|
constructing the template name using interpolated content from the browser
|
||||||
|
(``template="about/%s.html" % page``). At first glance, this looks like a
|
||||||
|
classic *directory traversal* vulnerability. But is it really?
|
||||||
|
|
||||||
|
Not exactly. Yes, a maliciously crafted value of ``page`` could cause
|
||||||
|
directory traversal, but although ``page`` *is* taken from the request URL,
|
||||||
|
not every value will be accepted. The key is in the URLconf: we're using
|
||||||
|
the regular expression ``\w+`` to match the ``page`` part of the URL, and
|
||||||
|
``\w`` only accepts letters and numbers. Thus, any malicious characters
|
||||||
|
(dots and slashes, here) will be rejected by the URL resolver before they
|
||||||
|
reach the view itself.
|
||||||
|
|
||||||
|
Generic views of objects
|
||||||
|
========================
|
||||||
|
|
||||||
|
The ``direct_to_template`` certainly is useful, but Django's generic views
|
||||||
|
really shine when it comes to presenting views on your database content. Because
|
||||||
|
it's such a common task, Django comes with a handful of built-in generic views
|
||||||
|
that make generating list and detail views of objects incredibly easy.
|
||||||
|
|
||||||
|
Let's take a look at one of these generic views: the "object list" view. We'll
|
||||||
|
be using these models::
|
||||||
|
|
||||||
|
# models.py
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Publisher(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
address = models.CharField(max_length=50)
|
||||||
|
city = models.CharField(max_length=60)
|
||||||
|
state_province = models.CharField(max_length=30)
|
||||||
|
country = models.CharField(max_length=50)
|
||||||
|
website = models.URLField()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-name"]
|
||||||
|
|
||||||
|
class Book(models.Model):
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
authors = models.ManyToManyField('Author')
|
||||||
|
publisher = models.ForeignKey(Publisher)
|
||||||
|
publication_date = models.DateField()
|
||||||
|
|
||||||
|
To build a list page of all books, we'd use a URLconf along these lines::
|
||||||
|
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from django.views.generic import list_detail
|
||||||
|
from mysite.books.models import Publisher
|
||||||
|
|
||||||
|
publisher_info = {
|
||||||
|
"queryset" : Publisher.objects.all(),
|
||||||
|
}
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^publishers/$', list_detail.object_list, publisher_info)
|
||||||
|
)
|
||||||
|
|
||||||
|
That's all the Python code we need to write. We still need to write a template,
|
||||||
|
however. We could explicitly tell the ``object_list`` view which template to use
|
||||||
|
by including a ``template_name`` key in the extra arguments dictionary, but in
|
||||||
|
the absence of an explicit template Django will infer one from the object's
|
||||||
|
name. In this case, the inferred template will be
|
||||||
|
``"books/publisher_list.html"`` -- the "books" part comes from the name of the
|
||||||
|
app that defines the model, while the "publisher" bit is just the lowercased
|
||||||
|
version of the model's name.
|
||||||
|
|
||||||
|
.. highlightlang:: html+django
|
||||||
|
|
||||||
|
This template will be rendered against a context containing a variable called
|
||||||
|
``object_list`` that contains all the book objects. A very simple template
|
||||||
|
might look like the following::
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Publishers</h2>
|
||||||
|
<ul>
|
||||||
|
{% for publisher in object_list %}
|
||||||
|
<li>{{ publisher.name }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
That's really all there is to it. All the cool features of generic views come
|
||||||
|
from changing the "info" dictionary passed to the generic view. The
|
||||||
|
:ref:`generic views reference<ref-generic-views>` documents all the generic
|
||||||
|
views and all their options in detail; the rest of this document will consider
|
||||||
|
some of the common ways you might customize and extend generic views.
|
||||||
|
|
||||||
|
Extending generic views
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. highlightlang:: python
|
||||||
|
|
||||||
|
There's no question that using generic views can speed up development
|
||||||
|
substantially. In most projects, however, there comes a moment when the
|
||||||
|
generic views no longer suffice. Indeed, the most common question asked by new
|
||||||
|
Django developers is how to make generic views handle a wider array of
|
||||||
|
situations.
|
||||||
|
|
||||||
|
Luckily, in nearly every one of these cases, there are ways to simply extend
|
||||||
|
generic views to handle a larger array of use cases. These situations usually
|
||||||
|
fall into a handful of patterns dealt with in the sections that follow.
|
||||||
|
|
||||||
|
Making "friendly" template contexts
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
You might have noticed that our sample publisher list template stores all the
|
||||||
|
books in a variable named ``object_list``. While this works just fine, it isn't
|
||||||
|
all that "friendly" to template authors: they have to "just know" that they're
|
||||||
|
dealing with books here. A better name for that variable would be
|
||||||
|
``publisher_list``; that variable's content is pretty obvious.
|
||||||
|
|
||||||
|
We can change the name of that variable easily with the ``template_object_name``
|
||||||
|
argument:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
publisher_info = {
|
||||||
|
"queryset" : Publisher.objects.all(),
|
||||||
|
**"template_object_name" : "publisher",**
|
||||||
|
}
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^publishers/$', list_detail.object_list, publisher_info)
|
||||||
|
)
|
||||||
|
|
||||||
|
Providing a useful ``template_object_name`` is always a good idea. Your
|
||||||
|
coworkers who design templates will thank you.
|
||||||
|
|
||||||
|
Adding extra context
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Often you simply need to present some extra information beyond that provided by
|
||||||
|
the generic view. For example, think of showing a list of all the other
|
||||||
|
publishers on each publisher detail page. The ``object_detail`` generic view
|
||||||
|
provides the publisher to the context, but it seems there's no way to get a list
|
||||||
|
of *all* publishers in that template.
|
||||||
|
|
||||||
|
But there is: all generic views take an extra optional parameter,
|
||||||
|
``extra_context``. This is a dictionary of extra objects that will be added to
|
||||||
|
the template's context. So, to provide the list of all publishers on the detail
|
||||||
|
detail view, we'd use an info dict like this:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
from mysite.books.models import Publisher, **Book**
|
||||||
|
|
||||||
|
publisher_info = {
|
||||||
|
"queryset" : Publisher.objects.all(),
|
||||||
|
"template_object_name" : "publisher",
|
||||||
|
**"extra_context" : {"book_list" : Book.objects.all()}**
|
||||||
|
}
|
||||||
|
|
||||||
|
This would populate a ``{{ book_list }}`` variable in the template context.
|
||||||
|
This pattern can be used to pass any information down into the template for the
|
||||||
|
generic view. It's very handy.
|
||||||
|
|
||||||
|
However, there's actually a subtle bug here -- can you spot it?
|
||||||
|
|
||||||
|
The problem has to do with when the queries in ``extra_context`` are evaluated.
|
||||||
|
Because this example puts ``Publisher.objects.all()`` in the URLconf, it will
|
||||||
|
be evaluated only once (when the URLconf is first loaded). Once you add or
|
||||||
|
remove publishers, you'll notice that the generic view doesn't reflect those
|
||||||
|
changes until you reload the Web server (see :ref:`caching-and-querysets`
|
||||||
|
for more information about when QuerySets are cached and evaluated).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This problem doesn't apply to the ``queryset`` generic view argument. Since
|
||||||
|
Django knows that particular QuerySet should *never* be cached, the generic
|
||||||
|
view takes care of clearing the cache when each view is rendered.
|
||||||
|
|
||||||
|
The solution is to use a callback in ``extra_context`` instead of a value. Any
|
||||||
|
callable (i.e., a function) that's passed to ``extra_context`` will be evaluated
|
||||||
|
when the view is rendered (instead of only once). You could do this with an
|
||||||
|
explicitly defined function:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
def get_books():
|
||||||
|
return Book.objects.all()
|
||||||
|
|
||||||
|
publisher_info = {
|
||||||
|
"queryset" : Publisher.objects.all(),
|
||||||
|
"template_object_name" : "publisher",
|
||||||
|
"extra_context" : **{"book_list" : get_books}**
|
||||||
|
}
|
||||||
|
|
||||||
|
or you could use a less obvious but shorter version that relies on the fact that
|
||||||
|
``Book.objects.all`` is itself a callable:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
publisher_info = {
|
||||||
|
"queryset" : Publisher.objects.all(),
|
||||||
|
"template_object_name" : "publisher",
|
||||||
|
"extra_context" : **{"book_list" : Book.objects.all}**
|
||||||
|
}
|
||||||
|
|
||||||
|
Notice the lack of parentheses after ``Book.objects.all``; this references
|
||||||
|
the function without actually calling it (which the generic view will do later).
|
||||||
|
|
||||||
|
Viewing subsets of objects
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Now let's take a closer look at this ``queryset`` key we've been using all
|
||||||
|
along. Most generic views take one of these ``queryset`` arguments -- it's how
|
||||||
|
the view knows which set of objects to display (see :ref:`topics-db-queries` for
|
||||||
|
more information about ``QuerySet`` objects, and see the
|
||||||
|
:ref:`generic views reference<ref-generic-views>` for the complete details).
|
||||||
|
|
||||||
|
To pick a simple example, we might want to order a list of books by
|
||||||
|
publication date, with the most recent first:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
book_info = {
|
||||||
|
"queryset" : Book.objects.all().order_by("-publication_date"),
|
||||||
|
}
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^publishers/$', list_detail.object_list, publisher_info),
|
||||||
|
**(r'^books/$', list_detail.object_list, book_info),**
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
That's a pretty simple example, but it illustrates the idea nicely. Of course,
|
||||||
|
you'll usually want to do more than just reorder objects. If you want to
|
||||||
|
present a list of books by a particular publisher, you can use the same
|
||||||
|
technique:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
**acme_books = {**
|
||||||
|
**"queryset": Book.objects.filter(publisher__name="Acme Publishing"),**
|
||||||
|
**"template_name" : "books/acme_list.html"**
|
||||||
|
**}**
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^publishers/$', list_detail.object_list, publisher_info),
|
||||||
|
**(r'^books/acme/$', list_detail.object_list, acme_books),**
|
||||||
|
)
|
||||||
|
|
||||||
|
Notice that along with a filtered ``queryset``, we're also using a custom
|
||||||
|
template name. If we didn't, the generic view would use the same template as the
|
||||||
|
"vanilla" object list, which might not be what we want.
|
||||||
|
|
||||||
|
Also notice that this isn't a very elegant way of doing publisher-specific
|
||||||
|
books. If we want to add another publisher page, we'd need another handful of
|
||||||
|
lines in the URLconf, and more than a few publishers would get unreasonable.
|
||||||
|
We'll deal with this problem in the next section.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you get a 404 when requesting ``/books/acme/``, check to ensure you
|
||||||
|
actually have a Publisher with the name 'ACME Publishing'. Generic
|
||||||
|
views have an ``allow_empty`` parameter for this case. See the
|
||||||
|
:ref:`generic views reference<ref-generic-views>` for more details.
|
||||||
|
|
||||||
|
Complex filtering with wrapper functions
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
Another common need is to filter down the objects given in a list page by some
|
||||||
|
key in the URL. Earlier we hard-coded the publisher's name in the URLconf, but
|
||||||
|
what if we wanted to write a view that displayed all the books by some arbitrary
|
||||||
|
publisher? We can "wrap" the ``object_list`` generic view to avoid writing a lot
|
||||||
|
of code by hand. As usual, we'll start by writing a URLconf:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
from mysite.books.views import books_by_publisher
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^publishers/$', list_detail.object_list, publisher_info),
|
||||||
|
**(r'^books/(\w+)/$', books_by_publisher),**
|
||||||
|
)
|
||||||
|
|
||||||
|
Next, we'll write the ``books_by_publisher`` view itself::
|
||||||
|
|
||||||
|
from django.http import Http404
|
||||||
|
from django.views.generic import list_detail
|
||||||
|
from mysite.books.models import Book, Publisher
|
||||||
|
|
||||||
|
def books_by_publisher(request, name):
|
||||||
|
|
||||||
|
# Look up the publisher (and raise a 404 if it can't be found).
|
||||||
|
try:
|
||||||
|
publisher = Publisher.objects.get(name__iexact=name)
|
||||||
|
except Publisher.DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
# Use the object_list view for the heavy lifting.
|
||||||
|
return list_detail.object_list(
|
||||||
|
request,
|
||||||
|
queryset = Book.objects.filter(publisher=publisher),
|
||||||
|
template_name = "books/books_by_publisher.html",
|
||||||
|
template_object_name = "books",
|
||||||
|
extra_context = {"publisher" : publisher}
|
||||||
|
)
|
||||||
|
|
||||||
|
This works because there's really nothing special about generic views -- they're
|
||||||
|
just Python functions. Like any view function, generic views expect a certain
|
||||||
|
set of arguments and return ``HttpResponse`` objects. Thus, it's incredibly easy
|
||||||
|
to wrap a small function around a generic view that does additional work before
|
||||||
|
(or after; see the next section) handing things off to the generic view.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Notice that in the preceding example we passed the current publisher being
|
||||||
|
displayed in the ``extra_context``. This is usually a good idea in wrappers
|
||||||
|
of this nature; it lets the template know which "parent" object is currently
|
||||||
|
being browsed.
|
||||||
|
|
||||||
|
Performing extra work
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The last common pattern we'll look at involves doing some extra work before
|
||||||
|
or after calling the generic view.
|
||||||
|
|
||||||
|
Imagine we had a ``last_accessed`` field on our ``Author`` object that we were
|
||||||
|
using to keep track of the last time anybody looked at that author::
|
||||||
|
|
||||||
|
# models.py
|
||||||
|
|
||||||
|
class Author(models.Model):
|
||||||
|
salutation = models.CharField(max_length=10)
|
||||||
|
first_name = models.CharField(max_length=30)
|
||||||
|
last_name = models.CharField(max_length=40)
|
||||||
|
email = models.EmailField()
|
||||||
|
headshot = models.ImageField(upload_to='/tmp')
|
||||||
|
last_accessed = models.DateTimeField()
|
||||||
|
|
||||||
|
The generic ``object_detail`` view, of course, wouldn't know anything about this
|
||||||
|
field, but once again we could easily write a custom view to keep that field
|
||||||
|
updated.
|
||||||
|
|
||||||
|
First, we'd need to add an author detail bit in the URLconf to point to a
|
||||||
|
custom view:
|
||||||
|
|
||||||
|
.. parsed-literal::
|
||||||
|
|
||||||
|
from mysite.books.views import author_detail
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
#...
|
||||||
|
**(r'^authors/(?P<author_id>\d+)/$', author_detail),**
|
||||||
|
)
|
||||||
|
|
||||||
|
Then we'd write our wrapper function::
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from mysite.books.models import Author
|
||||||
|
from django.views.generic import list_detail
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
def author_detail(request, author_id):
|
||||||
|
# Look up the Author (and raise a 404 if she's not found)
|
||||||
|
author = get_object_or_404(Author, pk=author_id)
|
||||||
|
|
||||||
|
# Record the last accessed date
|
||||||
|
author.last_accessed = datetime.datetime.now()
|
||||||
|
author.save()
|
||||||
|
|
||||||
|
# Show the detail page
|
||||||
|
return list_detail.object_detail(
|
||||||
|
request,
|
||||||
|
queryset = Author.objects.all(),
|
||||||
|
object_id = author_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This code won't actually work unless you create a
|
||||||
|
``books/author_detail.html`` template.
|
||||||
|
|
||||||
|
We can use a similar idiom to alter the response returned by the generic view.
|
||||||
|
If we wanted to provide a downloadable plain-text version of the list of
|
||||||
|
authors, we could use a view like this::
|
||||||
|
|
||||||
|
def author_list_plaintext(request):
|
||||||
|
response = list_detail.object_list(
|
||||||
|
request,
|
||||||
|
queryset = Author.objects.all(),
|
||||||
|
mimetype = "text/plain",
|
||||||
|
template_name = "books/author_list.txt"
|
||||||
|
)
|
||||||
|
response["Content-Disposition"] = "attachment; filename=authors.txt"
|
||||||
|
return response
|
||||||
|
|
||||||
|
This works because the generic views return simple ``HttpResponse`` objects
|
||||||
|
that can be treated like dictionaries to set HTTP headers. This
|
||||||
|
``Content-Disposition`` business, by the way, instructs the browser to
|
||||||
|
download and save the page instead of displaying it in the browser.
|
@ -14,6 +14,7 @@ Introductions to all the key parts of Django you'll need to know:
|
|||||||
forms/index
|
forms/index
|
||||||
forms/modelforms
|
forms/modelforms
|
||||||
templates
|
templates
|
||||||
|
generic-views
|
||||||
files
|
files
|
||||||
testing
|
testing
|
||||||
auth
|
auth
|
||||||
@ -25,3 +26,4 @@ Introductions to all the key parts of Django you'll need to know:
|
|||||||
serialization
|
serialization
|
||||||
settings
|
settings
|
||||||
signals
|
signals
|
||||||
|
|
||||||
|
54
tests/modeltests/custom_pk/fields.py
Normal file
54
tests/modeltests/custom_pk/fields.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class MyWrapper(object):
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s: %s>" % (self.__class__.__name__, self.value)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, self.__class__):
|
||||||
|
return self.value == other.value
|
||||||
|
return self.value == other
|
||||||
|
|
||||||
|
class MyAutoField(models.CharField):
|
||||||
|
__metaclass__ = models.SubfieldBase
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['max_length'] = 10
|
||||||
|
super(MyAutoField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def pre_save(self, instance, add):
|
||||||
|
value = getattr(instance, self.attname, None)
|
||||||
|
if not value:
|
||||||
|
value = MyWrapper(''.join(random.sample(string.lowercase, 10)))
|
||||||
|
setattr(instance, self.attname, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
if not isinstance(value, MyWrapper):
|
||||||
|
value = MyWrapper(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_db_prep_save(self, value):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
if isinstance(value, MyWrapper):
|
||||||
|
return unicode(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_db_prep_value(self, value):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
if isinstance(value, MyWrapper):
|
||||||
|
return unicode(value)
|
||||||
|
return value
|
@ -9,6 +9,8 @@ this behavior by explicitly adding ``primary_key=True`` to a field.
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models, transaction, IntegrityError
|
from django.db import models, transaction, IntegrityError
|
||||||
|
|
||||||
|
from fields import MyAutoField
|
||||||
|
|
||||||
class Employee(models.Model):
|
class Employee(models.Model):
|
||||||
employee_code = models.IntegerField(primary_key=True, db_column = 'code')
|
employee_code = models.IntegerField(primary_key=True, db_column = 'code')
|
||||||
first_name = models.CharField(max_length=20)
|
first_name = models.CharField(max_length=20)
|
||||||
@ -28,6 +30,16 @@ class Business(models.Model):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
class Bar(models.Model):
|
||||||
|
id = MyAutoField(primary_key=True, db_index=True)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return repr(self.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class Foo(models.Model):
|
||||||
|
bar = models.ForeignKey(Bar)
|
||||||
|
|
||||||
__test__ = {'API_TESTS':"""
|
__test__ = {'API_TESTS':"""
|
||||||
>>> dan = Employee(employee_code=123, first_name='Dan', last_name='Jones')
|
>>> dan = Employee(employee_code=123, first_name='Dan', last_name='Jones')
|
||||||
>>> dan.save()
|
>>> dan.save()
|
||||||
@ -121,6 +133,24 @@ DoesNotExist: Employee matching query does not exist.
|
|||||||
... print "Fail with %s" % type(e)
|
... print "Fail with %s" % type(e)
|
||||||
Pass
|
Pass
|
||||||
|
|
||||||
|
# Regression for #10785 -- Custom fields can be used for primary keys.
|
||||||
|
>>> new_bar = Bar.objects.create()
|
||||||
|
>>> new_foo = Foo.objects.create(bar=new_bar)
|
||||||
|
|
||||||
|
# FIXME: This still doesn't work, but will require some changes in
|
||||||
|
# get_db_prep_lookup to fix it.
|
||||||
|
# >>> f = Foo.objects.get(bar=new_bar.pk)
|
||||||
|
# >>> f == new_foo
|
||||||
|
# True
|
||||||
|
# >>> f.bar == new_bar
|
||||||
|
# True
|
||||||
|
|
||||||
|
>>> f = Foo.objects.get(bar=new_bar)
|
||||||
|
>>> f == new_foo
|
||||||
|
True
|
||||||
|
>>> f.bar == new_bar
|
||||||
|
True
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
|
||||||
# SQLite lets objects be saved with an empty primary key, even though an
|
# SQLite lets objects be saved with an empty primary key, even though an
|
||||||
|
9
tests/modeltests/proxy_models/fixtures/mypeople.json
Normal file
9
tests/modeltests/proxy_models/fixtures/mypeople.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"pk": 100,
|
||||||
|
"model": "proxy_models.myperson",
|
||||||
|
"fields": {
|
||||||
|
"name": "Elvis Presley"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -259,6 +259,40 @@ FieldError: Proxy model 'NoNewFields' contains model fields.
|
|||||||
>>> OtherPerson._default_manager.all()
|
>>> OtherPerson._default_manager.all()
|
||||||
[<OtherPerson: barney>, <OtherPerson: wilma>]
|
[<OtherPerson: barney>, <OtherPerson: wilma>]
|
||||||
|
|
||||||
|
# Test save signals for proxy models
|
||||||
|
>>> from django.db.models import signals
|
||||||
|
>>> def make_handler(model, event):
|
||||||
|
... def _handler(*args, **kwargs):
|
||||||
|
... print u"%s %s save" % (model, event)
|
||||||
|
... return _handler
|
||||||
|
>>> h1 = make_handler('MyPerson', 'pre')
|
||||||
|
>>> h2 = make_handler('MyPerson', 'post')
|
||||||
|
>>> h3 = make_handler('Person', 'pre')
|
||||||
|
>>> h4 = make_handler('Person', 'post')
|
||||||
|
>>> signals.pre_save.connect(h1, sender=MyPerson)
|
||||||
|
>>> signals.post_save.connect(h2, sender=MyPerson)
|
||||||
|
>>> signals.pre_save.connect(h3, sender=Person)
|
||||||
|
>>> signals.post_save.connect(h4, sender=Person)
|
||||||
|
>>> dino = MyPerson.objects.create(name=u"dino")
|
||||||
|
MyPerson pre save
|
||||||
|
MyPerson post save
|
||||||
|
|
||||||
|
# Test save signals for proxy proxy models
|
||||||
|
>>> h5 = make_handler('MyPersonProxy', 'pre')
|
||||||
|
>>> h6 = make_handler('MyPersonProxy', 'post')
|
||||||
|
>>> signals.pre_save.connect(h5, sender=MyPersonProxy)
|
||||||
|
>>> signals.post_save.connect(h6, sender=MyPersonProxy)
|
||||||
|
>>> dino = MyPersonProxy.objects.create(name=u"pebbles")
|
||||||
|
MyPersonProxy pre save
|
||||||
|
MyPersonProxy post save
|
||||||
|
|
||||||
|
>>> signals.pre_save.disconnect(h1, sender=MyPerson)
|
||||||
|
>>> signals.post_save.disconnect(h2, sender=MyPerson)
|
||||||
|
>>> signals.pre_save.disconnect(h3, sender=Person)
|
||||||
|
>>> signals.post_save.disconnect(h4, sender=Person)
|
||||||
|
>>> signals.pre_save.disconnect(h5, sender=MyPersonProxy)
|
||||||
|
>>> signals.post_save.disconnect(h6, sender=MyPersonProxy)
|
||||||
|
|
||||||
# A proxy has the same content type as the model it is proxying for (at the
|
# A proxy has the same content type as the model it is proxying for (at the
|
||||||
# storage level, it is meant to be essentially indistinguishable).
|
# storage level, it is meant to be essentially indistinguishable).
|
||||||
>>> ctype = ContentType.objects.get_for_model
|
>>> ctype = ContentType.objects.get_for_model
|
||||||
@ -266,7 +300,7 @@ FieldError: Proxy model 'NoNewFields' contains model fields.
|
|||||||
True
|
True
|
||||||
|
|
||||||
>>> MyPersonProxy.objects.all()
|
>>> MyPersonProxy.objects.all()
|
||||||
[<MyPersonProxy: barney>, <MyPersonProxy: fred>]
|
[<MyPersonProxy: barney>, <MyPersonProxy: dino>, <MyPersonProxy: fred>, <MyPersonProxy: pebbles>]
|
||||||
|
|
||||||
>>> u = User.objects.create(name='Bruce')
|
>>> u = User.objects.create(name='Bruce')
|
||||||
>>> User.objects.all()
|
>>> User.objects.all()
|
||||||
@ -327,4 +361,11 @@ True
|
|||||||
# Select related + filter on a related proxy of proxy field
|
# Select related + filter on a related proxy of proxy field
|
||||||
>>> ProxyImprovement.objects.select_related().get(associated_bug__summary__icontains='fix')
|
>>> ProxyImprovement.objects.select_related().get(associated_bug__summary__icontains='fix')
|
||||||
<ProxyImprovement: ProxyImprovement:improve that>
|
<ProxyImprovement: ProxyImprovement:improve that>
|
||||||
|
|
||||||
|
Proxy models can be loaded from fixtures (Regression for #11194)
|
||||||
|
>>> from django.core import management
|
||||||
|
>>> management.call_command('loaddata', 'mypeople.json', verbosity=0)
|
||||||
|
>>> MyPerson.objects.get(pk=100)
|
||||||
|
<MyPerson: Elvis Presley>
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
@ -9,6 +9,9 @@ class Animal(models.Model):
|
|||||||
count = models.IntegerField()
|
count = models.IntegerField()
|
||||||
weight = models.FloatField()
|
weight = models.FloatField()
|
||||||
|
|
||||||
|
# use a non-default name for the default manager
|
||||||
|
specimens = models.Manager()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.common_name
|
return self.common_name
|
||||||
|
|
||||||
@ -161,4 +164,10 @@ Weight = 1.2 (<type 'float'>)
|
|||||||
|
|
||||||
>>> models.signals.pre_save.disconnect(animal_pre_save_check)
|
>>> models.signals.pre_save.disconnect(animal_pre_save_check)
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Regression for #11286 -- Ensure that dumpdata honors the default manager
|
||||||
|
# Dump the current contents of the database as a JSON fixture
|
||||||
|
>>> management.call_command('dumpdata', 'fixtures_regress.animal', format='json')
|
||||||
|
[{"pk": 1, "model": "fixtures_regress.animal", "fields": {"count": 3, "weight": 1.2, "name": "Lion", "latin_name": "Panthera leo"}}, {"pk": 2, "model": "fixtures_regress.animal", "fields": {"count": 2, "weight": 2.29..., "name": "Platypus", "latin_name": "Ornithorhynchus anatinus"}}, {"pk": 10, "model": "fixtures_regress.animal", "fields": {"count": 42, "weight": 1.2, "name": "Emu", "latin_name": "Dromaius novaehollandiae"}}]
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
@ -33,6 +33,14 @@ class SelfReferChild(SelfRefer):
|
|||||||
class SelfReferChildSibling(SelfRefer):
|
class SelfReferChildSibling(SelfRefer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Many-to-Many relation between models, where one of the PK's isn't an Autofield
|
||||||
|
class Line(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Worksheet(models.Model):
|
||||||
|
id = models.CharField(primary_key=True, max_length=100)
|
||||||
|
lines = models.ManyToManyField(Line, blank=True, null=True)
|
||||||
|
|
||||||
__test__ = {"regressions": """
|
__test__ = {"regressions": """
|
||||||
# Multiple m2m references to the same model or a different model must be
|
# Multiple m2m references to the same model or a different model must be
|
||||||
# distinguished when accessing the relations through an instance attribute.
|
# distinguished when accessing the relations through an instance attribute.
|
||||||
@ -79,5 +87,11 @@ FieldError: Cannot resolve keyword 'porcupine' into field. Choices are: id, name
|
|||||||
>>> sr_sibling.related.all()
|
>>> sr_sibling.related.all()
|
||||||
[<SelfRefer: Hanna>]
|
[<SelfRefer: Hanna>]
|
||||||
|
|
||||||
|
# Regression for #11311 - The primary key for models in a m2m relation
|
||||||
|
# doesn't have to be an AutoField
|
||||||
|
>>> w = Worksheet(id='abc')
|
||||||
|
>>> w.save()
|
||||||
|
>>> w.delete()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ r"""
|
|||||||
|
|
||||||
>>> from django.conf import settings
|
>>> from django.conf import settings
|
||||||
>>> from django.core import mail
|
>>> from django.core import mail
|
||||||
>>> from django.core.mail import EmailMessage, mail_admins, mail_managers
|
>>> from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives
|
||||||
>>> from django.utils.translation import ugettext_lazy
|
>>> from django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
# Test normal ascii character case:
|
# Test normal ascii character case:
|
||||||
@ -95,4 +95,48 @@ BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection T
|
|||||||
>>> message['From']
|
>>> message['From']
|
||||||
'from@example.com'
|
'from@example.com'
|
||||||
|
|
||||||
|
# Handle attachments within an multipart/alternative mail correctly (#9367)
|
||||||
|
# (test is not as precise/clear as it could be w.r.t. email tree structure,
|
||||||
|
# but it's good enough.)
|
||||||
|
|
||||||
|
>>> headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"}
|
||||||
|
>>> subject, from_email, to = 'hello', 'from@example.com', 'to@example.com'
|
||||||
|
>>> text_content = 'This is an important message.'
|
||||||
|
>>> html_content = '<p>This is an <strong>important</strong> message.</p>'
|
||||||
|
>>> msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers)
|
||||||
|
>>> msg.attach_alternative(html_content, "text/html")
|
||||||
|
>>> msg.attach("an attachment.pdf", "%PDF-1.4.%...", mimetype="application/pdf")
|
||||||
|
>>> print msg.message().as_string()
|
||||||
|
Content-Type: multipart/mixed; boundary="..."
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Subject: hello
|
||||||
|
From: from@example.com
|
||||||
|
To: to@example.com
|
||||||
|
Date: Fri, 09 Nov 2001 01:08:47 -0000
|
||||||
|
Message-ID: foo
|
||||||
|
...
|
||||||
|
Content-Type: multipart/alternative; boundary="..."
|
||||||
|
MIME-Version: 1.0
|
||||||
|
...
|
||||||
|
Content-Type: text/plain; charset="utf-8"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
...
|
||||||
|
This is an important message.
|
||||||
|
...
|
||||||
|
Content-Type: text/html; charset="utf-8"
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
...
|
||||||
|
<p>This is an <strong>important</strong> message.</p>
|
||||||
|
...
|
||||||
|
...
|
||||||
|
Content-Type: application/pdf
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment; filename="an attachment.pdf"
|
||||||
|
...
|
||||||
|
JVBERi0xLjQuJS4uLg==
|
||||||
|
...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
22
tests/regressiontests/one_to_one_regress/tests.py
Normal file
22
tests/regressiontests/one_to_one_regress/tests.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from regressiontests.one_to_one_regress.models import Place, UndergroundBar
|
||||||
|
|
||||||
|
class OneToOneDeletionTests(TestCase):
|
||||||
|
def test_reverse_relationship_cache_cascade(self):
|
||||||
|
"""
|
||||||
|
Regression test for #9023: accessing the reverse relationship shouldn't
|
||||||
|
result in a cascading delete().
|
||||||
|
"""
|
||||||
|
place = Place.objects.create(name="Dempsey's", address="623 Vermont St")
|
||||||
|
bar = UndergroundBar.objects.create(place=place, serves_cocktails=False)
|
||||||
|
|
||||||
|
# The bug in #9023: if you access the one-to-one relation *before*
|
||||||
|
# setting to None and deleting, the cascade happens anyway.
|
||||||
|
place.undergroundbar
|
||||||
|
bar.place.name='foo'
|
||||||
|
bar.place = None
|
||||||
|
bar.save()
|
||||||
|
place.delete()
|
||||||
|
|
||||||
|
self.assertEqual(Place.objects.all().count(), 0)
|
||||||
|
self.assertEqual(UndergroundBar.objects.all().count(), 1)
|
Loading…
x
Reference in New Issue
Block a user