diff options
| author | Marc Garcia <garcia.marc@gmail.com> | 2009-07-01 23:33:02 +0000 |
|---|---|---|
| committer | Marc Garcia <garcia.marc@gmail.com> | 2009-07-01 23:33:02 +0000 |
| commit | f5095f8cb367e51e18e084ac118d6900ab9ba735 (patch) | |
| tree | 58baa1119dead8fbb2a0d98af8f70cca0d55ef09 /django | |
| parent | 87cd3dfa55770e5a987d43797c013d81530083db (diff) | |
[soc2009/i18n] merged up to trunk r11147
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/i18n-improvements@11148 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
23 files changed, 228 insertions, 105 deletions
diff --git a/django/contrib/admindocs/templates/admin_doc/model_index.html b/django/contrib/admindocs/templates/admin_doc/model_index.html index 4dd7caa2a6..47c94c0c70 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_index.html +++ b/django/contrib/admindocs/templates/admin_doc/model_index.html @@ -36,7 +36,7 @@ <ul> {% regroup models by app_label as grouped_models %} {% for group in grouped_models %} - <li><a href="#{{ group.grouper }}">{{ group.grouper|capfirst }}</a></li> + <li><a href="#app-{{ group.grouper }}">{{ group.grouper|capfirst }}</a></li> {% endfor %} </ul> </div> diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index f66ea3e9b0..4f22fe0a0a 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -214,6 +214,22 @@ def model_detail(request, app_label, model_name): 'help_text': field.help_text, }) + # Gather many-to-many fields. + for field in opts.many_to_many: + data_type = related_object_name = field.rel.to.__name__ + app_label = field.rel.to._meta.app_label + verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': app_label, 'object_name': data_type} + fields.append({ + 'name': "%s.all" % field.name, + "data_type": 'List', + 'verbose': utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.module_name), + }) + fields.append({ + 'name' : "%s.count" % field.name, + 'data_type' : 'Integer', + 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), + }) + # Gather model methods. for func_name, func in model.__dict__.items(): if (inspect.isfunction(func) and len(inspect.getargspec(func)[0]) == 1): @@ -233,7 +249,7 @@ def model_detail(request, app_label, model_name): }) # Gather related objects - for rel in opts.get_all_related_objects(): + for rel in opts.get_all_related_objects() + opts.get_all_related_many_to_many_objects(): verbose = _("related `%(app_label)s.%(object_name)s` objects") % {'app_label': rel.opts.app_label, 'object_name': rel.opts.object_name} accessor = rel.get_accessor_name() fields.append({ diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index ae3a672a80..89a3dd9bba 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -37,6 +37,9 @@ def post_comment(request, next=None): if not data.get('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 ctype = data.get("content_type") object_pk = data.get("object_pk") 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/admin/options.py b/django/contrib/gis/admin/options.py index 6a63a9fe78..fff1cb20a6 100644 --- a/django/contrib/gis/admin/options.py +++ b/django/contrib/gis/admin/options.py @@ -32,7 +32,7 @@ class GeoModelAdmin(ModelAdmin): map_height = 400 map_srid = 4326 map_template = 'gis/admin/openlayers.html' - openlayers_url = 'http://openlayers.org/api/2.7/OpenLayers.js' + openlayers_url = 'http://openlayers.org/api/2.8/OpenLayers.js' point_zoom = num_zoom - 6 wms_url = 'http://labs.metacarta.com/wms/vmap0' wms_layer = 'basic' diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index cf1ccf6483..5df15a88b1 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -225,7 +225,7 @@ class GeoQuery(sql.Query): values.append(self.convert_values(value, field)) else: values.extend(row[index_start:]) - return values + return tuple(values) def convert_values(self, value, field): """ 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/maps/google/gmap.py b/django/contrib/gis/maps/google/gmap.py index de8f75c5a0..cca5dc941f 100644 --- a/django/contrib/gis/maps/google/gmap.py +++ b/django/contrib/gis/maps/google/gmap.py @@ -21,7 +21,7 @@ class GoogleMap(object): def __init__(self, key=None, api_url=None, version=None, center=None, zoom=None, dom_id='map', kml_urls=[], polylines=None, polygons=None, markers=None, - template='gis/google/google-single.js', + template='gis/google/google-map.js', js_module='geodjango', extra_context={}): @@ -162,7 +162,7 @@ class GoogleMapSet(GoogleMap): # This is the template used to generate the GMap load JavaScript for # each map in the set. - self.map_template = kwargs.pop('map_template', 'gis/google/google-map.js') + self.map_template = kwargs.pop('map_template', 'gis/google/google-single.js') # Running GoogleMap.__init__(), and resetting the template # value with default obtained above. diff --git a/django/contrib/gis/templates/gis/google/google-base.js b/django/contrib/gis/templates/gis/google/google-base.js deleted file mode 100644 index f3a91edbc4..0000000000 --- a/django/contrib/gis/templates/gis/google/google-base.js +++ /dev/null @@ -1,7 +0,0 @@ -{% block vars %}var geodjango = {};{% for icon in icons %} -var {{ icon.varname }} = new GIcon(G_DEFAULT_ICON); -{% if icon.image %}{{ icon.varname }}.image = "{{ icon.image }}";{% endif %} -{% if icon.shadow %}{{ icon.varname }}.shadow = "{{ icon.shadow }}";{% endif %} {% if icon.shadowsize %}{{ icon.varname }}.shadowSize = new GSize({{ icon.shadowsize.0 }}, {{ icon.shadowsize.1 }});{% endif %} -{% if icon.iconanchor %}{{ icon.varname }}.iconAnchor = new GPoint({{ icon.iconanchor.0 }}, {{ icon.iconanchor.1 }});{% endif %} {% if icon.iconsize %}{{ icon.varname }}.iconSize = new GSize({{ icon.iconsize.0 }}, {{ icon.iconsize.1 }});{% endif %} -{% if icon.infowindowanchor %}{{ icon.varname }}.infoWindowAnchor = new GPoint({{ icon.infowindowanchor.0 }}, {{ icon.infowindowanchor.1 }});{% endif %}{% endfor %}{% endblock %} -{% block functions %}{% endblock %}
\ No newline at end of file diff --git a/django/contrib/gis/templates/gis/google/google-map.js b/django/contrib/gis/templates/gis/google/google-map.js index e5f3a0e0e3..06f11e35f3 100644 --- a/django/contrib/gis/templates/gis/google/google-map.js +++ b/django/contrib/gis/templates/gis/google/google-map.js @@ -1,10 +1,16 @@ {% autoescape off %} +{% block vars %}var geodjango = {};{% for icon in icons %} +var {{ icon.varname }} = new GIcon(G_DEFAULT_ICON); +{% if icon.image %}{{ icon.varname }}.image = "{{ icon.image }}";{% endif %} +{% if icon.shadow %}{{ icon.varname }}.shadow = "{{ icon.shadow }}";{% endif %} {% if icon.shadowsize %}{{ icon.varname }}.shadowSize = new GSize({{ icon.shadowsize.0 }}, {{ icon.shadowsize.1 }});{% endif %} +{% if icon.iconanchor %}{{ icon.varname }}.iconAnchor = new GPoint({{ icon.iconanchor.0 }}, {{ icon.iconanchor.1 }});{% endif %} {% if icon.iconsize %}{{ icon.varname }}.iconSize = new GSize({{ icon.iconsize.0 }}, {{ icon.iconsize.1 }});{% endif %} +{% if icon.infowindowanchor %}{{ icon.varname }}.infoWindowAnchor = new GPoint({{ icon.infowindowanchor.0 }}, {{ icon.infowindowanchor.1 }});{% endif %}{% endfor %} +{% endblock vars %}{% block functions %} {% block load %}{{ js_module }}.{{ dom_id }}_load = function(){ if (GBrowserIsCompatible()) { {{ js_module }}.{{ dom_id }} = new GMap2(document.getElementById("{{ dom_id }}")); {{ js_module }}.{{ dom_id }}.setCenter(new GLatLng({{ center.1 }}, {{ center.0 }}), {{ zoom }}); - {% block controls %}{{ js_module }}.{{ dom_id }}.addControl(new GSmallMapControl()); - {{ js_module }}.{{ dom_id }}.addControl(new GMapTypeControl());{% endblock %} + {% block controls %}{{ js_module }}.{{ dom_id }}.setUIToDefault();{% endblock %} {% if calc_zoom %}var bounds = new GLatLngBounds(); var tmp_bounds = new GLatLngBounds();{% endif %} {% for kml_url in kml_urls %}{{ js_module }}.{{ dom_id }}_kml{{ forloop.counter }} = new GGeoXml("{{ kml_url }}"); {{ js_module }}.{{ dom_id }}.addOverlay({{ js_module }}.{{ dom_id }}_kml{{ forloop.counter }});{% endfor %} @@ -26,4 +32,4 @@ alert("Sorry, the Google Maps API is not compatible with this browser."); } } -{% endblock %}{% endautoescape %} +{% endblock load %}{% endblock functions %}{% endautoescape %} diff --git a/django/contrib/gis/templates/gis/google/google-multi.js b/django/contrib/gis/templates/gis/google/google-multi.js index 49ce584e36..e3c7e8f02b 100644 --- a/django/contrib/gis/templates/gis/google/google-multi.js +++ b/django/contrib/gis/templates/gis/google/google-multi.js @@ -1,4 +1,4 @@ -{% extends "gis/google/google-base.js" %} +{% extends "gis/google/google-map.js" %} {% block functions %} {{ load_map_js }} {{ js_module }}.load = function(){ diff --git a/django/contrib/gis/templates/gis/google/google-single.js b/django/contrib/gis/templates/gis/google/google-single.js index ab7901e42e..b930e4594f 100644 --- a/django/contrib/gis/templates/gis/google/google-single.js +++ b/django/contrib/gis/templates/gis/google/google-single.js @@ -1,2 +1,2 @@ -{% extends "gis/google/google-base.js" %} -{% block functions %}{% include "gis/google/google-map.js" %}{% endblock %}
\ No newline at end of file +{% extends "gis/google/google-map.js" %} +{% block vars %}{# No vars here because used within GoogleMapSet #}{% endblock %}
\ No newline at end of file diff --git a/django/contrib/gis/tests/relatedapp/models.py b/django/contrib/gis/tests/relatedapp/models.py index 1125d7fb85..726f9826c0 100644 --- a/django/contrib/gis/tests/relatedapp/models.py +++ b/django/contrib/gis/tests/relatedapp/models.py @@ -40,5 +40,5 @@ class Author(models.Model): class Book(models.Model): title = models.CharField(max_length=100) - author = models.ForeignKey(Author, related_name='books') + author = models.ForeignKey(Author, related_name='books', null=True) objects = models.GeoManager() diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 8c4f83b15a..502a3c0be9 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -257,6 +257,13 @@ class RelatedGeoModelTest(unittest.TestCase): self.assertEqual(1, len(qs)) self.assertEqual(3, qs[0].num_books) + def test13_select_related_null_fk(self): + "Testing `select_related` on a nullable ForeignKey via `GeoManager`. See #11381." + no_author = Book.objects.create(title='Without Author') + b = Book.objects.select_related('author').get(title='Without Author') + # Should be `None`, and not a 'dummy' model. + self.assertEqual(None, b.author) + # TODO: Related tests for KML, GML, and distance lookups. def suite(): 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/core/urlresolvers.py b/django/core/urlresolvers.py index ac83756f31..b3dcfdb8c1 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -195,6 +195,7 @@ class RegexURLResolver(object): return sub_match[0], sub_match[1], sub_match_dict tried.append(pattern.regex.pattern) raise Resolver404, {'tried': tried, 'path': new_path} + raise Resolver404, {'tried': [], 'path' : path} def _get_urlconf_module(self): try: diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index f6041c73f3..1ac75426c1 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -25,6 +25,13 @@ class BaseDatabaseCreation(object): def __init__(self, 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()): """ 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 # For MySQL, r_name must be unique in the first 64 characters. # 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;' % \ (qn(r_table), qn(truncate_name(r_name, self.connection.ops.max_name_length())), qn(r_col), qn(table), qn(col), @@ -187,8 +194,7 @@ class BaseDatabaseCreation(object): output.append('\n'.join(table_output)) for r_table, r_col, table, col in deferred: - 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)) 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())), @@ -289,7 +295,7 @@ class BaseDatabaseCreation(object): col = f.column r_table = model._meta.db_table 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;' % \ (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(table)), diff --git a/django/db/models/base.py b/django/db/models/base.py index 13ff7e8f35..a5c99865a6 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -411,29 +411,37 @@ class Model(object): save.alters_data = True - def save_base(self, raw=False, cls=None, force_insert=False, - force_update=False): + def save_base(self, raw=False, cls=None, origin=None, + force_insert=False, force_update=False): """ 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 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) - if not cls: + if cls is None: cls = self.__class__ - meta = self._meta - signal = True - signals.pre_save.send(sender=self.__class__, instance=self, raw=raw) + meta = cls._meta + if not meta.proxy: + origin = cls else: 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. # 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 # 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(): # At this point, parent's primary key field may be unknown # (for example, from administration form which doesn't fill @@ -441,7 +449,8 @@ class Model(object): 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)) - self.save_base(cls=parent) + self.save_base(cls=parent, origin=org) + if field: setattr(self, field.attname, self._get_pk_val(parent._meta)) if meta.proxy: @@ -492,8 +501,8 @@ class Model(object): setattr(self, meta.pk.attname, result) transaction.commit_unless_managed() - if signal: - signals.post_save.send(sender=self.__class__, instance=self, + if origin: + signals.post_save.send(sender=origin, instance=self, created=(not record_exists), raw=raw) save_base.alters_data = True diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 419695b74b..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,6 +132,7 @@ class RelatedField(object): v, field = getattr(v, v._meta.pk.name), v._meta.pk except AttributeError: pass + if field: if lookup_type in ('range', 'in'): v = [v] @@ -183,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: @@ -231,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) @@ -271,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) @@ -958,4 +982,3 @@ class ManyToManyField(RelatedField, Field): # A ManyToManyField is not represented by a single column, # so return None. return None - diff --git a/django/templatetags/cache.py b/django/templatetags/cache.py index 9c6ca76854..387dd8721c 100644 --- a/django/templatetags/cache.py +++ b/django/templatetags/cache.py @@ -3,6 +3,7 @@ from django.template import resolve_variable from django.core.cache import cache from django.utils.encoding import force_unicode from django.utils.http import urlquote +from django.utils.hashcompat import md5_constructor register = Library() @@ -23,7 +24,8 @@ class CacheNode(Node): except (ValueError, TypeError): raise TemplateSyntaxError('"cache" tag got a non-integer timeout value: %r' % expire_time) # Build a unicode key for this fragment and all vary-on's. - cache_key = u':'.join([self.fragment_name] + [urlquote(resolve_variable(var, context)) for var in self.vary_on]) + args = md5_constructor(u':'.join([urlquote(resolve_variable(var, context)) for var in self.vary_on])) + cache_key = 'template.cache.%s.%s' % (self.fragment_name, args.hexdigest()) value = cache.get(cache_key) if value is None: value = self.nodelist.render(context) |
