diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 5564548133..4df48ff9f5 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -317,7 +317,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): def get_queryset(self): # Avoid a circular import. 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.filter(**{ self.ct_field.name: ContentType.objects.get_for_model(self.instance), diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index c1bb45c742..92e3165680 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -14,7 +14,7 @@ if lib_path: lib_names = None elif os.name == 'nt': # Windows NT shared library - lib_names = ['gdal15'] + lib_names = ['gdal16', 'gdal15'] elif os.name == 'posix': # *NIX library names. lib_names = ['gdal', 'GDAL', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0'] diff --git a/django/contrib/gis/tests/test_geoip.py b/django/contrib/gis/tests/test_geoip.py index 44b080223c..430d61b6d5 100644 --- a/django/contrib/gis/tests/test_geoip.py +++ b/django/contrib/gis/tests/test_geoip.py @@ -84,16 +84,15 @@ class GeoIPTest(unittest.TestCase): self.assertEqual('USA', d['country_code3']) self.assertEqual('Houston', d['city']) self.assertEqual('TX', d['region']) - self.assertEqual('77002', d['postal_code']) self.assertEqual(713, d['area_code']) geom = g.geos(query) 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 = (lat_lon[1], lat_lon[0]) for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon): - self.assertAlmostEqual(lon, tup[0], 9) - self.assertAlmostEqual(lat, tup[1], 9) + self.assertAlmostEqual(lon, tup[0], 4) + self.assertAlmostEqual(lat, tup[1], 4) def suite(): s = unittest.TestSuite() diff --git a/django/contrib/gis/utils/geoip.py b/django/contrib/gis/utils/geoip.py index 8c21ab290a..eedaef95dd 100644 --- a/django/contrib/gis/utils/geoip.py +++ b/django/contrib/gis/utils/geoip.py @@ -6,7 +6,7 @@ GeoIP(R) is a registered trademark of MaxMind, LLC of Boston, Massachusetts. For IP-based geolocation, this module requires the GeoLite Country and City - datasets, in binary format (CSV will not work!). The datasets may be + datasets, in binary format (CSV will not work!). The datasets may be downloaded from MaxMind at http://www.maxmind.com/download/geoip/database/. Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory corresponding to settings.GEOIP_PATH. See the GeoIP docstring and examples @@ -34,7 +34,7 @@ >>> g.lat_lon('salon.com') (37.789798736572266, -122.39420318603516) >>> g.lon_lat('uh.edu') - (-95.415199279785156, 29.77549934387207) + (-95.415199279785156, 29.77549934387207) >>> g.geos('24.124.1.80').wkt 'POINT (-95.2087020874023438 39.0392990112304688)' """ @@ -45,7 +45,7 @@ from django.conf import settings if not settings.configured: settings.configure() # Creating the settings dictionary with any settings, if needed. -GEOIP_SETTINGS = dict((key, getattr(settings, key)) +GEOIP_SETTINGS = dict((key, getattr(settings, key)) for key in ('GEOIP_PATH', 'GEOIP_LIBRARY_PATH', 'GEOIP_COUNTRY', 'GEOIP_CITY') if hasattr(settings, key)) lib_path = GEOIP_SETTINGS.get('GEOIP_LIBRARY_PATH', None) @@ -83,8 +83,17 @@ class GeoIPRecord(Structure): ('postal_code', c_char_p), ('latitude', 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), ('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 @@ -99,9 +108,12 @@ def record_output(func): rec_by_addr = record_output(lgeoip.GeoIP_record_by_addr) 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.restype = DBTYPE +geoip_close = lgeoip.GeoIP_delete +geoip_close.argtypes = [DBTYPE] +geoip_close.restype = None # String output routines. def string_output(func): @@ -136,6 +148,12 @@ class GeoIP(object): GEOIP_CHECK_CACHE = 2 GEOIP_INDEX_CACHE = 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): """ @@ -174,13 +192,19 @@ class GeoIP(object): if not isinstance(path, basestring): raise TypeError('Invalid path type: %s' % type(path).__name__) - cntry_ptr, city_ptr = (None, None) if os.path.isdir(path): - # Getting the country and city files using the settings - # dictionary. If no settings are provided, default names - # are assigned. - country = 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')) + # Constructing the GeoIP database filenames using the settings + # dictionary. If the database files for the GeoLite country + # and/or city datasets exist, then try and open them. + country_db = os.path.join(path, country or GEOIP_SETTINGS.get('GEOIP_COUNTRY', 'GeoIP.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): # Otherwise, some detective work will be needed to figure # out whether the given database path is for the GeoIP country @@ -188,29 +212,22 @@ class GeoIP(object): ptr = geoip_open(path, cache) info = geoip_dbinfo(ptr) if lite_regex.match(info): - # GeoLite City database. - city, city_ptr = path, ptr + # GeoLite City database detected. + self._city = ptr + self._city_file = path elif free_regex.match(info): - # GeoIP Country database. - country, cntry_ptr = path, ptr + # GeoIP Country database detected. + self._country = ptr + self._country_file = path else: raise GeoIPException('Unable to recognize database edition: %s' % info) else: raise GeoIPException('GeoIP path must be a valid file or directory.') - - # `_init_db` does the dirty work. - self._init_db(country, cache, '_country', cntry_ptr) - self._init_db(city, cache, '_city', city_ptr) - 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 __del__(self): + # Cleaning any GeoIP file handles lying around. + if self._country: geoip_close(self._country) + if self._city: geoip_close(self._city) def _check_query(self, query, country=False, city=False, city_or_country=False): "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__) # 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.') - elif country and self._country is None: + elif country and not self._country: 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) def city(self, query): @@ -247,7 +264,7 @@ class GeoIP(object): return dict((tup[0], getattr(record, tup[0])) for tup in record._fields_) else: return None - + def country_code(self, query): "Returns the country code for the given IP Address or FQDN." self._check_query(query, city_or_country=True) @@ -268,12 +285,12 @@ class GeoIP(object): def country(self, query): """ - Returns a dictonary with with the country code and name when given an + Returns a dictonary with with the country code and name when given an IP address or a Fully Qualified Domain Name (FQDN). For example, both '24.124.1.80' and 'djangoproject.com' are valid parameters. """ # Returning the country code and name - return {'country_code' : self.country_code(query), + return {'country_code' : self.country_code(query), 'country_name' : self.country_name(query), } @@ -318,7 +335,7 @@ class GeoIP(object): ci = geoip_dbinfo(self._city) return ci city_info = property(city_info) - + def info(self): "Returns information about all GeoIP databases in use." return 'Country:\n\t%s\nCity:\n\t%s' % (self.country_info, self.city_info) diff --git a/django/core/mail.py b/django/core/mail.py index 6a45b46587..c305699158 100644 --- a/django/core/mail.py +++ b/django/core/mail.py @@ -195,7 +195,7 @@ class EmailMessage(object): A container for email information. """ content_subtype = 'plain' - multipart_subtype = 'mixed' + mixed_subtype = 'mixed' encoding = None # None => use settings default 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 msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET), self.content_subtype, encoding) - if self.attachments: - 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 = self._create_message(msg) msg['Subject'] = self.subject msg['From'] = self.extra_headers.pop('From', self.from_email) msg['To'] = ', '.join(self.to) @@ -277,8 +268,7 @@ class EmailMessage(object): def attach(self, filename=None, content=None, mimetype=None): """ Attaches a file with the given filename and content. The filename can - be omitted (useful for multipart/alternative messages) and the mimetype - is guessed, if not provided. + be omitted and the mimetype is guessed, if not provided. If the first parameter is a MIMEBase subclass it is inserted directly into the resulting message attachments. @@ -296,15 +286,26 @@ class EmailMessage(object): content = open(path, 'rb').read() 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 - object. + Converts the content, mimetype pair into a MIME attachment object. """ - if mimetype is None: - mimetype, _ = mimetypes.guess_type(filename) - if mimetype is None: - mimetype = DEFAULT_ATTACHMENT_MIME_TYPE basetype, subtype = mimetype.split('/', 1) if basetype == 'text': attachment = SafeMIMEText(smart_str(content, @@ -314,6 +315,18 @@ class EmailMessage(object): attachment = MIMEBase(basetype, subtype) attachment.set_payload(content) 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: attachment.add_header('Content-Disposition', 'attachment', filename=filename) @@ -325,11 +338,39 @@ class EmailMultiAlternatives(EmailMessage): messages. For example, including text and HTML versions of the text is 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.""" - 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, fail_silently=False, auth_user=None, auth_password=None): diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index 1f2a2db981..9172d938d2 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -73,7 +73,7 @@ class Command(BaseCommand): model_list = get_models(app) for model in model_list: - objects.extend(model.objects.all()) + objects.extend(model._default_manager.all()) try: return serializers.serialize(format, objects, indent=indent) diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 53874f9f73..1ac75426c1 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -26,8 +26,11 @@ class BaseDatabaseCreation(object): self.connection = connection def _digest(self, *args): - "Generate a 32 bit digest of a set of arguments that can be used to shorten identifying names" - return '%x' % (abs(hash(args)) % (1<<32)) + """ + 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()): """ diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 78019f2bd1..529898ea27 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -112,9 +112,9 @@ class RelatedField(object): def do_related_class(self, other, cls): self.set_attributes_from_rel() - related = RelatedObject(other, cls, self) + self.related = RelatedObject(other, cls, self) 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): # If we are doing a lookup on a Related Field, we must be @@ -132,13 +132,13 @@ class RelatedField(object): v, field = getattr(v, v._meta.pk.name), v._meta.pk except AttributeError: pass - if not field: - field = self.rel.get_related_field() - if lookup_type in ('range', 'in'): - v = [v] - v = field.get_db_prep_lookup(lookup_type, v) - if isinstance(v, list): - v = v[0] + + if field: + if lookup_type in ('range', 'in'): + v = [v] + v = field.get_db_prep_lookup(lookup_type, v) + if isinstance(v, list): + v = v[0] return v if hasattr(value, 'as_sql') or hasattr(value, '_as_sql'): @@ -184,7 +184,6 @@ class SingleRelatedObjectDescriptor(object): def __get__(self, instance, instance_type=None): if instance is None: return self - try: return getattr(instance, self.cache_name) except AttributeError: @@ -232,6 +231,7 @@ class ReverseSingleRelatedObjectDescriptor(object): def __get__(self, instance, instance_type=None): if instance is None: return self + cache_name = self.field.get_cache_name() try: return getattr(instance, cache_name) @@ -272,6 +272,29 @@ class ReverseSingleRelatedObjectDescriptor(object): (value, instance._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 try: val = getattr(value, self.field.rel.get_related_field().attname) diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 07aa477d67..28ace85ca8 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -20,7 +20,7 @@ tutorial, so that the template contains an HTML ``
`` element: {% if error_message %}

{{ error_message }}

{% endif %} - + {% for choice in poll.choice_set.all %}
@@ -36,12 +36,12 @@ A quick rundown: selects one of the radio buttons and submits the form, it'll send the POST data ``choice=3``. This is HTML Forms 101. - * We set the form's ``action`` to ``vote/``, and we set ``method="post"``. - Using ``method="post"`` (as opposed to ``method="get"``) is very - important, because the act of submitting this form will alter data - server-side. Whenever you create a form that alters data server-side, use - ``method="post"``. This tip isn't specific to Django; it's just good Web - development practice. + * We set the form's ``action`` to ``/polls/{{ poll.id }}/vote/``, and we + set ``method="post"``. Using ``method="post"`` (as opposed to + ``method="get"``) is very important, because the act of submitting this + form will alter data server-side. Whenever you create a form that alters + data server-side, use ``method="post"``. This tip isn't specific to + Django; it's just good Web development practice. * ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone through its loop @@ -173,11 +173,11 @@ bunch of our own code. We'll just have to take a few steps to make the conversion. We will: 1. Convert the URLconf. - + 2. Rename a few templates. - + 3. Delete some the old, now unneeded views. - + 4. Fix up URL handling for the new views. Read on for details. diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index 0ca577059e..c8aafa8626 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -43,8 +43,8 @@ modify the filename as necessary to get a unique name. The actual name of the stored file will be returned. The ``content`` argument must be an instance of -:class:`django.db.files.File` or of a subclass of -:class:`~django.db.files.File`. +:class:`django.core.files.File` or of a subclass of +:class:`~django.core.files.File`. ``Storage.delete(name)`` ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 2ec74e4306..177df12862 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -800,21 +800,22 @@ you can use the name of the model, rather than the model object itself:: class Manufacturer(models.Model): # ... -Note, however, that this only refers to models in the same ``models.py`` file -- -you cannot use a string to reference a model defined in another application or -imported from elsewhere. +.. versionadded:: 1.0 -.. versionchanged:: 1.0 - Refering models in other applications must include the application label. - -To refer to models defined in another -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:: +To refer to models defined in another application, you can explicitly specify +a model with the full 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): 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 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 @@ -824,6 +825,9 @@ deal with the field names of your model object. .. _foreign-key-arguments: +Arguments +~~~~~~~~~ + :class:`ForeignKey` accepts an extra set of arguments -- all optional -- that 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 ` and :ref:`lazy ` relationships. +Database Representation +~~~~~~~~~~~~~~~~~~~~~~~ + 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 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: +Arguments +~~~~~~~~~ + :class:`ManyToManyField` accepts an extra set of arguments -- all optional -- that control how the relationship functions. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 370ac887b7..690ca7f391 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -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, any attempt to ``save()`` a ``ModelForm`` with missing fields will fail. To avoid this failure, you must instantiate your model with initial values - for the missing, but required fields, or use ``save(commit=False)`` and - manually set any extra required fields:: + for the missing, but required fields:: - instance = Instance(required_field='value') - form = InstanceForm(request.POST, instance=instance) - new_instance = form.save() + author = Author(title='Mr') + form = PartialAuthorForm(request.POST, instance=author) + form.save() - instance = form.save(commit=False) - instance.required_field = 'new value' - new_instance = instance.save() + Alternatively, you can use ``save(commit=False)`` and manually set + any extra required fields:: + + 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 ``save(commit=False)``. @@ -563,8 +566,8 @@ number of objects needed:: >>> formset.initial [{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}] -If the value of ``max_num`` is higher than the number of objects returned, up to -``extra`` additional blank forms will be added to the formset, so long as the +If the value of ``max_num`` is higher than the number of objects returned, up to +``extra`` additional blank forms will be added to the formset, so long as the total number of forms does not exceed ``max_num``:: >>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=2) diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py index b1d0cb37d0..b88af16782 100644 --- a/tests/modeltests/custom_pk/models.py +++ b/tests/modeltests/custom_pk/models.py @@ -136,11 +136,14 @@ 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) ->>> f = Foo.objects.get(bar=new_bar.pk) ->>> f == new_foo -True ->>> f.bar == new_bar -True + +# 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 diff --git a/tests/regressiontests/fixtures_regress/models.py b/tests/regressiontests/fixtures_regress/models.py index 621c80c15f..16a9fc4fc5 100644 --- a/tests/regressiontests/fixtures_regress/models.py +++ b/tests/regressiontests/fixtures_regress/models.py @@ -9,6 +9,9 @@ class Animal(models.Model): count = models.IntegerField() weight = models.FloatField() + # use a non-default name for the default manager + specimens = models.Manager() + def __unicode__(self): return self.common_name @@ -161,4 +164,10 @@ Weight = 1.2 () >>> 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"}}] + """} diff --git a/tests/regressiontests/m2m_regress/models.py b/tests/regressiontests/m2m_regress/models.py index 5484b26d17..913e719902 100644 --- a/tests/regressiontests/m2m_regress/models.py +++ b/tests/regressiontests/m2m_regress/models.py @@ -33,6 +33,14 @@ class SelfReferChild(SelfRefer): class SelfReferChildSibling(SelfRefer): 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": """ # Multiple m2m references to the same model or a different model must be # 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() [] +# 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() + """ } diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 40e2f7665f..f4c416c231 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -4,7 +4,7 @@ r""" >>> from django.conf import settings >>> 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 # Test normal ascii character case: @@ -95,4 +95,48 @@ BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection T >>> message['From'] '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 = '

This is an important message.

' +>>> 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 +... +

This is an important message.

+... +... +Content-Type: application/pdf +MIME-Version: 1.0 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="an attachment.pdf" +... +JVBERi0xLjQuJS4uLg== +... + """ diff --git a/tests/regressiontests/one_to_one_regress/tests.py b/tests/regressiontests/one_to_one_regress/tests.py new file mode 100644 index 0000000000..a0a0c0584f --- /dev/null +++ b/tests/regressiontests/one_to_one_regress/tests.py @@ -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)