From 71012a4be3830b1040815243ebb7c0d48dbd8e57 Mon Sep 17 00:00:00 2001 From: Jason Pellerin Date: Wed, 29 Nov 2006 20:02:43 +0000 Subject: [PATCH] [multi-db] Merge trunk to [3812]. Some tests still failing. git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@4139 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- AUTHORS | 7 ++++ README | 4 +-- django/conf/global_settings.py | 4 +++ .../admin/templatetags/admin_modify.py | 4 ++- django/contrib/admin/views/main.py | 2 ++ django/contrib/auth/decorators.py | 8 +++++ django/contrib/auth/handlers/modpython.py | 31 +++++++++-------- django/core/context_processors.py | 6 +++- django/core/handlers/modpython.py | 7 ++-- django/core/handlers/wsgi.py | 28 ++++++++++++++-- django/core/management.py | 22 +++++++++---- django/core/serializers/json.py | 2 +- django/core/servers/basehttp.py | 9 +++-- django/core/servers/fastcgi.py | 2 +- django/core/validators.py | 9 ++--- django/core/xheaders.py | 5 +-- django/db/backends/util.py | 6 ++-- django/db/models/fields/generic.py | 2 +- django/forms/__init__.py | 27 +++++++++------ django/http/__init__.py | 31 +++++++++-------- django/middleware/common.py | 3 +- django/middleware/doc.py | 9 ++--- django/template/defaultfilters.py | 2 +- django/template/defaulttags.py | 12 +++++-- django/utils/datastructures.py | 3 ++ django/utils/text.py | 6 +++- docs/authentication.txt | 24 +++++++++++++- docs/contributing.txt | 33 ++++++++++++------- docs/db-api.txt | 2 +- docs/django-admin.txt | 18 ++++++++-- docs/forms.txt | 33 +++++++++++++++++-- docs/model-api.txt | 4 ++- docs/serialization.txt | 15 +++++++++ docs/settings.txt | 6 ++++ docs/templates_python.txt | 17 ++++++---- tests/regressiontests/defaultfilters/tests.py | 3 ++ tests/regressiontests/templates/tests.py | 5 +++ 37 files changed, 310 insertions(+), 101 deletions(-) diff --git a/AUTHORS b/AUTHORS index bad8008d86..c5d14eb86d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -42,6 +42,7 @@ And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- people who have submitted patches, reported bugs, added translations, helped answer newbie questions, and generally made Django that much better: + adurdin@gmail.com akaihola Andreas ant9000@netwise.it @@ -68,15 +69,19 @@ answer newbie questions, and generally made Django that much better: Alex Dedul deric@monowerks.com dne@mayonnaise.net + Maximillian Dornseif + dummy@habmalnefrage.de Jeremy Dunck Andy Dustman Clint Ecker + favo@exoweb.net gandalf@owca.info Baishampayan Ghose martin.glueck@gmail.com Simon Greenhill Espen Grindhaug Brant Harris + heckj@mac.com hipertracker@gmail.com Ian Holsman Kieran Holland @@ -96,6 +101,7 @@ answer newbie questions, and generally made Django that much better: lakin.wecker@gmail.com Stuart Langridge Eugene Lazutkin + Jeong-Min Lee Christopher Lenz limodou Martin Maney @@ -122,6 +128,7 @@ answer newbie questions, and generally made Django that much better: Daniel Poelzleithner J. Rademaker Michael Radziej + ramiro Brian Ray rhettg@gmail.com Oliver Rutherfurd diff --git a/README b/README index d52451d3ba..084f863a1e 100644 --- a/README +++ b/README @@ -25,10 +25,10 @@ http://code.djangoproject.com/newticket To get more help: * Join the #django channel on irc.freenode.net. Lots of helpful people - hang out there. Read the archives at http://loglibrary.com/179 . + hang out there. Read the archives at http://simon.bofh.ms/logger/django/ . * Join the django-users mailing list, or read the archives, at - http://groups-beta.google.com/group/django-users. + http://groups.google.com/group/django-users. To contribute to Django: diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 20ed49b583..7b699f6b84 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -275,6 +275,10 @@ CACHE_MIDDLEWARE_KEY_PREFIX = '' COMMENTS_ALLOW_PROFANITIES = False +# The profanities that will trigger a validation error in the +# 'hasNoProfanities' validator. All of these should be in lower-case. +PROFANITIES_LIST = ['asshat', 'asshead', 'asshole', 'cunt', 'fuck', 'gook', 'nigger', 'shit'] + # The group ID that designates which users are banned. # Set to None if you're not using it. COMMENTS_BANNED_USERS_GROUP = None diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 55d8bb2b95..875e61f959 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -160,8 +160,10 @@ class EditInlineNode(template.Node): context.push() if relation.field.rel.edit_inline == models.TABULAR: bound_related_object_class = TabularBoundRelatedObject - else: + elif relation.field.rel.edit_inline == models.STACKED: bound_related_object_class = StackedBoundRelatedObject + else: + bound_related_object_class = relation.field.rel.edit_inline original = context.get('original', None) bound_related_object = relation.bind(context['form'], original, bound_related_object_class) context['bound_related_object'] = bound_related_object diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 7c942ca5b8..38d64bd73b 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -727,6 +727,8 @@ class ChangeList(object): for bit in self.query.split(): or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields] other_qs = QuerySet(self.model) + if qs._select_related: + other_qs = other_qs.select_related() other_qs = other_qs.filter(reduce(operator.or_, or_queries)) qs = qs & other_qs diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 0102496a33..8164d8314e 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -26,3 +26,11 @@ login_required.__doc__ = ( to the log-in page if necessary. """ ) + +def permission_required(perm, login_url=LOGIN_URL): + """ + Decorator for views that checks if a user has a particular permission + enabled, redirectiing to the log-in page if necessary. + """ + return user_passes_test(lambda u: u.has_perm(perm), login_url=login_url) + diff --git a/django/contrib/auth/handlers/modpython.py b/django/contrib/auth/handlers/modpython.py index e6719794a1..c7d921313d 100644 --- a/django/contrib/auth/handlers/modpython.py +++ b/django/contrib/auth/handlers/modpython.py @@ -22,6 +22,8 @@ def authenhandler(req, **kwargs): os.environ['DJANGO_SETTINGS_MODULE'] = settings_module from django.contrib.auth.models import User + from django import db + db.reset_queries() # check that the username is valid kwargs = {'username': req.user, 'is_active': True} @@ -30,18 +32,21 @@ def authenhandler(req, **kwargs): if superuser_only: kwargs['is_superuser'] = True try: - user = User.objects.get(**kwargs) - except User.DoesNotExist: - return apache.HTTP_UNAUTHORIZED - - # check the password and any permission given - if user.check_password(req.get_basic_auth_pw()): - if permission_name: - if user.has_perm(permission_name): - return apache.OK + try: + user = User.objects.get(**kwargs) + except User.DoesNotExist: + return apache.HTTP_UNAUTHORIZED + + # check the password and any permission given + if user.check_password(req.get_basic_auth_pw()): + if permission_name: + if user.has_perm(permission_name): + return apache.OK + else: + return apache.HTTP_UNAUTHORIZED else: - return apache.HTTP_UNAUTHORIZED + return apache.OK else: - return apache.OK - else: - return apache.HTTP_UNAUTHORIZED + return apache.HTTP_UNAUTHORIZED + finally: + db.connection.close() diff --git a/django/core/context_processors.py b/django/core/context_processors.py index 2ae9a6d972..f4b288dfc4 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -51,15 +51,19 @@ def request(request): class PermLookupDict(object): def __init__(self, user, module_name): self.user, self.module_name = user, module_name + def __repr__(self): - return str(self.user.get_permission_list()) + return str(self.user.get_all_permissions()) + def __getitem__(self, perm_name): return self.user.has_perm("%s.%s" % (self.module_name, perm_name)) + def __nonzero__(self): return self.user.has_module_perms(self.module_name) class PermWrapper(object): def __init__(self, user): self.user = user + def __getitem__(self, module_name): return PermLookupDict(self.user, module_name) diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index 07c98e3b59..db3c33147b 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -155,8 +155,11 @@ def populate_apache_request(http_response, mod_python_req): for c in http_response.cookies.values(): mod_python_req.headers_out.add('Set-Cookie', c.output(header='')) mod_python_req.status = http_response.status_code - for chunk in http_response.iterator: - mod_python_req.write(chunk) + try: + for chunk in http_response: + mod_python_req.write(chunk) + finally: + http_response.close() def handler(req): # mod_python hooks into this function. diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 5c48c9dace..87e67e9c5b 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -4,6 +4,11 @@ from django.dispatch import dispatcher from django.utils import datastructures from django import http from pprint import pformat +from shutil import copyfileobj +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html STATUS_CODE_TEXT = { @@ -50,6 +55,21 @@ STATUS_CODE_TEXT = { 505: 'HTTP VERSION NOT SUPPORTED', } +def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0): + """ + A version of shutil.copyfileobj that will not read more than 'size' bytes. + This makes it safe from clients sending more than CONTENT_LENGTH bytes of + data in the body. + """ + if not size: + return copyfileobj(fsrc, fdst, length) + while size > 0: + buf = fsrc.read(min(length, size)) + if not buf: + break + fdst.write(buf) + size -= len(buf) + class WSGIRequest(http.HttpRequest): def __init__(self, environ): self.environ = environ @@ -119,7 +139,11 @@ class WSGIRequest(http.HttpRequest): try: return self._raw_post_data except AttributeError: - self._raw_post_data = self.environ['wsgi.input'].read(int(self.environ["CONTENT_LENGTH"])) + buf = StringIO() + content_length = int(self.environ['CONTENT_LENGTH']) + safe_copyfileobj(self.environ['wsgi.input'], buf, size=content_length) + self._raw_post_data = buf.getvalue() + buf.close() return self._raw_post_data GET = property(_get_get, _set_get) @@ -163,4 +187,4 @@ class WSGIHandler(BaseHandler): for c in response.cookies.values(): response_headers.append(('Set-Cookie', c.output(header=''))) start_response(status, response_headers) - return response.iterator + return response diff --git a/django/core/management.py b/django/core/management.py index 5164585dbb..bd162a1e8d 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -812,7 +812,8 @@ def get_validation_errors(outfile, app=None): try: f = opts.get_field(fn) except models.FieldDoesNotExist: - e.add(opts, '"admin.list_filter" refers to %r, which isn\'t a field.' % fn) + if not hasattr(cls, fn): + e.add(opts, '"admin.list_display_links" refers to %r, which isn\'t an attribute, method or property.' % fn) if fn not in opts.admin.list_display: e.add(opts, '"admin.list_display_links" refers to %r, which is not defined in "admin.list_display".' % fn) # list_filter @@ -870,10 +871,12 @@ def get_validation_errors(outfile, app=None): return len(e.errors) -def validate(outfile=sys.stdout): +def validate(outfile=sys.stdout, silent_success=False): "Validates all installed models." try: num_errors = get_validation_errors(outfile) + if silent_success and num_errors == 0: + return outfile.write('%s error%s found.\n' % (num_errors, num_errors != 1 and 's' or '')) except ImproperlyConfigured: outfile.write("Skipping validation because things aren't configured properly.") @@ -896,7 +899,7 @@ def _check_for_validation_errors(app=None): sys.stderr.write(s.read()) sys.exit(1) -def runserver(addr, port, use_reloader=True): +def runserver(addr, port, use_reloader=True, admin_media_dir=''): "Starts a lightweight Web server for development." from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException from django.core.handlers.wsgi import WSGIHandler @@ -914,7 +917,10 @@ def runserver(addr, port, use_reloader=True): print "Development server is running at http://%s:%s/" % (addr, port) print "Quit the server with %s." % quit_command try: - run(addr, int(port), AdminMediaHandler(WSGIHandler())) + import django + path = admin_media_dir or django.__path__[0] + '/contrib/admin/media' + handler = AdminMediaHandler(WSGIHandler(), path) + run(addr, int(port), handler) except WSGIServerException, e: # Use helpful error messages instead of ugly tracebacks. ERRORS = { @@ -935,7 +941,7 @@ def runserver(addr, port, use_reloader=True): autoreload.main(inner_run) else: inner_run() -runserver.args = '[--noreload] [optional port number, or ipaddr:port]' +runserver.args = '[--noreload] [--adminmedia=ADMIN_MEDIA_PATH] [optional port number, or ipaddr:port]' def createcachetable(tablename): "Creates the table needed to use the SQL cache backend" @@ -1121,7 +1127,8 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None): help='Tells Django to NOT use the auto-reloader when running the development server.') parser.add_option('--verbosity', action='store', dest='verbosity', default='2', type='choice', choices=['0', '1', '2'], - help='Verbosity level; 0=minimal output, 1=normal output, 2=all output') + help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'), + parser.add_option('--adminmedia', dest='admin_media_path', default='', help='Lets you manually specify the directory to serve admin media from when running the development server.'), options, args = parser.parse_args(argv[1:]) @@ -1185,11 +1192,12 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None): addr, port = args[1].split(':') except ValueError: addr, port = '', args[1] - action_mapping[action](addr, port, options.use_reloader) + action_mapping[action](addr, port, options.use_reloader, options.admin_media_path) elif action == 'runfcgi': action_mapping[action](args[1:]) else: from django.db import models + validate(silent_success=True) try: mod_list = [models.get_app(app_label) for app_label in args[1:]] except ImportError, e: diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index 72234a624b..15770f160e 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -16,7 +16,7 @@ class Serializer(PythonSerializer): Convert a queryset to JSON. """ def end_serialization(self): - simplejson.dump(self.objects, self.stream, cls=DateTimeAwareJSONEncoder) + simplejson.dump(self.objects, self.stream, cls=DateTimeAwareJSONEncoder, **self.options) def getvalue(self): return self.stream.getvalue() diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 4bd0e50e53..fe534d5da0 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -594,11 +594,14 @@ class AdminMediaHandler(object): Use this ONLY LOCALLY, for development! This hasn't been tested for security and is not super efficient. """ - def __init__(self, application): + def __init__(self, application, media_dir = None): from django.conf import settings - import django self.application = application - self.media_dir = django.__path__[0] + '/contrib/admin/media' + if not media_dir: + import django + self.media_dir = django.__path__[0] + '/contrib/admin/media' + else: + self.media_dir = media_dir self.media_url = settings.ADMIN_MEDIA_PREFIX def __call__(self, environ, start_response): diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py index 7377bed1c5..c6507fe173 100644 --- a/django/core/servers/fastcgi.py +++ b/django/core/servers/fastcgi.py @@ -74,7 +74,7 @@ def fastcgi_help(message=None): print message return False -def runfastcgi(argset, **kwargs): +def runfastcgi(argset=[], **kwargs): options = FASTCGI_OPTIONS.copy() options.update(kwargs) for x in argset: diff --git a/django/core/validators.py b/django/core/validators.py index 8f40ceb51a..705425ea18 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -227,9 +227,8 @@ def hasNoProfanities(field_data, all_data): catch 'motherfucker' as well. Raises a ValidationError such as: Watch your mouth! The words "f--k" and "s--t" are not allowed here. """ - bad_words = ['asshat', 'asshead', 'asshole', 'cunt', 'fuck', 'gook', 'nigger', 'shit'] # all in lower case field_data = field_data.lower() # normalize - words_seen = [w for w in bad_words if field_data.find(w) > -1] + words_seen = [w for w in settings.PROFANITIES_LIST if field_data.find(w) > -1] if words_seen: from django.utils.text import get_text_list plural = len(words_seen) > 1 @@ -352,10 +351,12 @@ class IsValidFloat(object): float(data) except ValueError: raise ValidationError, gettext("Please enter a valid decimal number.") - if len(data) > (self.max_digits + 1): + # Negative floats require more space to input. + max_allowed_length = data.startswith('-') and (self.max_digits + 2) or (self.max_digits + 1) + if len(data) > max_allowed_length: raise ValidationError, ngettext("Please enter a valid decimal number with at most %s total digit.", "Please enter a valid decimal number with at most %s total digits.", self.max_digits) % self.max_digits - if (not '.' in data and len(data) > (self.max_digits - self.decimal_places)) or ('.' in data and len(data) > (self.max_digits - (self.decimal_places - len(data.split('.')[1])) + 1)): + if (not '.' in data and len(data) > (max_allowed_length - self.decimal_places)) or ('.' in data and len(data) > (self.max_digits - (self.decimal_places - len(data.split('.')[1])) + 1)): raise ValidationError, ngettext( "Please enter a valid decimal number with a whole part of at most %s digit.", "Please enter a valid decimal number with a whole part of at most %s digits.", str(self.max_digits-self.decimal_places)) % str(self.max_digits-self.decimal_places) if '.' in data and len(data.split('.')[1]) > self.decimal_places: diff --git a/django/core/xheaders.py b/django/core/xheaders.py index e173bcbca8..69f6115839 100644 --- a/django/core/xheaders.py +++ b/django/core/xheaders.py @@ -13,9 +13,10 @@ def populate_xheaders(request, response, model, object_id): """ Adds the "X-Object-Type" and "X-Object-Id" headers to the given HttpResponse according to the given model and object_id -- but only if the - given HttpRequest object has an IP address within the INTERNAL_IPS setting. + given HttpRequest object has an IP address within the INTERNAL_IPS setting + or if the request is from a logged in staff member. """ from django.conf import settings - if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: + if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or (request.user.is_authenticated() and request.user.is_staff): response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.object_name.lower()) response['X-Object-Id'] = str(object_id) diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 88318941c8..3ec1b41485 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -110,9 +110,11 @@ def dictfetchone(cursor): def dictfetchmany(cursor, number): "Returns a certain number of rows from a cursor as a dict" desc = cursor.description - return [_dict_helper(desc, row) for row in cursor.fetchmany(number)] + for row in cursor.fetchmany(number): + yield _dict_helper(desc, row) def dictfetchall(cursor): "Returns all rows from a cursor as a dict" desc = cursor.description - return [_dict_helper(desc, row) for row in cursor.fetchall()] + for row in cursor.fetchall(): + yield _dict_helper(desc, row) diff --git a/django/db/models/fields/generic.py b/django/db/models/fields/generic.py index 5f4de40e69..7d7651029c 100644 --- a/django/db/models/fields/generic.py +++ b/django/db/models/fields/generic.py @@ -117,7 +117,7 @@ class GenericRelation(RelatedField, Field): return self.object_id_field_name def m2m_reverse_name(self): - return self.model._meta.pk.attname + return self.object_id_field_name def contribute_to_class(self, cls, name): super(GenericRelation, self).contribute_to_class(cls, name) diff --git a/django/forms/__init__.py b/django/forms/__init__.py index 730f7a54da..537a109527 100644 --- a/django/forms/__init__.py +++ b/django/forms/__init__.py @@ -434,11 +434,11 @@ class HiddenField(FormField): (self.get_id(), self.field_name, escape(data)) class CheckboxField(FormField): - def __init__(self, field_name, checked_by_default=False, validator_list=None): + def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False): if validator_list is None: validator_list = [] self.field_name = field_name self.checked_by_default = checked_by_default - self.is_required = False # because the validator looks for these + self.is_required = is_required self.validator_list = validator_list[:] def render(self, data): @@ -639,8 +639,8 @@ class CheckboxSelectMultipleField(SelectMultipleField): checked_html = ' checked="checked"' field_name = '%s%s' % (self.field_name, value) output.append('
  • ' % \ - (self.get_id() + value , self.__class__.__name__, field_name, checked_html, - self.get_id() + value, choice)) + (self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html, + self.get_id() + escape(value), choice)) output.append('') return '\n'.join(output) @@ -743,7 +743,7 @@ class FloatField(TextField): if validator_list is None: validator_list = [] self.max_digits, self.decimal_places = max_digits, decimal_places validator_list = [self.isValidFloat] + validator_list - TextField.__init__(self, field_name, max_digits+1, max_digits+1, is_required, validator_list) + TextField.__init__(self, field_name, max_digits+2, max_digits+2, is_required, validator_list) def isValidFloat(self, field_data, all_data): v = validators.IsValidFloat(self.max_digits, self.decimal_places) @@ -952,10 +952,7 @@ class USStateField(TextField): raise validators.CriticalValidationError, e.messages def html2python(data): - if data: - return data.upper() # Should always be stored in upper case - else: - return None + return data.upper() # Should always be stored in upper case html2python = staticmethod(html2python) class CommaSeparatedIntegerField(TextField): @@ -972,9 +969,19 @@ class CommaSeparatedIntegerField(TextField): except validators.ValidationError, e: raise validators.CriticalValidationError, e.messages + def render(self, data): + if data is None: + data = '' + elif isinstance(data, (list, tuple)): + data = ','.join(data) + return super(CommaSeparatedIntegerField, self).render(data) + class RawIdAdminField(CommaSeparatedIntegerField): def html2python(data): - return data.split(',') + if data: + return data.split(',') + else: + return [] html2python = staticmethod(html2python) class XMLLargeTextField(LargeTextField): diff --git a/django/http/__init__.py b/django/http/__init__.py index c4ac302ec5..bb0e973aae 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -161,10 +161,10 @@ class HttpResponse(object): if not mimetype: mimetype = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, settings.DEFAULT_CHARSET) if hasattr(content, '__iter__'): - self._iterator = content + self._container = content self._is_string = False else: - self._iterator = [content] + self._container = [content] self._is_string = True self.headers = {'Content-Type': mimetype} self.cookies = SimpleCookie() @@ -213,32 +213,37 @@ class HttpResponse(object): self.cookies[key]['max-age'] = 0 def _get_content(self): - content = ''.join(self._iterator) + content = ''.join(self._container) if isinstance(content, unicode): content = content.encode(self._charset) return content def _set_content(self, value): - self._iterator = [value] + self._container = [value] self._is_string = True content = property(_get_content, _set_content) - def _get_iterator(self): - "Output iterator. Converts data into client charset if necessary." - for chunk in self._iterator: - if isinstance(chunk, unicode): - chunk = chunk.encode(self._charset) - yield chunk + def __iter__(self): + self._iterator = self._container.__iter__() + return self - iterator = property(_get_iterator) + def next(self): + chunk = self._iterator.next() + if isinstance(chunk, unicode): + chunk = chunk.encode(self._charset) + return chunk + + def close(self): + if hasattr(self._container, 'close'): + self._container.close() # The remaining methods partially implement the file-like object interface. # See http://docs.python.org/lib/bltin-file-objects.html def write(self, content): if not self._is_string: raise Exception, "This %s instance is not writable" % self.__class__ - self._iterator.append(content) + self._container.append(content) def flush(self): pass @@ -246,7 +251,7 @@ class HttpResponse(object): def tell(self): if not self._is_string: raise Exception, "This %s instance cannot tell its position" % self.__class__ - return sum([len(chunk) for chunk in self._iterator]) + return sum([len(chunk) for chunk in self._container]) class HttpResponseRedirect(HttpResponse): def __init__(self, redirect_to): diff --git a/django/middleware/common.py b/django/middleware/common.py index d63b71fed7..4f060b8590 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -64,8 +64,9 @@ class CommonMiddleware(object): is_internal = referer and (domain in referer) path = request.get_full_path() if referer and not _is_ignorable_404(path) and (is_internal or '?' not in referer): + ua = request.META.get('HTTP_USER_AGENT','') mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain), - "Referrer: %s\nRequested URL: %s\n" % (referer, request.get_full_path())) + "Referrer: %s\nRequested URL: %s\nUser Agent: %s\n" % (referer, request.get_full_path(), ua)) return response # Use ETags, if requested. diff --git a/django/middleware/doc.py b/django/middleware/doc.py index 6600e588cd..48c155c392 100644 --- a/django/middleware/doc.py +++ b/django/middleware/doc.py @@ -7,11 +7,12 @@ class XViewMiddleware(object): """ def process_view(self, request, view_func, view_args, view_kwargs): """ - If the request method is HEAD and the IP is internal, quickly return - with an x-header indicating the view function. This is used by the - documentation module to lookup the view function for an arbitrary page. + If the request method is HEAD and either the IP is internal or the + user is a logged-in staff member, quickly return with an x-header + indicating the view function. This is used by the documentation module + to lookup the view function for an arbitrary page. """ - if request.method == 'HEAD' and request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS: + if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or (request.user.is_authenticated() and request.user.is_staff)): response = http.HttpResponse() response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) return response diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index a2e9d2f405..cf1d3d5f6d 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -15,7 +15,7 @@ register = Library() def addslashes(value): "Adds slashes - useful for passing strings to JavaScript, for example." - return value.replace('"', '\\"').replace("'", "\\'") + return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") def capfirst(value): "Capitalizes the first character of the value" diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index e8a58824dc..f7585368d1 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -13,14 +13,18 @@ class CommentNode(Node): return '' class CycleNode(Node): - def __init__(self, cyclevars): + def __init__(self, cyclevars, variable_name=None): self.cyclevars = cyclevars self.cyclevars_len = len(cyclevars) self.counter = -1 + self.variable_name = variable_name def render(self, context): self.counter += 1 - return self.cyclevars[self.counter % self.cyclevars_len] + value = self.cyclevars[self.counter % self.cyclevars_len] + if self.variable_name: + context[self.variable_name] = value + return value class DebugNode(Node): def render(self, context): @@ -125,6 +129,8 @@ class IfChangedNode(Node): self._last_seen = None 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: firstloop = (self._last_seen == None) @@ -385,7 +391,7 @@ def cycle(parser, token): raise TemplateSyntaxError("Second 'cycle' argument must be 'as'") cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks name = args[3] - node = CycleNode(cyclevars) + node = CycleNode(cyclevars, name) if not hasattr(parser, '_namedCycleNodes'): parser._namedCycleNodes = {} diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 6aef313d35..cecb4da170 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -14,6 +14,9 @@ class MergeDict(object): pass raise KeyError + def __contains__(self, key): + return self.has_key(key) + def get(self, key, default): try: return self[key] diff --git a/django/utils/text.py b/django/utils/text.py index 7df9bc03b7..9e7bb3b6c4 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -94,7 +94,8 @@ def compress_string(s): return zbuf.getvalue() ustring_re = re.compile(u"([\u0080-\uffff])") -def javascript_quote(s): + +def javascript_quote(s, quote_double_quotes=False): def fix(match): return r"\u%04x" % ord(match.group(1)) @@ -104,9 +105,12 @@ def javascript_quote(s): elif type(s) != unicode: raise TypeError, s s = s.replace('\\', '\\\\') + s = s.replace('\r', '\\r') s = s.replace('\n', '\\n') s = s.replace('\t', '\\t') s = s.replace("'", "\\'") + if quote_double_quotes: + s = s.replace('"', '"') return str(ustring_re.sub(fix, s)) smart_split_re = re.compile('("(?:[^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'(?:[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'|[^\\s]+)') diff --git a/docs/authentication.txt b/docs/authentication.txt index f161e9d357..31a894512a 100644 --- a/docs/authentication.txt +++ b/docs/authentication.txt @@ -456,6 +456,10 @@ As a shortcut, you can use the convenient ``user_passes_test`` decorator:: # ... my_view = user_passes_test(lambda u: u.has_perm('polls.can_vote'))(my_view) +We are using this particular test as a relatively simple example, however be +aware that if you just want to test if a permission is available to a user, +you can use the ``permission_required()`` decorator described below. + Here's the same thing, using Python 2.4's decorator syntax:: from django.contrib.auth.decorators import user_passes_test @@ -488,6 +492,24 @@ Example in Python 2.4 syntax:: def my_view(request): # ... +The permission_required decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since checking whether a user has a particular permission available to them is a +relatively common operation, Django provides a shortcut for that particular +case: the ``permission_required()`` decorator. Using this decorator, the +earlier example can be written as:: + + from django.contrib.auth.decorators import permission_required + + def my_view(request): + # ... + + my_view = permission_required('polls.can_vote')(my_view) + +Note that ``permission_required()`` also takes an optional ``login_url`` +parameter. + Limiting access to generic views -------------------------------- @@ -677,7 +699,7 @@ timestamps. Messages are used by the Django admin after successful actions. For example, ``"The poll Foo was created successfully."`` is a message. -The API is simple:: +The API is simple: * To create a new message, use ``user_obj.message_set.create(message='message_text')``. diff --git a/docs/contributing.txt b/docs/contributing.txt index 3d101c3241..7ecda7425c 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -247,18 +247,23 @@ Django tarball. It's our policy to make sure all tests pass at all times. The tests cover: - * Models and the database API (``tests/testapp/models``). - * The cache system (``tests/otherthests/cache.py``). - * The ``django.utils.dateformat`` module (``tests/othertests/dateformat.py``). - * Database typecasts (``tests/othertests/db_typecasts.py``). - * The template system (``tests/othertests/templates.py`` and - ``tests/othertests/defaultfilters.py``). - * ``QueryDict`` objects (``tests/othertests/httpwrappers.py``). - * Markup template tags (``tests/othertests/markup.py``). - * The ``django.utils.timesince`` module (``tests/othertests/timesince.py``). + * Models and the database API (``tests/modeltests/``). + * The cache system (``tests/regressiontests/cache.py``). + * The ``django.utils.dateformat`` module (``tests/regressiontests/dateformat/``). + * Database typecasts (``tests/regressiontests/db_typecasts/``). + * The template system (``tests/regressiontests/templates/`` and + ``tests/regressiontests/defaultfilters/``). + * ``QueryDict`` objects (``tests/regressiontests/httpwrappers/``). + * Markup template tags (``tests/regressiontests/markup/``). We appreciate any and all contributions to the test suite! +The Django tests all use the testing infrastructure that ships with Django for +testing applications. See `Testing Django Applications`_ for an explanation of +how to write new tests. + +.. _Testing Django Applications: http://www.djangoproject.com/documentation/testing/ + Running the unit tests ---------------------- @@ -268,10 +273,14 @@ To run the tests, ``cd`` to the ``tests/`` directory and type:: Yes, the unit tests need a settings module, but only for database connection info -- the ``DATABASE_ENGINE``, ``DATABASE_USER`` and ``DATABASE_PASSWORD``. +You will also need a ``ROOT_URLCONF`` setting (it's value is ignored; it just +needs to be present) and a ``SITE_ID`` setting (any integer value will do) in +order for all the tests to pass. -The unit tests will not touch your database; they create a new database, called -``django_test_db``, which is deleted when the tests are finished. This means -your user account needs permission to execute ``CREATE DATABASE``. +The unit tests will not touch your existing databases; they create a new +database, called ``django_test_db``, which is deleted when the tests are +finished. This means your user account needs permission to execute ``CREATE +DATABASE``. Requesting features =================== diff --git a/docs/db-api.txt b/docs/db-api.txt index bd178dbd7d..7800ff324a 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -1511,7 +1511,7 @@ Many-to-many relationships -------------------------- Both ends of a many-to-many relationship get automatic API access to the other -end. The API works just as a "backward" one-to-many relationship. See _Backward +end. The API works just as a "backward" one-to-many relationship. See Backward_ above. The only difference is in the attribute naming: The model that defines the diff --git a/docs/django-admin.txt b/docs/django-admin.txt index ffafc83972..ed162f0520 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -352,8 +352,9 @@ options. **New in Django development version** -Inform django-admin that the user should NOT be prompted for any input. Useful if -the django-admin script will be executed as an unattended, automated script. +Inform django-admin that the user should NOT be prompted for any input. Useful +if the django-admin script will be executed as an unattended, automated +script. --noreload ---------- @@ -383,6 +384,19 @@ Verbosity determines the amount of notification and debug information that will be printed to the console. '0' is no output, '1' is normal output, and `2` is verbose output. +--adminmedia +------------ + +**New in Django development version** + +Example usage:: + django-admin.py manage.py --adminmedia=/tmp/new-admin-style/ + +Tell Django where to find the various stylesheets and Javascript files for the +admin interface when running the development server. Normally these files are +served out of the Django source tree, but since some designers change these +files for their site, this option allows you to test against custom versions. + Extra niceties ============== diff --git a/docs/forms.txt b/docs/forms.txt index d6ef6f791b..0ffb0bdcb7 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -136,7 +136,7 @@ template:: {% endblock %} Before we get back to the problems with these naive set of views, let's go over -some salient points of the above template:: +some salient points of the above template: * Field "widgets" are handled for you: ``{{ form.field }}`` automatically creates the "right" type of widget for the form, as you can see with the @@ -148,8 +148,8 @@ some salient points of the above template:: If you must use tables, use tables. If you're a semantic purist, you can probably find better HTML than in the above template. - * To avoid name conflicts, the ``id``s of form elements take the form - "id_*fieldname*". + * To avoid name conflicts, the ``id`` values of form elements take the + form "id_*fieldname*". By creating a creation form we've solved problem number 3 above, but we still don't have any validation. Let's revise the validation issue by writing a new @@ -481,6 +481,33 @@ the data being validated. Also, because consistency in user interfaces is important, we strongly urge you to put punctuation at the end of your validation messages. +When Are Validators Called? +--------------------------- + +After a form has been submitted, Django first checks to see that all the +required fields are present and non-empty. For each field that passes that +test *and if the form submission contained data* for that field, all the +validators for that field are called in turn. The emphasised portion in the +last sentence is important: if a form field is not submitted (because it +contains no data -- which is normal HTML behaviour), the validators are not +run against the field. + +This feature is particularly important for models using +``models.BooleanField`` or custom manipulators using things like +``forms.CheckBoxField``. If the checkbox is not selected, it will not +contribute to the form submission. + +If you would like your validator to *always* run, regardless of whether the +field it is attached to contains any data, set the ``always_test`` attribute +on the validator function. For example:: + + def my_custom_validator(field_data, all_data): + # ... + + my_custom_validator.always_test = True + +This validator will always be executed for any field it is attached to. + Ready-made Validators --------------------- diff --git a/docs/model-api.txt b/docs/model-api.txt index b46a11c463..c6c4200239 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -543,7 +543,9 @@ The default value for the field. ``editable`` ~~~~~~~~~~~~ -If ``False``, the field will not be editable in the admin. Default is ``True``. +If ``False``, the field will not be editable in the admin or via form +processing using the object's ``AddManipulator`` or ``ChangeManipulator`` +classes. Default is ``True``. ``help_text`` ~~~~~~~~~~~~~ diff --git a/docs/serialization.txt b/docs/serialization.txt index 25199e7a50..694e2d25db 100644 --- a/docs/serialization.txt +++ b/docs/serialization.txt @@ -96,6 +96,21 @@ Django "ships" with a few included serializers: .. _json: http://json.org/ .. _simplejson: http://undefined.org/python/#simplejson +Notes For Specific Serialization Formats +---------------------------------------- + +json +~~~~ + +If you are using UTF-8 (or any other non-ASCII encoding) data with the JSON +serializer, you must pass ``ensure_ascii=False`` as a parameter to the +``serialize()`` call. Otherwise the output will not be encoded correctly. + +For example:: + + json_serializer = serializers.get_serializer("json") + json_serializer.serialize(queryset, ensure_ascii=False, stream=response) + Writing custom serializers `````````````````````````` diff --git a/docs/settings.txt b/docs/settings.txt index fe2c566aee..6764f01513 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -603,6 +603,12 @@ Whether to prepend the "www." subdomain to URLs that don't have it. This is only used if ``CommonMiddleware`` is installed (see the `middleware docs`_). See also ``APPEND_SLASH``. +PROFANITIES_LIST +---------------- + +A list of profanities that will trigger a validation error when the +``hasNoProfanities`` validator is called. + ROOT_URLCONF ------------ diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 950b122339..bc05d769ad 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -763,17 +763,17 @@ will use the function's name as the tag name. Shortcut for simple tags ~~~~~~~~~~~~~~~~~~~~~~~~ -Many template tags take a single argument -- a string or a template variable -reference -- and return a string after doing some processing based solely on +Many template tags take a number of arguments -- strings or a template variables +-- and return a string after doing some processing based solely on the input argument and some external information. For example, the ``current_time`` tag we wrote above is of this variety: we give it a format string, it returns the time as a string. To ease the creation of the types of tags, Django provides a helper function, ``simple_tag``. This function, which is a method of -``django.template.Library``, takes a function that accepts one argument, wraps -it in a ``render`` function and the other necessary bits mentioned above and -registers it with the template system. +``django.template.Library``, takes a function that accepts any number of +arguments, wraps it in a ``render`` function and the other necessary bits +mentioned above and registers it with the template system. Our earlier ``current_time`` function could thus be written like this:: @@ -789,11 +789,16 @@ In Python 2.4, the decorator syntax also works:: ... A couple of things to note about the ``simple_tag`` helper function: - * Only the (single) argument is passed into our function. * Checking for the required number of arguments, etc, has already been done by the time our function is called, so we don't need to do that. * The quotes around the argument (if any) have already been stripped away, so we just receive a plain string. + * If the argument was a template variable, our function is passed the + current value of the variable, not the variable itself. + +When your template tag does not need access to the current context, writing a +function to work with the input values and using the ``simple_tag`` helper is +the easiest way to create a new tag. Inclusion tags ~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py index 9b1cfda833..32d6ef5202 100644 --- a/tests/regressiontests/defaultfilters/tests.py +++ b/tests/regressiontests/defaultfilters/tests.py @@ -15,6 +15,9 @@ r""" >>> addslashes('"double quotes" and \'single quotes\'') '\\"double quotes\\" and \\\'single quotes\\\'' +>>> addslashes(r'\ : backslashes, too') +'\\\\ : backslashes, too' + >>> capfirst('hello world') 'Hello world' diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 5a8dd2d6a2..368a46b8fb 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -187,6 +187,7 @@ class Templates(unittest.TestCase): 'cycle05': ('{% cycle %}', {}, template.TemplateSyntaxError), 'cycle06': ('{% cycle a %}', {}, template.TemplateSyntaxError), 'cycle07': ('{% cycle a,b,c as foo %}{% cycle bar %}', {}, template.TemplateSyntaxError), + 'cycle08': ('{% cycle a,b,c as foo %}{% cycle foo %}{{ foo }}{{ foo }}{% cycle foo %}{{ foo }}', {}, 'abbbcc'), ### EXCEPTIONS ############################################################ @@ -304,6 +305,10 @@ class Templates(unittest.TestCase): 'ifchanged01': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,2,3) }, '123'), 'ifchanged02': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,3) }, '13'), 'ifchanged03': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% endfor %}', { 'num': (1,1,1) }, '1'), + 'ifchanged04': ('{% for n in num %}{% ifchanged %}{{ n }}{% endifchanged %}{% for x in numx %}{% ifchanged %}{{ x }}{% endifchanged %}{% endfor %}{% endfor %}', { 'num': (1, 2, 3), 'numx': (2, 2, 2)}, '122232'), + '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'), ### IFEQUAL TAG ########################################################### 'ifequal01': ("{% ifequal a b %}yes{% endifequal %}", {"a": 1, "b": 2}, ""),