diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 393c25b7dc..79d3afe81b 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -101,6 +101,7 @@ DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. +DATABASE_OPTIONS = {} # Set to empy dictionary for default. # Optional named database connections in addition to the default. OTHER_DATABASES = {} @@ -231,6 +232,10 @@ MONTH_DAY_FORMAT = 'F j' # Hint: you really don't! TRANSACTIONS_MANAGED = False +# The User-Agent string to use when checking for URL validity through the +# isExistingURL validator. +URL_VALIDATOR_USER_AGENT = "Django/0.96pre (http://www.djangoproject.com)" + ############## # MIDDLEWARE # ############## diff --git a/django/contrib/admin/media/js/dateparse.js b/django/contrib/admin/media/js/dateparse.js index 51821c78e5..e1c870e146 100644 --- a/django/contrib/admin/media/js/dateparse.js +++ b/django/contrib/admin/media/js/dateparse.js @@ -169,8 +169,8 @@ var dateParsePatterns = [ handler: function(bits) { var d = new Date(); d.setYear(parseInt(bits[1])); - d.setDate(parseInt(bits[3], 10)); d.setMonth(parseInt(bits[2], 10) - 1); + d.setDate(parseInt(bits[3], 10)); return d; } }, diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index d09fa28b10..fcb5876b0c 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -177,8 +177,8 @@ def output_all(form_fields): output_all = register.simple_tag(output_all) def auto_populated_field_script(auto_pop_fields, change = False): + t = [] for field in auto_pop_fields: - t = [] if change: t.append('document.getElementById("id_%s")._changed = true;' % field.name) else: diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index a8aff1cfb3..90a84baaff 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -34,7 +34,7 @@ class CommentManager(models.Manager): """ Given a rating_string, this returns a tuple of (rating_range, options). >>> s = "scale:1-10|First_category|Second_category" - >>> get_rating_options(s) + >>> Comment.objects.get_rating_options(s) ([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ['First category', 'Second category']) """ rating_range, options = rating_string.split('|', 1) diff --git a/django/core/management.py b/django/core/management.py index 2014efddfa..d7f3994aef 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -186,6 +186,33 @@ def get_sql_reset(app): get_sql_reset.help_doc = "Prints the DROP TABLE SQL, then the CREATE TABLE SQL, for the given app name(s)." get_sql_reset.args = APP_ARGS +def get_sql_initial_data_for_model(model): + from django.db import models + from django.conf import settings + + opts = model._meta + app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) + output = [] + + # Some backends can't execute more than one SQL statement at a time, + # so split into separate statements. + statements = re.compile(r";[ \t]*$", re.M) + + # Find custom SQL, if it's available. + sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.object_name.lower(), settings.DATABASE_ENGINE)), + os.path.join(app_dir, "%s.sql" % opts.object_name.lower())] + for sql_file in sql_files: + if os.path.exists(sql_file): + fp = open(sql_file, 'U') + for statement in statements.split(fp.read()): + # Remove any comments from the file + statement = re.sub(r"--.*[\n\Z]", "", statement) + if statement.strip(): + output.append(statement + ";") + fp.close() + + return output + def get_sql_initial_data(app): "Returns a list of the initial INSERT SQL statements for the given app." from django.db import model_connection_name diff --git a/django/core/paginator.py b/django/core/paginator.py index 026fe0a675..380808a3dd 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -1,54 +1,46 @@ -from math import ceil - class InvalidPage(Exception): pass class ObjectPaginator(object): """ - This class makes pagination easy. Feed it a QuerySet, plus the number of - objects you want on each page. Then read the hits and pages properties to + This class makes pagination easy. Feed it a QuerySet or list, plus the number + of objects you want on each page. Then read the hits and pages properties to see how many pages it involves. Call get_page with a page number (starting at 0) to get back a list of objects for that page. Finally, check if a page number has a next/prev page using has_next_page(page_number) and has_previous_page(page_number). + + Use orphans to avoid small final pages. For example: + 13 records, num_per_page=10, orphans=2 --> pages==2, len(self.get_page(0))==10 + 12 records, num_per_page=10, orphans=2 --> pages==1, len(self.get_page(0))==12 """ - def __init__(self, query_set, num_per_page): + def __init__(self, query_set, num_per_page, orphans=0): self.query_set = query_set self.num_per_page = num_per_page - self._hits, self._pages = None, None - self._has_next = {} # Caches page_number -> has_next_boolean + self.orphans = orphans + self._hits = self._pages = None - def get_page(self, page_number): + def validate_page_number(self, page_number): try: page_number = int(page_number) except ValueError: raise InvalidPage - if page_number < 0: + if page_number < 0 or page_number > self.pages - 1: raise InvalidPage + return page_number - # Retrieve one extra record, and check for the existence of that extra - # record to determine whether there's a next page. - limit = self.num_per_page + 1 - offset = page_number * self.num_per_page - - object_list = list(self.query_set[offset:offset+limit]) - - if not object_list: - raise InvalidPage - - self._has_next[page_number] = (len(object_list) > self.num_per_page) - return object_list[:self.num_per_page] + def get_page(self, page_number): + page_number = self.validate_page_number(page_number) + bottom = page_number * self.num_per_page + top = bottom + self.num_per_page + if top + self.orphans >= self.hits: + top = self.hits + return self.query_set[bottom:top] def has_next_page(self, page_number): "Does page $page_number have a 'next' page?" - if not self._has_next.has_key(page_number): - if self._pages is None: - offset = (page_number + 1) * self.num_per_page - self._has_next[page_number] = len(self.query_set[offset:offset+1]) > 0 - else: - self._has_next[page_number] = page_number < (self.pages - 1) - return self._has_next[page_number] + return page_number < self.pages - 1 def has_previous_page(self, page_number): return page_number > 0 @@ -58,8 +50,7 @@ class ObjectPaginator(object): Returns the 1-based index of the first object on the given page, relative to total objects found (hits). """ - if page_number == 0: - return 1 + page_number = self.validate_page_number(page_number) return (self.num_per_page * page_number) + 1 def last_on_page(self, page_number): @@ -67,20 +58,30 @@ class ObjectPaginator(object): Returns the 1-based index of the last object on the given page, relative to total objects found (hits). """ - if page_number == 0 and self.num_per_page >= self._hits: - return self._hits - elif page_number == (self._pages - 1) and (page_number + 1) * self.num_per_page > self._hits: - return self._hits - return (page_number + 1) * self.num_per_page + page_number = self.validate_page_number(page_number) + page_number += 1 # 1-base + if page_number == self.pages: + return self.hits + return page_number * self.num_per_page def _get_hits(self): if self._hits is None: - self._hits = self.query_set.count() + # Try .count() or fall back to len(). + try: + self._hits = int(self.query_set.count()) + except (AttributeError, TypeError, ValueError): + # AttributeError if query_set has no object count. + # TypeError if query_set.count() required arguments. + # ValueError if int() fails. + self._hits = len(self.query_set) return self._hits def _get_pages(self): if self._pages is None: - self._pages = int(ceil(self.hits / float(self.num_per_page))) + hits = (self.hits - 1 - self.orphans) + if hits < 1: + hits = 0 + self._pages = hits // self.num_per_page + 1 return self._pages hits = property(_get_hits) diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index fb293c7c13..5b0acdc480 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -28,6 +28,7 @@ class Serializer(object): self.options = options self.stream = options.get("stream", StringIO()) + self.selected_fields = options.get("fields") self.start_serialization() for obj in queryset: @@ -36,11 +37,14 @@ class Serializer(object): if field is obj._meta.pk: continue elif field.rel is None: - self.handle_field(obj, field) + if self.selected_fields is None or field.attname in self.selected_fields: + self.handle_field(obj, field) else: - self.handle_fk_field(obj, field) + if self.selected_fields is None or field.attname[:-3] in self.selected_fields: + self.handle_fk_field(obj, field) for field in obj._meta.many_to_many: - self.handle_m2m_field(obj, field) + if self.selected_fields is None or field.attname in self.selected_fields: + self.handle_m2m_field(obj, field) self.end_object(obj) self.end_serialization() return self.getvalue() diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 4181bc7f2b..859816c226 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -76,7 +76,7 @@ def Deserializer(object_list, **options): m2m_data[field.name] = field.rel.to._default_manager.in_bulk(field_value).values() # Handle FK fields - elif field.rel and isinstance(field.rel, models.ManyToOneRel): + elif field.rel and isinstance(field.rel, models.ManyToOneRel) and field_value is not None: try: data[field.name] = field.rel.to._default_manager.get(pk=field_value) except field.rel.to.DoesNotExist: diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 09fff408cf..512b8c6176 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -166,7 +166,11 @@ class Deserializer(base.Deserializer): # If it doesn't exist, set the field to None (which might trigger # validation error, but that's expected). RelatedModel = self._get_model_from_node(node, "to") - return RelatedModel.objects.get(pk=getInnerText(node).strip().encode(self.encoding)) + # Check if there is a child node named 'None', returning None if so. + if len(node.childNodes) == 1 and node.childNodes[0].nodeName == 'None': + return None + else: + return RelatedModel.objects.get(pk=getInnerText(node).strip().encode(self.encoding)) def _handle_m2m_field_node(self, node): """ diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py index 0df68dcf56..fccb7bf087 100644 --- a/django/core/servers/fastcgi.py +++ b/django/core/servers/fastcgi.py @@ -33,9 +33,9 @@ Optional Fcgi settings: (setting=value) method=IMPL prefork or threaded (default prefork) maxrequests=NUMBER number of requests a child handles before it is killed and a new child is forked (0 = no limit). - maxspare=NUMBER max number of spare processes to keep running. - minspare=NUMBER min number of spare processes to prefork. - maxchildren=NUMBER hard limit number of processes in prefork mode. + maxspare=NUMBER max number of spare processes / threads + minspare=NUMBER min number of spare processes / threads. + maxchildren=NUMBER hard limit number of processes / threads daemonize=BOOL whether to detach from terminal. pidfile=FILE write the spawned process-id to this file. workdir=DIRECTORY change to this directory when daemonizing @@ -110,7 +110,11 @@ def runfastcgi(argset=[], **kwargs): } elif options['method'] in ('thread', 'threaded'): from flup.server.fcgi import WSGIServer - wsgi_opts = {} + wsgi_opts = { + 'maxSpare': int(options["maxspare"]), + 'minSpare': int(options["minspare"]), + 'maxThreads': int(options["maxchildren"]), + } else: return fastcgi_help("ERROR: Implementation must be one of prefork or thread.") diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 6abd71dc41..93c9c30cca 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -21,7 +21,10 @@ class NoReverseMatch(Exception): def get_mod_func(callback): # Converts 'django.views.news.stories.story_detail' to # ['django.views.news.stories', 'story_detail'] - dot = callback.rindex('.') + try: + dot = callback.rindex('.') + except ValueError: + return callback, '' return callback[:dot], callback[dot+1:] def reverse_helper(regex, *args, **kwargs): diff --git a/django/core/validators.py b/django/core/validators.py index 4c3f59143e..a2f3dc3bc3 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -8,6 +8,7 @@ validator will *always* be run, regardless of whether its associated form field is required. """ +import urllib2 from django.conf import settings from django.utils.translation import gettext, gettext_lazy, ngettext from django.utils.functional import Promise, lazy @@ -223,18 +224,26 @@ def isWellFormedXmlFragment(field_data, all_data): isWellFormedXml('%s' % field_data, all_data) def isExistingURL(field_data, all_data): - import urllib2 try: - u = urllib2.urlopen(field_data) + headers = { + "Accept" : "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", + "Accept-Language" : "en-us,en;q=0.5", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Connection" : "close", + "User-Agent": settings.URL_VALIDATOR_USER_AGENT + } + req = urllib2.Request(field_data,None, headers) + u = urllib2.urlopen(req) except ValueError: - raise ValidationError, gettext("Invalid URL: %s") % field_data + raise ValidationError, _("Invalid URL: %s") % field_data except urllib2.HTTPError, e: # 401s are valid; they just mean authorization is required. - if e.code not in ('401',): - raise ValidationError, gettext("The URL %s is a broken link.") % field_data + # 301 and 302 are redirects; they just mean look somewhere else. + if str(e.code) not in ('401','301','302'): + raise ValidationError, _("The URL %s is a broken link.") % field_data except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError, gettext("The URL %s is a broken link.") % field_data - + raise ValidationError, _("The URL %s is a broken link.") % field_data + def isValidUSState(field_data, all_data): "Checks that the given string is a valid two-letter U.S. state abbreviation" states = ['AA', 'AE', 'AK', 'AL', 'AP', 'AR', 'AS', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'FM', 'GA', 'GU', 'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MH', 'MI', 'MN', 'MO', 'MP', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'PR', 'PW', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VA', 'VI', 'VT', 'WA', 'WI', 'WV', 'WY'] @@ -344,6 +353,38 @@ class UniqueAmongstFieldsWithPrefix(object): if field_name != self.field_name and value == field_data: raise ValidationError, self.error_message +class NumberIsInRange(object): + """ + Validator that tests if a value is in a range (inclusive). + """ + def __init__(self, lower=None, upper=None, error_message=''): + self.lower, self.upper = lower, upper + if not error_message: + if lower and upper: + self.error_message = gettext("This value must be between %s and %s.") % (lower, upper) + elif lower: + self.error_message = gettext("This value must be at least %s.") % lower + elif upper: + self.error_message = gettext("This value must be no more than %s.") % upper + else: + self.error_message = error_message + + def __call__(self, field_data, all_data): + # Try to make the value numeric. If this fails, we assume another + # validator will catch the problem. + try: + val = float(field_data) + except ValueError: + return + + # Now validate + if self.lower and self.upper and (val < self.lower or val > self.upper): + raise ValidationError(self.error_message) + elif self.lower and val < self.lower: + raise ValidationError(self.error_message) + elif self.upper and val > self.upper: + raise ValidationError(self.error_message) + class IsAPowerOf(object): """ >>> v = IsAPowerOf(2) diff --git a/django/db/__init__.py b/django/db/__init__.py index d594c8ba35..b648f58008 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -28,7 +28,7 @@ _local = local() if not settings.DATABASE_ENGINE: settings.DATABASE_ENGINE = 'dummy' - + def connect(settings, **kw): """Connect to the database specified in settings. Returns a @@ -48,6 +48,8 @@ class ConnectionInfo(object): super(ConnectionInfo, self).__init__(**kw) if settings is None: from django.conf import settings + if not settings.DATABASE_OPTIONS: + settings.DATABASE_OPTIONS = {} self.settings = settings self.backend = self.load_backend() self.connection = self.backend.DatabaseWrapper(settings) diff --git a/django/db/backends/ado_mssql/base.py b/django/db/backends/ado_mssql/base.py index 8d813c6704..ae71747a9f 100644 --- a/django/db/backends/ado_mssql/base.py +++ b/django/db/backends/ado_mssql/base.py @@ -50,6 +50,7 @@ Database.convertVariantToPython = variantToPython class DatabaseWrapper(object): def __init__(self, settings): self.settings = settings + self.options = settings.DATABASE_OPTIONS self.connection = None self.queries = [] diff --git a/django/db/backends/ansi/sql.py b/django/db/backends/ansi/sql.py index a6c50298e6..447075cb84 100644 --- a/django/db/backends/ansi/sql.py +++ b/django/db/backends/ansi/sql.py @@ -291,6 +291,8 @@ class SchemaBuilder(object): output.append(BoundStatement(fp.read(), db.connection)) else: for statement in statements.split(fp.read()): + # Remove any comments from the file + statement = re.sub(r"--.*[\n\Z]", "", statement) if statement.strip(): output.append(BoundStatement(statement + ";", db.connection)) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index d04c924f0a..b0a0b004ef 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -64,6 +64,7 @@ class DatabaseWrapper(object): self.connection = None self.queries = [] self.server_version = None + self.options = settings.DATABASE_OPTIONS def _valid_connection(self): if self.connection is not None: @@ -90,6 +91,7 @@ class DatabaseWrapper(object): kwargs['host'] = settings.DATABASE_HOST if settings.DATABASE_PORT: kwargs['port'] = int(settings.DATABASE_PORT) + kwargs.update(self.options) self.connection = Database.connect(**kwargs) cursor = self.connection.cursor() if self.connection.get_server_info() >= '4.1': diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 754fcef776..3f7b32e844 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -18,6 +18,7 @@ class DatabaseWrapper(object): self.settings = settings self.connection = None self.queries = [] + self.options = settings.DATABASE_OPTIONS def _valid_connection(self): return self.connection is not None @@ -29,10 +30,10 @@ class DatabaseWrapper(object): settings.DATABASE_HOST = 'localhost' if len(settings.DATABASE_PORT.strip()) != 0: dsn = Database.makedsn(settings.DATABASE_HOST, int(settings.DATABASE_PORT), settings.DATABASE_NAME) - self.connection = Database.connect(settings.DATABASE_USER, settings.DATABASE_PASSWORD, dsn) + self.connection = Database.connect(settings.DATABASE_USER, settings.DATABASE_PASSWORD, dsn, **self.options) else: conn_string = "%s/%s@%s" % (settings.DATABASE_USER, settings.DATABASE_PASSWORD, settings.DATABASE_NAME) - self.connection = Database.connect(conn_string) + self.connection = Database.connect(conn_string, **self.options) return FormatStylePlaceholderCursor(self.connection) def _commit(self): diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index cc6686ed81..ac5cb2f84d 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -18,6 +18,7 @@ class DatabaseWrapper(object): self.settings = settings self.connection = None self.queries = [] + self.options = settings.DATABASE_OPTIONS def cursor(self): settings = self.settings @@ -34,7 +35,7 @@ class DatabaseWrapper(object): conn_string += " host=%s" % settings.DATABASE_HOST if settings.DATABASE_PORT: conn_string += " port=%s" % settings.DATABASE_PORT - self.connection = Database.connect(conn_string) + self.connection = Database.connect(conn_string, **self.options) self.connection.set_isolation_level(1) # make transactions transparent to all cursors cursor = self.connection.cursor() cursor.execute("SET TIME ZONE %s", [settings.TIME_ZONE]) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index d62376596f..e7fe807d93 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -18,6 +18,7 @@ class DatabaseWrapper(object): self.settings = settings self.connection = None self.queries = [] + self.options = settings.DATABASE_OPTIONS def cursor(self): settings = self.settings @@ -34,7 +35,7 @@ class DatabaseWrapper(object): conn_string += " host=%s" % settings.DATABASE_HOST if settings.DATABASE_PORT: conn_string += " port=%s" % settings.DATABASE_PORT - self.connection = Database.connect(conn_string) + self.connection = Database.connect(conn_string, **self.options) self.connection.set_isolation_level(1) # make transactions transparent to all cursors cursor = self.connection.cursor() cursor.tzinfo_factory = None diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 0617179fe1..d467b6b6d4 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -46,13 +46,17 @@ class DatabaseWrapper(local): self.settings = settings self.connection = None self.queries = [] + self.options = settings.DATABASE_OPTIONS def cursor(self): settings = self.settings if self.connection is None: - self.connection = Database.connect(settings.DATABASE_NAME, - detect_types=Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES) - + kwargs = { + 'database': settings.DATABASE_NAME, + 'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES, + } + kwargs.update(self.options) + self.connection = Database.connect(**kwargs) # Register extract and date_trunc functions. self.connection.create_function("django_extract", 2, _sqlite_extract) self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 3ec1b41485..d8f86fef4f 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -17,7 +17,7 @@ class CursorDebugWrapper(object): if not isinstance(params, (tuple, dict)): params = tuple(params) self.db.queries.append({ - 'sql': sql % tuple(params), + 'sql': sql % params, 'time': "%.3f" % (stop - start), }) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index df61a3a331..c0d7a727ec 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -591,7 +591,7 @@ class FileField(Field): # If the raw path is passed in, validate it's under the MEDIA_ROOT. def isWithinMediaRoot(field_data, all_data): f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data)) - if not f.startswith(os.path.normpath(settings.MEDIA_ROOT)): + if not f.startswith(os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))): raise validators.ValidationError, _("Enter a valid filename.") field_list[1].validator_list.append(isWithinMediaRoot) return field_list diff --git a/django/db/models/manipulators.py b/django/db/models/manipulators.py index 9a898e217f..c61e82f813 100644 --- a/django/db/models/manipulators.py +++ b/django/db/models/manipulators.py @@ -286,7 +286,7 @@ def manipulator_validator_unique_together(field_name_list, opts, self, field_dat # This is really not going to work for fields that have different # form fields, e.g. DateTime. # This validation needs to occur after html2python to be effective. - field_val = all_data.get(f.attname, None) + field_val = all_data.get(f.name, None) if field_val is None: # This will be caught by another validator, assuming the field # doesn't have blank=True. diff --git a/django/forms/__init__.py b/django/forms/__init__.py index 5f47059f03..0b9ac05edb 100644 --- a/django/forms/__init__.py +++ b/django/forms/__init__.py @@ -108,8 +108,13 @@ class FormWrapper(object): This allows dictionary-style lookups of formfields. It also handles feeding prepopulated data and validation error messages to the formfield objects. """ - def __init__(self, manipulator, data, error_dict, edit_inline=True): - self.manipulator, self.data = manipulator, data + def __init__(self, manipulator, data=None, error_dict=None, edit_inline=True): + self.manipulator = manipulator + if data is None: + data = {} + if error_dict is None: + error_dict = {} + self.data = data self.error_dict = error_dict self._inline_collections = None self.edit_inline = edit_inline diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 79558bbf9e..54089cb3c3 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -188,17 +188,35 @@ url_re = re.compile( r'(?::\d+)?' # optional port r'(?:/?|/\S+)$', re.IGNORECASE) +try: + from django.conf import settings + URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT +except ImportError: + # It's OK if Django settings aren't configured. + URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' + class URLField(RegexField): - def __init__(self, required=True, verify_exists=False, widget=None): + def __init__(self, required=True, verify_exists=False, widget=None, + validator_user_agent=URL_VALIDATOR_USER_AGENT): RegexField.__init__(self, url_re, u'Enter a valid URL.', required, widget) self.verify_exists = verify_exists + self.user_agent = validator_user_agent def clean(self, value): value = RegexField.clean(self, value) if self.verify_exists: import urllib2 + from django.conf import settings + headers = { + "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", + "Accept-Language": "en-us,en;q=0.5", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Connection": "close", + "User-Agent": self.user_agent, + } try: - u = urllib2.urlopen(value) + req = urllib2.Request(field_data, None, headers) + u = urllib2.urlopen(req) except ValueError: raise ValidationError(u'Enter a valid URL.') except: # urllib2.URLError, httplib.InvalidURL, etc. diff --git a/django/template/__init__.py b/django/template/__init__.py index 4e0bcf384e..5affafeba9 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -868,8 +868,11 @@ class Library(object): dict = func(*args) if not getattr(self, 'nodelist', False): - from django.template.loader import get_template - t = get_template(file_name) + from django.template.loader import get_template, select_template + if hasattr(file_name, '__iter__'): + t = select_template(file_name) + else: + t = get_template(file_name) self.nodelist = t.nodelist return self.nodelist.render(context_class(dict)) diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index cf1d3d5f6d..969ef7b28b 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -421,7 +421,11 @@ def filesizeformat(bytes): Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 bytes, etc). """ - bytes = float(bytes) + try: + bytes = float(bytes) + except TypeError: + return "0 bytes" + if bytes < 1024: return "%d byte%s" % (bytes, bytes != 1 and 's' or '') if bytes < 1024 * 1024: diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 02266dfa5c..992917c77f 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -124,17 +124,27 @@ class ForNode(Node): return nodelist.render(context) class IfChangedNode(Node): - def __init__(self, nodelist): + def __init__(self, nodelist, *varlist): self.nodelist = nodelist self._last_seen = None + self._varlist = varlist def render(self, context): if context.has_key('forloop') and context['forloop']['first']: self._last_seen = None - content = self.nodelist.render(context) - if content != self._last_seen: + try: + if self._varlist: + # Consider multiple parameters. + # This automatically behaves like a OR evaluation of the multiple variables. + compare_to = [resolve_variable(var, context) for var in self._varlist] + else: + compare_to = self.nodelist.render(context) + except VariableDoesNotExist: + compare_to = None + + if compare_to != self._last_seen: firstloop = (self._last_seen == None) - self._last_seen = content + self._last_seen = compare_to context.push() context['ifchanged'] = {'firstloop': firstloop} content = self.nodelist.render(context) @@ -634,23 +644,34 @@ def ifchanged(parser, token): """ Check if a value has changed from the last iteration of a loop. - The 'ifchanged' block tag is used within a loop. It checks its own rendered - contents against its previous state and only displays its content if the - value has changed:: + The 'ifchanged' block tag is used within a loop. It has two possible uses. -

Archive for {{ year }}

+ 1. Checks its own rendered contents against its previous state and only + displays the content if it has changed. For example, this displays a list of + days, only displaying the month if it changes:: - {% for date in days %} - {% ifchanged %}

{{ date|date:"F" }}

{% endifchanged %} - {{ date|date:"j" }} - {% endfor %} +

Archive for {{ year }}

+ + {% for date in days %} + {% ifchanged %}

{{ date|date:"F" }}

{% endifchanged %} + {{ date|date:"j" }} + {% endfor %} + + 2. If given a variable, check if that variable has changed. For example, the + following shows the date every time it changes, but only shows the hour if both + the hour and the date has changed:: + + {% for date in days %} + {% ifchanged date.date %} {{date.date}} {% endifchanged %} + {% ifchanged date.hour date.date %} + {{date.hour}} + {% endifchanged %} + {% endfor %} """ bits = token.contents.split() - if len(bits) != 1: - raise TemplateSyntaxError, "'ifchanged' tag takes no arguments" nodelist = parser.parse(('endifchanged',)) parser.delete_first_token() - return IfChangedNode(nodelist) + return IfChangedNode(nodelist, *bits[1:]) ifchanged = register.tag(ifchanged) #@register.tag diff --git a/docs/forms.txt b/docs/forms.txt index 4a4ba37289..1c683c44f7 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -610,6 +610,15 @@ fails. If no message is passed in, a default message is used. string "123" is less than the string "2", for example. If you don't want string comparison here, you will need to write your own validator. +``NumberIsInRange`` + Takes two boundary number, ``lower`` and ``upper`` and checks that the + field is greater than ``lower`` (if given) and less than ``upper`` (if + given). + + Both checks are inclusive; that is, ``NumberIsInRange(10, 20)`` will allow + values of both 10 and 20. This validator only checks numeric fields + (i.e. floats and integer fields). + ``IsAPowerOf`` Takes an integer argument and when called as a validator, checks that the field being validated is a power of the integer. diff --git a/docs/settings.txt b/docs/settings.txt index 2253faf5ff..61a8f7622a 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -265,6 +265,14 @@ Default: ``''`` (Empty string) The name of the database to use. For SQLite, it's the full path to the database file. +DATABASE_OPTIONS +---------------- + +Default: ``{}`` (Empty dictionary) + +Extra parameters to use when connecting to the database. Consult backend +module's document for available keywords. + DATABASE_PASSWORD ----------------- @@ -821,6 +829,16 @@ manual configuration option (see below), Django will *not* touch the ``TZ`` environment variable, and it'll be up to you to ensure your processes are running in the correct environment. +URL_VALIDATOR_USER_AGENT +------------------------ + +Default: ``Django/ (http://www.djangoproject.com/)`` + +The string to use as the ``User-Agent`` header when checking to see if URLs +exist (see the ``verify_exists`` option on URLField_). + +.. URLField: ../model_api/#urlfield + USE_ETAGS --------- diff --git a/docs/syndication_feeds.txt b/docs/syndication_feeds.txt index 225b67eb02..59a9022d9b 100644 --- a/docs/syndication_feeds.txt +++ b/docs/syndication_feeds.txt @@ -95,7 +95,7 @@ latest five news items:: from django.contrib.syndication.feeds import Feed from chicagocrime.models import NewsItem - class SiteNewsFeed(Feed): + class LatestEntries(Feed): title = "Chicagocrime.org site news" link = "/sitenews/" description = "Updates on changes and additions to chicagocrime.org." @@ -120,14 +120,14 @@ One thing's left to do. In an RSS feed, each ```` has a ````, put into those elements. * To specify the contents of ``<title>`` and ``<description>``, create - `Django templates`_ called ``feeds/sitenews_title.html`` and - ``feeds/sitenews_description.html``, where ``sitenews`` is the ``slug`` + `Django templates`_ called ``feeds/latest_title.html`` and + ``feeds/latest_description.html``, where ``latest`` is the ``slug`` specified in the URLconf for the given feed. Note the ``.html`` extension is required. The RSS system renders that template for each item, passing it two template context variables: * ``{{ obj }}`` -- The current object (one of whichever objects you - returned in ``items()``). + returned in ``items()``). * ``{{ site }}`` -- A ``django.models.core.sites.Site`` object representing the current site. This is useful for ``{{ site.domain }}`` or ``{{ site.name }}``. @@ -145,6 +145,16 @@ put into those elements. Both ``get_absolute_url()`` and ``item_link()`` should return the item's URL as a normal Python string. + * For the LatestEntries example above, we could have very simple feed templates: + + * latest_title.html:: + + {{ obj.title }} + + * latest_description.html:: + + {{ obj.description }} + .. _chicagocrime.org: http://www.chicagocrime.org/ .. _object-relational mapper: http://www.djangoproject.com/documentation/db_api/ .. _Django templates: http://www.djangoproject.com/documentation/templates/ diff --git a/docs/templates.txt b/docs/templates.txt index 92d4b53660..cb06fa27d9 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -525,16 +525,29 @@ ifchanged Check if a value has changed from the last iteration of a loop. -The ``ifchanged`` block tag is used within a loop. It checks its own rendered -contents against its previous state and only displays its content if the value -has changed:: +The 'ifchanged' block tag is used within a loop. It has two possible uses. - <h1>Archive for {{ year }}</h1> +1. Checks its own rendered contents against its previous state and only + displays the content if it has changed. For example, this displays a list of + days, only displaying the month if it changes:: - {% for day in days %} - {% ifchanged %}<h3>{{ day|date:"F" }}</h3>{% endifchanged %} - <a href="{{ day|date:"M/d"|lower }}/">{{ day|date:"j" }}</a> - {% endfor %} + <h1>Archive for {{ year }}</h1> + + {% for date in days %} + {% ifchanged %}<h3>{{ date|date:"F" }}</h3>{% endifchanged %} + <a href="{{ date|date:"M/d"|lower }}/">{{ date|date:"j" }}</a> + {% endfor %} + +2. If given a variable, check if that variable has changed. For example, the + following shows the date every time it changes, but only shows the hour if both + the hour and the date has changed:: + + {% for date in days %} + {% ifchanged date.date %} {{date.date}} {% endifchanged %} + {% ifchanged date.hour date.date %} + {{date.hour}} + {% endifchanged %} + {% endfor %} ifequal ~~~~~~~ diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index ea2385dc79..3319b5cafa 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -64,4 +64,17 @@ True >>> paginator.last_on_page(1) 9 +# Add a few more records to test out the orphans feature. +>>> for x in range(10, 13): +... Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() + +# With orphans set to 3 and 10 items per page, we should get all 12 items on a single page: +>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=3) +>>> paginator.pages +1 + +# With orphans only set to 1, we should get two pages: +>>> paginator = ObjectPaginator(Article.objects.all(), 10, orphans=1) +>>> paginator.pages +2 """} diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index bcb9c66254..3c31bb0604 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -328,6 +328,21 @@ class Templates(unittest.TestCase): 'ifchanged05': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (1, 2, 3)}, '1123123123'), 'ifchanged06': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2)}, '1222'), 'ifchanged07': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% for y in numy %}{% ifchanged %}{{ y }}{% endifchanged %}{% endfor %}{% endfor %}{% endfor %}', { 'num': (1, 1, 1), 'numx': (2, 2, 2), 'numy': (3, 3, 3)}, '1233323332333'), + + # Test one parameter given to ifchanged. + 'ifchanged-param01': ('{% for n in num %}{% ifchanged n %}..{% endifchanged %}{{ n }}{% endfor %}', { 'num': (1,2,3) }, '..1..2..3'), + 'ifchanged-param02': ('{% for n in num %}{% for x in numx %}{% ifchanged n %}..{% endifchanged %}{{ x }}{% endfor %}{% endfor %}', { 'num': (1,2,3), 'numx': (5,6,7) }, '..567..567..567'), + + # Test multiple parameters to ifchanged. + 'ifchanged-param03': ('{% for n in num %}{{ n }}{% for x in numx %}{% ifchanged x n %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1,1,2), 'numx': (5,6,6) }, '156156256'), + + # Test a date+hour like construct, where the hour of the last day + # is the same but the date had changed, so print the hour anyway. + 'ifchanged-param04': ('{% for d in days %}{% ifchanged %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), + + # Logically the same as above, just written with explicit + # ifchanged for the day. + 'ifchanged-param04': ('{% for d in days %}{% ifchanged d.day %}{{ d.day }}{% endifchanged %}{% for h in d.hours %}{% ifchanged d.day h %}{{ h }}{% endifchanged %}{% endfor %}{% endfor %}', {'days':[{'day':1, 'hours':[1,2,3]},{'day':2, 'hours':[3]},] }, '112323'), ### IFEQUAL TAG ########################################################### 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""),