diff --git a/AUTHORS b/AUTHORS index b55ca93762..a41428f4f5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -84,10 +84,12 @@ answer newbie questions, and generally made Django that much better: Russell Cloran colin@owlfish.com crankycoder@gmail.com + Paul Collier Pete Crosier Matt Croydon flavio.curella@gmail.com Jure Cuhalev + John D'Agostino dackze+django@gmail.com David Danier Dirk Datzert @@ -121,6 +123,7 @@ answer newbie questions, and generally made Django that much better: Afonso Fernández Nogueira Matthew Flanagan Eric Floehr + Vincent Foley Jorge Gajon gandalf@owca.info Marc Garcia @@ -195,7 +198,11 @@ answer newbie questions, and generally made Django that much better: Waylan Limberg limodou Philip Lindborg +<<<<<<< .working Simon Litchfield +======= + msaelices +>>>>>>> .merge-right.r6370 Matt McClanahan Martin Maney Petr Marhoun @@ -262,6 +269,7 @@ answer newbie questions, and generally made Django that much better: Brian Rosner Oliver Rutherfurd ryankanno + Manuel Saelices Ivan Sagalaev (Maniac) Vinay Sajip David Schein diff --git a/django/bin/compile-messages.py b/django/bin/compile-messages.py index 2e1e908bbf..2838cb8aa4 100755 --- a/django/bin/compile-messages.py +++ b/django/bin/compile-messages.py @@ -4,20 +4,31 @@ import optparse import os import sys -def compile_messages(locale=None): - basedir = None +try: + set +except NameError: + from sets import Set as set # For Python 2.3 - if os.path.isdir(os.path.join('conf', 'locale')): - basedir = os.path.abspath(os.path.join('conf', 'locale')) - elif os.path.isdir('locale'): - basedir = os.path.abspath('locale') - else: - print "This script should be run from the Django SVN tree or your project or app tree." + +def compile_messages(locale=None): + basedirs = [os.path.join('conf', 'locale'), 'locale'] + if os.environ.get('DJANGO_SETTINGS_MODULE'): + from django.conf import settings + basedirs += settings.LOCALE_PATHS + + # Gather existing directories. + basedirs = set(map(os.path.abspath, filter(os.path.isdir, basedirs))) + + if not basedirs: + print "This script should be run from the Django SVN tree or your project or app tree, or with the settings module specified." sys.exit(1) - if locale is not None: - basedir = os.path.join(basedir, locale, 'LC_MESSAGES') + for basedir in basedirs: + if locale: + basedir = os.path.join(basedir, locale, 'LC_MESSAGES') + compile_messages_in_dir(basedir) +def compile_messages_in_dir(basedir): for dirpath, dirnames, filenames in os.walk(basedir): for f in filenames: if f.endswith('.po'): @@ -40,9 +51,13 @@ def main(): parser = optparse.OptionParser() parser.add_option('-l', '--locale', dest='locale', help="The locale to process. Default is to process all.") + parser.add_option('--settings', + help='Python path to settings module, e.g. "myproject.settings". If provided, all LOCALE_PATHS will be processed. If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be checked as well.') options, args = parser.parse_args() if len(args): parser.error("This program takes no arguments") + if options.settings: + os.environ['DJANGO_SETTINGS_MODULE'] = options.settings compile_messages(options.locale) if __name__ == "__main__": diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index c8420f3307..b3cbf095c3 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -271,12 +271,14 @@ MIDDLEWARE_CLASSES = ( # SESSIONS # ############ -SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want. -SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seconds (default: 2 weeks). -SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or None for standard domain cookie. -SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only). -SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request. -SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether sessions expire when a user closes his browser. +SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want. +SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seconds (default: 2 weeks). +SESSION_COOKIE_DOMAIN = None # A string like ".lawrence.com", or None for standard domain cookie. +SESSION_COOKIE_SECURE = False # Whether the session cookie should be secure (https:// only). +SESSION_SAVE_EVERY_REQUEST = False # Whether to save the session data on every request. +SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether sessions expire when a user closes his browser. +SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data +SESSION_FILE_PATH = '/tmp/' # Directory to store session files if using the file session module ######### # CACHE # diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 527bbee3f9..70240860d3 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -85,7 +85,7 @@ class AdminBoundField(object): def original_value(self): if self.original: - return self.original.__dict__[self.field.column] + return self.original.__dict__[self.field.attname] def existing_display(self): try: diff --git a/django/contrib/sessions/backends/__init__.py b/django/contrib/sessions/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py new file mode 100644 index 0000000000..382212bb70 --- /dev/null +++ b/django/contrib/sessions/backends/base.py @@ -0,0 +1,143 @@ +import base64 +import md5 +import os +import random +import sys +import time +from django.conf import settings +from django.core.exceptions import SuspiciousOperation + +try: + import cPickle as pickle +except ImportError: + import pickle + +class SessionBase(object): + """ + Base class for all Session classes. + """ + + TEST_COOKIE_NAME = 'testcookie' + TEST_COOKIE_VALUE = 'worked' + + def __init__(self, session_key=None): + self._session_key = session_key + self.accessed = False + self.modified = False + + def __contains__(self, key): + return key in self._session + + def __getitem__(self, key): + return self._session[key] + + def __setitem__(self, key, value): + self._session[key] = value + self.modified = True + + def __delitem__(self, key): + del self._session[key] + self.modified = True + + def keys(self): + return self._session.keys() + + def items(self): + return self._session.items() + + def get(self, key, default=None): + return self._session.get(key, default) + + def pop(self, key, *args): + return self._session.pop(key, *args) + + def set_test_cookie(self): + self[self.TEST_COOKIE_NAME] = self.TEST_COOKIE_VALUE + + def test_cookie_worked(self): + return self.get(self.TEST_COOKIE_NAME) == self.TEST_COOKIE_VALUE + + def delete_test_cookie(self): + del self[self.TEST_COOKIE_NAME] + + def encode(self, session_dict): + "Returns the given session dictionary pickled and encoded as a string." + pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL) + pickled_md5 = md5.new(pickled + settings.SECRET_KEY).hexdigest() + return base64.encodestring(pickled + pickled_md5) + + def decode(self, session_data): + encoded_data = base64.decodestring(session_data) + pickled, tamper_check = encoded_data[:-32], encoded_data[-32:] + if md5.new(pickled + settings.SECRET_KEY).hexdigest() != tamper_check: + raise SuspiciousOperation("User tampered with session cookie.") + try: + return pickle.loads(pickled) + # Unpickling can cause a variety of exceptions. If something happens, + # just return an empty dictionary (an empty session). + except: + return {} + + def _get_new_session_key(self): + "Returns session key that isn't being used." + # The random module is seeded when this Apache child is created. + # Use settings.SECRET_KEY as added salt. + while 1: + session_key = md5.new("%s%s%s%s" % (random.randint(0, sys.maxint - 1), + os.getpid(), time.time(), settings.SECRET_KEY)).hexdigest() + if not self.exists(session_key): + break + return session_key + + def _get_session_key(self): + if self._session_key: + return self._session_key + else: + self._session_key = self._get_new_session_key() + return self._session_key + + def _set_session_key(self, session_key): + self._session_key = session_key + + session_key = property(_get_session_key, _set_session_key) + + def _get_session(self): + # Lazily loads session from storage. + self.accessed = True + try: + return self._session_cache + except AttributeError: + if self._session_key is None: + self._session_cache = {} + else: + self._session_cache = self.load() + return self._session_cache + + _session = property(_get_session) + + # Methods that child classes must implement. + + def exists(self, session_key): + """ + Returns True if the given session_key already exists. + """ + raise NotImplementedError + + def save(self): + """ + Saves the session data. + """ + raise NotImplementedError + + def delete(self, session_key): + """ + Clears out the session data under this key. + """ + raise NotImplementedError + + def load(self): + """ + Loads the session data and returns a dictionary. + """ + raise NotImplementedError + diff --git a/django/contrib/sessions/backends/cache.py b/django/contrib/sessions/backends/cache.py new file mode 100644 index 0000000000..c3e641e691 --- /dev/null +++ b/django/contrib/sessions/backends/cache.py @@ -0,0 +1,26 @@ +from django.conf import settings +from django.contrib.sessions.backends.base import SessionBase +from django.core.cache import cache + +class SessionStore(SessionBase): + """ + A cache-based session store. + """ + def __init__(self, session_key=None): + self._cache = cache + super(SessionStore, self).__init__(session_key) + + def load(self): + session_data = self._cache.get(self.session_key) + return session_data or {} + + def save(self): + self._cache.set(self.session_key, self._session, settings.SESSION_COOKIE_AGE) + + def exists(self, session_key): + if self._cache.get(session_key): + return True + return False + + def delete(self, session_key): + self._cache.delete(session_key) \ No newline at end of file diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py new file mode 100644 index 0000000000..d1496d63bf --- /dev/null +++ b/django/contrib/sessions/backends/db.py @@ -0,0 +1,49 @@ +from django.conf import settings +from django.contrib.sessions.models import Session +from django.contrib.sessions.backends.base import SessionBase +from django.core.exceptions import SuspiciousOperation +import datetime + +class SessionStore(SessionBase): + """ + Implements database session store + """ + def __init__(self, session_key=None): + super(SessionStore, self).__init__(session_key) + + def load(self): + try: + s = Session.objects.get( + session_key = self.session_key, + expire_date__gt=datetime.datetime.now() + ) + return self.decode(s.session_data) + except (Session.DoesNotExist, SuspiciousOperation): + + # Create a new session_key for extra security. + self.session_key = self._get_new_session_key() + self._session_cache = {} + + # Save immediately to minimize collision + self.save() + return {} + + def exists(self, session_key): + try: + Session.objects.get(session_key=session_key) + except Session.DoesNotExist: + return False + return True + + def save(self): + Session.objects.create( + session_key = self.session_key, + session_data = self.encode(self._session), + expire_date = datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE) + ) + + def delete(self, session_key): + try: + Session.objects.get(session_key=session_key).delete() + except Session.DoesNotExist: + pass \ No newline at end of file diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py new file mode 100644 index 0000000000..221db5cc60 --- /dev/null +++ b/django/contrib/sessions/backends/file.py @@ -0,0 +1,68 @@ +import os +from django.conf import settings +from django.contrib.sessions.backends.base import SessionBase +from django.core.exceptions import SuspiciousOperation + +class SessionStore(SessionBase): + """ + Implements a file based session store. + """ + def __init__(self, session_key=None): + self.storage_path = settings.SESSION_FILE_PATH + self.file_prefix = settings.SESSION_COOKIE_NAME + super(SessionStore, self).__init__(session_key) + + def _key_to_file(self, session_key=None): + """ + Get the file associated with this session key. + """ + if session_key is None: + session_key = self.session_key + + # Make sure we're not vulnerable to directory traversal. Session keys + # should always be md5s, so they should never contain directory components. + if os.path.sep in session_key: + raise SuspiciousOperation("Invalid characters (directory components) in session key") + + return os.path.join(self.storage_path, self.file_prefix + session_key) + + def load(self): + session_data = {} + try: + session_file = open(self._key_to_file(), "rb") + try: + try: + session_data = self.decode(session_file.read()) + except(EOFError, SuspiciousOperation): + self._session_key = self._get_new_session_key() + self._session_cache = {} + self.save() + finally: + session_file.close() + except(IOError): + pass + return session_data + + def save(self): + try: + f = open(self._key_to_file(self.session_key), "wb") + try: + f.write(self.encode(self._session)) + finally: + f.close() + except(IOError, EOFError): + pass + + def exists(self, session_key): + if os.path.exists(self._key_to_file(session_key)): + return True + return False + + def delete(self, session_key): + try: + os.unlink(self._key_to_file(session_key)) + except OSError: + pass + + def clean(self): + pass diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py index 2531c8e244..4c3c5acc43 100644 --- a/django/contrib/sessions/middleware.py +++ b/django/contrib/sessions/middleware.py @@ -1,6 +1,4 @@ from django.conf import settings -from django.contrib.sessions.models import Session -from django.core.exceptions import SuspiciousOperation from django.utils.cache import patch_vary_headers from email.Utils import formatdate import datetime @@ -9,73 +7,11 @@ import time TEST_COOKIE_NAME = 'testcookie' TEST_COOKIE_VALUE = 'worked' -class SessionWrapper(object): - def __init__(self, session_key): - self.session_key = session_key - self.accessed = False - self.modified = False - - def __contains__(self, key): - return key in self._session - - def __getitem__(self, key): - return self._session[key] - - def __setitem__(self, key, value): - self._session[key] = value - self.modified = True - - def __delitem__(self, key): - del self._session[key] - self.modified = True - - def keys(self): - return self._session.keys() - - def items(self): - return self._session.items() - - def get(self, key, default=None): - return self._session.get(key, default) - - def pop(self, key, *args): - self.modified = self.modified or key in self._session - return self._session.pop(key, *args) - - def set_test_cookie(self): - self[TEST_COOKIE_NAME] = TEST_COOKIE_VALUE - - def test_cookie_worked(self): - return self.get(TEST_COOKIE_NAME) == TEST_COOKIE_VALUE - - def delete_test_cookie(self): - del self[TEST_COOKIE_NAME] - - def _get_session(self): - # Lazily loads session from storage. - self.accessed = True - try: - return self._session_cache - except AttributeError: - if self.session_key is None: - self._session_cache = {} - else: - try: - s = Session.objects.get(session_key=self.session_key, - expire_date__gt=datetime.datetime.now()) - self._session_cache = s.get_decoded() - except (Session.DoesNotExist, SuspiciousOperation): - self._session_cache = {} - # Set the session_key to None to force creation of a new - # key, for extra security. - self.session_key = None - return self._session_cache - - _session = property(_get_session) - class SessionMiddleware(object): + def process_request(self, request): - request.session = SessionWrapper(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)) + engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) + request.session = engine.SessionStore(request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)) def process_response(self, request, response): # If request.session was modified, or if response.session was set, save @@ -89,25 +25,22 @@ class SessionMiddleware(object): if accessed: patch_vary_headers(response, ('Cookie',)) if modified or settings.SESSION_SAVE_EVERY_REQUEST: - if request.session.session_key: - session_key = request.session.session_key - else: - obj = Session.objects.get_new_session_object() - session_key = obj.session_key - if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE: max_age = None expires = None else: max_age = settings.SESSION_COOKIE_AGE rfcdate = formatdate(time.time() + settings.SESSION_COOKIE_AGE) + # Fixed length date must have '-' separation in the format # DD-MMM-YYYY for compliance with Netscape cookie standard - expires = (rfcdate[:7] + "-" + rfcdate[8:11] - + "-" + rfcdate[12:26] + "GMT") - new_session = Session.objects.save(session_key, request.session._session, - datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)) - response.set_cookie(settings.SESSION_COOKIE_NAME, session_key, + expires = datetime.datetime.strftime(datetime.datetime.utcnow() + \ + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE), "%a, %d-%b-%Y %H:%M:%S GMT") + + # Save the seesion data and refresh the client cookie. + request.session.save() + response.set_cookie(settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, secure=settings.SESSION_COOKIE_SECURE or None) + return response diff --git a/django/contrib/sessions/models.py b/django/contrib/sessions/models.py index fda10c9743..c086396947 100644 --- a/django/contrib/sessions/models.py +++ b/django/contrib/sessions/models.py @@ -1,4 +1,4 @@ -import base64, md5, random, sys, datetime, os, time +import base64, md5, random, sys, datetime import cPickle as pickle from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -74,6 +74,7 @@ class Session(models.Model): session_data = models.TextField(_('session data')) expire_date = models.DateTimeField(_('expire date')) objects = SessionManager() + class Meta: db_table = 'django_session' verbose_name = _('session') diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index e83442123e..cfb475fc49 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -1,35 +1,59 @@ r""" ->>> s = SessionWrapper(None) -Inject data into the session cache. ->>> s._session_cache = {} ->>> s._session_cache['some key'] = 'exists' +>>> from django.contrib.sessions.backends.db import SessionStore as DatabaseSession +>>> from django.contrib.sessions.backends.cache import SessionStore as CacheSession +>>> from django.contrib.sessions.backends.file import SessionStore as FileSession ->>> s.accessed +>>> db_session = DatabaseSession() +>>> db_session.modified False ->>> s.modified -False - ->>> s.pop('non existant key', 'does not exist') +>>> db_session['cat'] = "dog" +>>> db_session.modified +True +>>> db_session.pop('cat') +'dog' +>>> db_session.pop('some key', 'does not exist') 'does not exist' ->>> s.accessed +>>> db_session.save() +>>> db_session.exists(db_session.session_key) True ->>> s.modified +>>> db_session.delete(db_session.session_key) +>>> db_session.exists(db_session.session_key) False ->>> s.pop('some key') -'exists' ->>> s.accessed +>>> file_session = FileSession() +>>> file_session.modified +False +>>> file_session['cat'] = "dog" +>>> file_session.modified True ->>> s.modified -True - ->>> s.pop('some key', 'does not exist') +>>> file_session.pop('cat') +'dog' +>>> file_session.pop('some key', 'does not exist') 'does not exist' +>>> file_session.save() +>>> file_session.exists(file_session.session_key) +True +>>> file_session.delete(file_session.session_key) +>>> file_session.exists(file_session.session_key) +False + +>>> cache_session = CacheSession() +>>> cache_session.modified +False +>>> cache_session['cat'] = "dog" +>>> cache_session.modified +True +>>> cache_session.pop('cat') +'dog' +>>> cache_session.pop('some key', 'does not exist') +'does not exist' +>>> cache_session.save() +>>> cache_session.delete(cache_session.session_key) +>>> cache_session.exists(cache_session.session_key) +False """ -from django.contrib.sessions.middleware import SessionWrapper - if __name__ == '__main__': import doctest doctest.testmod() diff --git a/django/core/context_processors.py b/django/core/context_processors.py index 3c826b1a7d..55c19c17e8 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -13,11 +13,19 @@ def auth(request): """ Returns context variables required by apps that use Django's authentication system. + + If there is no 'user' attribute in the request, uses AnonymousUser (from + django.contrib.auth). """ + if hasattr(request, 'user'): + user = request.user + else: + from django.contrib.auth.models import AnonymousUser + user = AnonymousUser() return { - 'user': request.user, - 'messages': request.user.get_and_delete_messages(), - 'perms': PermWrapper(request.user), + 'user': user, + 'messages': user.get_and_delete_messages(), + 'perms': PermWrapper(user), } def debug(request): diff --git a/django/core/handlers/modpython.py b/django/core/handlers/modpython.py index f98566be96..d4f5e55011 100644 --- a/django/core/handlers/modpython.py +++ b/django/core/handlers/modpython.py @@ -42,8 +42,11 @@ class ModPythonRequest(http.HttpRequest): return '%s%s' % (self.path, self._req.args and ('?' + self._req.args) or '') def is_secure(self): - # Note: modpython 3.2.10+ has req.is_https(), but we need to support previous versions - return 'HTTPS' in self._req.subprocess_env and self._req.subprocess_env['HTTPS'] == 'on' + try: + return self._req.is_https() + except AttributeError: + # mod_python < 3.2.10 doesn't have req.is_https(). + return self._req.subprocess_env.get('HTTPS', '').lower() in ('on', '1') def _load_post_and_files(self): "Populates self._post and self._files" diff --git a/django/db/models/base.py b/django/db/models/base.py index e56c82266b..311732f105 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -83,6 +83,11 @@ class Model(object): def _get_pk_val(self): return getattr(self, self._meta.pk.attname) + def _set_pk_val(self, value): + return setattr(self, self._meta.pk.attname, value) + + pk = property(_get_pk_val, _set_pk_val) + def __repr__(self): return smart_str(u'<%s: %s>' % (self.__class__.__name__, unicode(self))) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index c14f8c3fe3..597271c997 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -878,6 +878,11 @@ class IPAddressField(Field): def validate(self, field_data, all_data): validators.isValidIPAddress4(field_data, None) + def formfield(self, **kwargs): + defaults = {'form_class': forms.IPAddressField} + defaults.update(kwargs) + return super(IPAddressField, self).formfield(**defaults) + class NullBooleanField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): diff --git a/django/middleware/http.py b/django/middleware/http.py index 8db3e4a524..78e066c67b 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -55,6 +55,7 @@ class SetRemoteAddrFromForwardedFor(object): return None else: # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs. - # Take just the first one. - real_ip = real_ip.split(",")[0] + # Take just the last one. + # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/ + real_ip = real_ip.split(",")[-1].strip() request.META['REMOTE_ADDR'] = real_ip diff --git a/django/newforms/fields.py b/django/newforms/fields.py index 8fb1d4f392..a39987e1b5 100644 --- a/django/newforms/fields.py +++ b/django/newforms/fields.py @@ -26,7 +26,7 @@ __all__ = ( 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', 'BooleanField', 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', - 'SplitDateTimeField', + 'SplitDateTimeField', 'IPAddressField', ) # These values, if given to to_python(), will trigger the self.required check. @@ -635,3 +635,11 @@ class SplitDateTimeField(MultiValueField): raise ValidationError(ugettext(u'Enter a valid time.')) return datetime.datetime.combine(*data_list) return None + +ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') + +class IPAddressField(RegexField): + def __init__(self, *args, **kwargs): + RegexField.__init__(self, ipv4_re, + error_message=ugettext(u'Enter a valid IPv4 address.'), + *args, **kwargs) diff --git a/django/newforms/forms.py b/django/newforms/forms.py index 0699e3a5c7..b502fb4de2 100644 --- a/django/newforms/forms.py +++ b/django/newforms/forms.py @@ -63,7 +63,7 @@ class BaseForm(StrAndUnicode): # information. Any improvements to the form API should be made to *this* # class, not to the Form class. def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, - initial=None, error_class=ErrorList): + initial=None, error_class=ErrorList, label_suffix=':'): self.is_bound = data is not None or files is not None self.data = data or {} self.files = files or {} @@ -71,6 +71,7 @@ class BaseForm(StrAndUnicode): self.prefix = prefix self.initial = initial or {} self.error_class = error_class + self.label_suffix = label_suffix self._errors = None # Stores the errors after clean() has been called. # The base_fields class attribute is the *class-wide* definition of @@ -134,9 +135,10 @@ class BaseForm(StrAndUnicode): output.append(error_row % force_unicode(bf_errors)) if bf.label: label = escape(force_unicode(bf.label)) - # Only add a colon if the label does not end in punctuation. - if label[-1] not in ':?.!': - label += ':' + # Only add the suffix if the label does not end in punctuation. + if self.label_suffix: + if label[-1] not in ':?.!': + label += self.label_suffix label = bf.label_tag(label) or '' else: label = '' diff --git a/django/test/client.py b/django/test/client.py index faacc5bf9e..6a05d9dd9c 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -4,8 +4,6 @@ from cStringIO import StringIO from urlparse import urlparse from django.conf import settings from django.contrib.auth import authenticate, login -from django.contrib.sessions.models import Session -from django.contrib.sessions.middleware import SessionWrapper from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest from django.core.signals import got_request_exception @@ -132,9 +130,10 @@ class Client: def _session(self): "Obtain the current session variables" if 'django.contrib.sessions' in settings.INSTALLED_APPS: + engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None) if cookie: - return SessionWrapper(cookie.value) + return engine.SessionStore(cookie.value) return {} session = property(_session) @@ -247,24 +246,23 @@ class Client: """ user = authenticate(**credentials) if user and user.is_active and 'django.contrib.sessions' in settings.INSTALLED_APPS: - obj = Session.objects.get_new_session_object() + engine = __import__(settings.SESSION_ENGINE, {}, {}, ['']) # Create a fake request to store login details request = HttpRequest() - request.session = SessionWrapper(obj.session_key) + request.session = engine.SessionStore() login(request, user) # Set the cookie to represent the session - self.cookies[settings.SESSION_COOKIE_NAME] = obj.session_key + self.cookies[settings.SESSION_COOKIE_NAME] = request.session.session_key self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/' self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None - # Set the session values - Session.objects.save(obj.session_key, request.session._session, - datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)) + # Save the session values + request.session.save() return True else: @@ -275,9 +273,6 @@ class Client: Causes the authenticated user to be logged out. """ - try: - Session.objects.get(session_key=self.cookies['sessionid'].value).delete() - except KeyError: - pass - + session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore() + session.delete(session_key=self.cookies['sessionid'].value) self.cookies = SimpleCookie() diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index ac890d5da6..40e99c3962 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -72,12 +72,23 @@ class SortedDict(dict): def items(self): return zip(self.keyOrder, self.values()) + def iteritems(self): + for key in self.keyOrder: + yield key, dict.__getitem__(self, key) + def keys(self): return self.keyOrder[:] + def iterkeys(self): + return iter(self.keyOrder) + def values(self): return [dict.__getitem__(self, k) for k in self.keyOrder] + def itervalues(self): + for key in self.keyOrder: + yield dict.__getitem__(self, key) + def update(self, dict): for k, v in dict.items(): self.__setitem__(k, v) @@ -91,6 +102,15 @@ class SortedDict(dict): "Returns the value of the item at the given zero-based index." return self[self.keyOrder[index]] + def insert(self, index, key, value): + "Inserts the key, value pair before the item with the given index." + if key in self.keyOrder: + n = self.keyOrder.index(key) + del self.keyOrder[n] + if n < index: index -= 1 + self.keyOrder.insert(index, key) + dict.__setitem__(self, key, value) + def copy(self): "Returns a copy of this object." # This way of initializing the copy means it works for subclasses, too. diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 0e6541c721..9a296c3f2d 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -166,8 +166,8 @@ class DateFormat(TimeFormat): def O(self): "Difference to Greenwich time in hours; e.g. '+0200'" - tz = self.timezone.utcoffset(self.data) - return u"%+03d%02d" % (tz.seconds // 3600, (tz.seconds // 60) % 60) + seconds = self.Z() + return u"%+03d%02d" % (seconds // 3600, (seconds // 60) % 60) def r(self): "RFC 822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'" diff --git a/django/utils/timesince.py b/django/utils/timesince.py index 455788e7d7..e1132c3ab3 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -1,11 +1,20 @@ -import datetime, math, time +import datetime +import time + from django.utils.tzinfo import LocalTimezone from django.utils.translation import ungettext, ugettext def timesince(d, now=None): """ - Takes two datetime objects and returns the time between then and now - as a nicely formatted string, e.g "10 minutes" + Takes two datetime objects and returns the time between d and now + as a nicely formatted string, e.g. "10 minutes". If d occurs after now, + then "0 minutes" is returned. + + Units used are years, months, weeks, days, hours, and minutes. + Seconds and microseconds are ignored. Up to two adjacent units will be + displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are + possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. + Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ chunks = ( @@ -32,6 +41,9 @@ def timesince(d, now=None): # ignore microsecond part of 'd' since we removed it from 'now' delta = now - (d - datetime.timedelta(0, 0, d.microsecond)) since = delta.days * 24 * 60 * 60 + delta.seconds + if since <= 0: + # d is in the future compared to now, stop processing. + return u'0 ' + ugettext('minutes') for i, (seconds, name) in enumerate(chunks): count = since // seconds if count != 0: diff --git a/docs/apache_auth.txt b/docs/apache_auth.txt index 9beb1ba43a..cab57fe6d5 100644 --- a/docs/apache_auth.txt +++ b/docs/apache_auth.txt @@ -34,12 +34,12 @@ with the standard ``Auth*`` and ``Require`` directives:: If you're using Apache 2.2, you'll need to take a couple extra steps. You'll need to ensure that ``mod_auth_basic`` and ``mod_authz_user`` - are loaded. These might be compiled staticly into Apache, or you might + are loaded. These might be compiled statically into Apache, or you might need to use ``LoadModule`` to load them dynamically (as shown in the example at the bottom of this note). You'll also need to insert configuration directives that prevent Apache - from trying to use other authentication modules. Depnding on which other + from trying to use other authentication modules. Depending on which other authentication modules you have loaded, you might need one or more of the following directives:: diff --git a/docs/django-admin.txt b/docs/django-admin.txt index 68fcad24fe..0f99987bad 100644 --- a/docs/django-admin.txt +++ b/docs/django-admin.txt @@ -645,11 +645,11 @@ Examples: To run the test server on port 7000 with ``fixture1`` and ``fixture2``:: django-admin.py testserver --addrport 7000 fixture1 fixture2 - django-admin.py testserver fixture1 fixture2 --addrport 8080 + django-admin.py testserver fixture1 fixture2 --addrport 7000 (The above statements are equivalent. We include both of them to demonstrate -that it doesn't matter whether the options come before or after the -``testserver`` command.) +that it doesn't matter whether the options come before or after the fixture +arguments.) To run on 1.2.3.4:7000 with a `test` fixture:: diff --git a/docs/email.txt b/docs/email.txt index 97bdec0037..17c2b2115a 100644 --- a/docs/email.txt +++ b/docs/email.txt @@ -328,7 +328,7 @@ attribute on the ``EmailMessage`` class to change the main content type. The major type will always be ``"text"``, but you can change it to the subtype. For example:: - msg = EmailMessage(subject, html_content, from_email, to) + msg = EmailMessage(subject, html_content, from_email, [to]) msg.content_subtype = "html" # Main content is now text/html msg.send() diff --git a/docs/i18n.txt b/docs/i18n.txt index 25191e9402..bf73c88008 100644 --- a/docs/i18n.txt +++ b/docs/i18n.txt @@ -669,8 +669,11 @@ To create message files, you use the same ``make-messages.py`` tool as with the Django message files. You only need to be in the right place -- in the directory where either the ``conf/locale`` (in case of the source tree) or the ``locale/`` (in case of app messages or project messages) directory are located. And you -use the same ``compile-messages.py`` to produce the binary ``django.mo`` files that -are used by ``gettext``. +use the same ``compile-messages.py`` to produce the binary ``django.mo`` files +that are used by ``gettext``. + +You can also run ``compile-message.py --settings=path.to.settings`` to make +the compiler process all the directories in your ``LOCALE_PATHS`` setting. Application message files are a bit complicated to discover -- they need the ``LocaleMiddleware``. If you don't use the middleware, only the Django message @@ -710,7 +713,7 @@ language choice in a ``django_language`` cookie. After setting the language choice, Django redirects the user, following this algorithm: - * Django looks for a ``next`` parameter in ``POST`` request. + * Django looks for a ``next`` parameter in the ``POST`` data. * If that doesn't exist, or is empty, Django tries the URL in the ``Referrer`` header. * If that's empty -- say, if a user's browser suppresses that header -- diff --git a/docs/install.txt b/docs/install.txt index 173f4941ee..2de8529d24 100644 --- a/docs/install.txt +++ b/docs/install.txt @@ -86,7 +86,7 @@ to create a temporary test database. .. _SQLite: http://www.sqlite.org/ .. _pysqlite: http://initd.org/tracker/pysqlite .. _MySQL backend: ../databases/ -.. _cx_Oracle: http://www.python.net/crew/atuining/cx_Oracle/ +.. _cx_Oracle: http://cx-oracle.sourceforge.net/ .. _Oracle: http://www.oracle.com/ .. _testing framework: ../testing/ diff --git a/docs/model-api.txt b/docs/model-api.txt index 1f0bb60285..adb9cfceb1 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -1284,6 +1284,17 @@ won't add the automatic ``id`` column. Each model requires exactly one field to have ``primary_key=True``. +The ``pk`` property +------------------- +**New in Django development version** + +Regardless of whether you define a primary key field yourself, or let Django +supply one for you, each model will have a property called ``pk``. It behaves +like a normal attribute on the model, but is actually an alias for whichever +attribute is the primary key field for the model. You can read and set this +value, just as you would for any other attribute, and it will update the +correct field in the model. + Admin options ============= diff --git a/docs/newforms.txt b/docs/newforms.txt index 1c13de0316..6ff96e3b98 100644 --- a/docs/newforms.txt +++ b/docs/newforms.txt @@ -516,6 +516,26 @@ include ``%s`` -- then the library will act as if ``auto_id`` is ``True``. By default, ``auto_id`` is set to the string ``'id_%s'``. +Normally, a colon (``:``) will be appended after any label name when a form is +rendered. It's possible to change the colon to another character, or omit it +entirely, using the ``label_suffix`` parameter:: + + >>> f = ContactForm(auto_id='id_for_%s', label_suffix='') + >>> print f.as_ul() +
  • +
  • +
  • +
  • + >>> f = ContactForm(auto_id='id_for_%s', label_suffix=' ->') + >>> print f.as_ul() +
  • +
  • +
  • +
  • + +Note that the label suffix is added only if the last character of the +label isn't a punctuation character (``.``, ``!``, ``?`` or ``:``) + Notes on field ordering ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1267,6 +1287,15 @@ When you use a ``FileField`` on a form, you must also remember to Takes two optional arguments for validation, ``max_value`` and ``min_value``. These control the range of values permitted in the field. +``IPAddressField`` +~~~~~~~~~~~~~~~~~~ + + * Default widget: ``TextInput`` + * Empty value: ``''`` (an empty string) + * Normalizes to: A Unicode object. + * Validates that the given value is a valid IPv4 address, using a regular + expression. + ``MultipleChoiceField`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1693,7 +1722,7 @@ the full list of conversions: ``ForeignKey`` ``ModelChoiceField`` (see below) ``ImageField`` ``ImageField`` ``IntegerField`` ``IntegerField`` - ``IPAddressField`` ``CharField`` + ``IPAddressField`` ``IPAddressField`` ``ManyToManyField`` ``ModelMultipleChoiceField`` (see below) ``NullBooleanField`` ``CharField`` diff --git a/docs/sessions.txt b/docs/sessions.txt index 96a88c617a..ab7ea56aaa 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -10,18 +10,21 @@ Cookies contain a session ID -- not the data itself. Enabling sessions ================= -Sessions are implemented via a piece of middleware_ and a Django model. +Sessions are implemented via a piece of middleware_. -To enable session functionality, do these two things: +To enable session functionality, do the following: * Edit the ``MIDDLEWARE_CLASSES`` setting and make sure ``MIDDLEWARE_CLASSES`` contains ``'django.contrib.sessions.middleware.SessionMiddleware'``. The default ``settings.py`` created by ``django-admin.py startproject`` has ``SessionMiddleware`` activated. - * Add ``'django.contrib.sessions'`` to your ``INSTALLED_APPS`` setting, and - run ``manage.py syncdb`` to install the single database table that stores - session data. + * Add ``'django.contrib.sessions'`` to your ``INSTALLED_APPS`` setting, + and run ``manage.py syncdb`` to install the single database table + that stores session data. + + **New in development version**: this step is optional if you're not using + the database session backend; see `configuring the session engine`_. If you don't want to use sessions, you might as well remove the ``SessionMiddleware`` line from ``MIDDLEWARE_CLASSES`` and ``'django.contrib.sessions'`` @@ -29,6 +32,44 @@ from your ``INSTALLED_APPS``. It'll save you a small bit of overhead. .. _middleware: ../middleware/ +Configuring the session engine +============================== + +**New in development version**. + +By default, Django stores sessions in your database (using the model +``django.contrib.sessions.models.Session``). Though this is convenient, in +some setups it's faster to store session data elsewhere, so Django can be +configured to store session data on your filesystem or in your cache. + +Using file-based sessions +------------------------- + +To use file-based sessions, set the ``SESSION_ENGINE`` setting to +``"django.contrib.sessions.backends.file"``. + +You might also want to set the ``SESSION_FILE_PATH`` setting (which +defaults to ``/tmp``) to control where Django stores session files. Be +sure to check that your Web server has permissions to read and write to +this location. + +Using cache-based sessions +-------------------------- + +To store session data using Django's cache system, set ``SESSION_ENGINE`` +to ``"django.contrib.sessions.backends.cache"``. You'll want to make sure +you've configured your cache; see the `cache documentation`_ for details. + +.. _cache documentation: ../cache/ + +.. note:: + + You should probably only use cache-based sessions if you're using the + memcached cache backend. The local memory and simple cache backends + don't retain data long enough to be good choices, and it'll be faster + to use file or database sessions directly instead of sending everything + through the file or database cache backends. + Using sessions in views ======================= @@ -153,9 +194,21 @@ Here's a typical usage example:: Using sessions out of views =========================== -Internally, each session is just a normal Django model. The ``Session`` model -is defined in ``django/contrib/sessions/models.py``. Because it's a normal -model, you can access sessions using the normal Django database API:: +**New in Django development version** + +An API is available to manipulate session data outside of a view:: + + >>> from django.contrib.sessions.engines.db import SessionStore + >>> s = SessionStore(session_key='2b1189a188b44ad18c35e113ac6ceead') + >>> s['last_login'] = datetime.datetime(2005, 8, 20, 13, 35, 10) + >>> s['last_login'] + datetime.datetime(2005, 8, 20, 13, 35, 0) + >>> s.save() + +If you're using the ``django.contrib.sessions.engine.db`` backend, each +session is just a normal Django model. The ``Session`` model is defined in +``django/contrib/sessions/models.py``. Because it's a normal model, you can +access sessions using the normal Django database API:: >>> from django.contrib.sessions.models import Session >>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead') @@ -245,6 +298,31 @@ Settings A few `Django settings`_ give you control over session behavior: +SESSION_ENGINE +-------------- + +**New in Django development version** + +Default: ``django.contrib.sessions.backends.db`` + +Controls where Django stores session data. Valid values are: + + * ``'django.contrib.sessions.backends.db'`` + * ``'django.contrib.sessions.backends.file'`` + * ``'django.contrib.sessions.backends.cache'`` + +See `configuring the session engine`_ for more details. + +SESSION_FILE_PATH +----------------- + +**New in Django development version** + +Default: ``/tmp/`` + +If you're using file-based session storage, this sets the directory in +which Django will store session data. + SESSION_COOKIE_AGE ------------------ diff --git a/docs/settings.txt b/docs/settings.txt index 2e6185f444..e40374a822 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -257,10 +257,11 @@ The database backend to use. The build-in database backends are ``'postgresql_psycopg2'``, ``'postgresql'``, ``'mysql'``, ``'mysql_old'``, ``'sqlite3'``, ``'oracle'``, or ``'ado_mssql'``. -You can also use a database backend that doesn't ship with Django by -setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e. +In the Django development version, you can use a database backend that doesn't +ship with Django by setting ``DATABASE_ENGINE`` to a fully-qualified path (i.e. ``mypackage.backends.whatever``). Writing a whole new database backend from -scratch is left as an exercise to the reader. +scratch is left as an exercise to the reader; see the other backends for +examples. DATABASE_HOST ------------- @@ -733,6 +734,21 @@ Default: ``'root@localhost'`` The e-mail address that error messages come from, such as those sent to ``ADMINS`` and ``MANAGERS``. +SESSION_ENGINE +-------------- + +**New in Django development version** + +Default: ``django.contrib.sessions.backends.db`` + +Controls where Django stores session data. Valid values are: + + * ``'django.contrib.sessions.backends.db'`` + * ``'django.contrib.sessions.backends.file'`` + * ``'django.contrib.sessions.backends.cache'`` + +See the `session docs`_ for more details. + SESSION_COOKIE_AGE ------------------ @@ -775,6 +791,17 @@ Default: ``False`` Whether to expire the session when the user closes his or her browser. See the `session docs`_. +SESSION_FILE_PATH +----------------- + +**New in Django development version** + +Default: ``/tmp/`` + +If you're using file-based session storage, this sets the directory in +which Django will store session data. See the `session docs`_ for +more details. + SESSION_SAVE_EVERY_REQUEST -------------------------- diff --git a/docs/templates.txt b/docs/templates.txt index 9adf15731b..cd436a987d 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -1275,17 +1275,23 @@ For example, if ``blog_date`` is a date instance representing midnight on 1 June 2006, and ``comment_date`` is a date instance for 08:00 on 1 June 2006, then ``{{ comment_date|timesince:blog_date }}`` would return "8 hours". +Minutes is the smallest unit used, and "0 minutes" will be returned for any +date that is in the future relative to the comparison point. + timeuntil ~~~~~~~~~ Similar to ``timesince``, except that it measures the time from now until the given date or datetime. For example, if today is 1 June 2006 and ``conference_date`` is a date instance holding 29 June 2006, then -``{{ conference_date|timeuntil }}`` will return "28 days". +``{{ conference_date|timeuntil }}`` will return "4 weeks". Takes an optional argument that is a variable containing the date to use as the comparison point (instead of *now*). If ``from_date`` contains 22 June -2006, then ``{{ conference_date|timeuntil:from_date }}`` will return "7 days". +2006, then ``{{ conference_date|timeuntil:from_date }}`` will return "1 week". + +Minutes is the smallest unit used, and "0 minutes" will be returned for any +date that is in the past relative to the comparison point. title ~~~~~ diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py index 0a09579761..58770ef2ce 100644 --- a/tests/modeltests/basic/models.py +++ b/tests/modeltests/basic/models.py @@ -33,6 +33,11 @@ __test__ = {'API_TESTS': """ >>> a.id 1L +# Models have a pk property that is an alias for the primary key attribute (by +# default, the 'id' attribute). +>>> a.pk +1L + # Access database columns via Python attributes. >>> a.headline 'Area man programs in Python' diff --git a/tests/modeltests/custom_pk/models.py b/tests/modeltests/custom_pk/models.py index 375859c897..381d81b987 100644 --- a/tests/modeltests/custom_pk/models.py +++ b/tests/modeltests/custom_pk/models.py @@ -56,6 +56,15 @@ DoesNotExist: Employee matching query does not exist. >>> Employee.objects.filter(pk__in=['ABC123','XYZ456']) [, ] +# The primary key can be accessed via the pk property on the model. +>>> e = Employee.objects.get(pk='ABC123') +>>> e.pk +u'ABC123' + +# Or we can use the real attribute name for the primary key: +>>> e.employee_code +u'ABC123' + # Fran got married and changed her last name. >>> fran = Employee.objects.get(pk='XYZ456') >>> fran.last_name = 'Jones' diff --git a/tests/modeltests/fixtures/models.py b/tests/modeltests/fixtures/models.py index a1e2446e56..5b53115733 100644 --- a/tests/modeltests/fixtures/models.py +++ b/tests/modeltests/fixtures/models.py @@ -9,6 +9,7 @@ FIXTURE_DIRS setting. """ from django.db import models +from django.conf import settings class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') @@ -53,7 +54,13 @@ __test__ = {'API_TESTS': """ # object list is unaffected >>> Article.objects.all() [, , , , ] +"""} +# Database flushing does not work on MySQL with the default storage engine +# because it requires transaction support. +if settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): + __test__['API_TESTS'] += \ +""" # Reset the database representation of this app. This will delete all data. >>> management.call_command('flush', verbosity=0, interactive=False) >>> Article.objects.all() @@ -75,7 +82,7 @@ Multiple fixtures named 'fixture2' in '...fixtures'. Aborting. # Dump the current contents of the database as a JSON fixture >>> management.call_command('dumpdata', 'fixtures', format='json') [{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16 12:00:00"}}, {"pk": 1, "model": "fixtures.article", "fields": {"headline": "Python program becomes self aware", "pub_date": "2006-06-16 11:00:00"}}] -"""} +""" from django.test import TestCase diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index a455f21e67..db16d2c198 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db import connection class Square(models.Model): root = models.IntegerField() @@ -7,18 +8,27 @@ class Square(models.Model): def __unicode__(self): return "%s ** 2 == %s" % (self.root, self.square) +if connection.features.uses_case_insensitive_names: + t_convert = lambda x: x.upper() +else: + t_convert = lambda x: x +qn = connection.ops.quote_name + __test__ = {'API_TESTS': """ #4896: Test cursor.executemany >>> from django.db import connection >>> cursor = connection.cursor() ->>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', -... [(i, i**2) for i in range(-5, 6)]) and None or None +>>> opts = Square._meta +>>> f1, f2 = opts.get_field('root'), opts.get_field('square') +>>> query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' +... % (t_convert(opts.db_table), qn(f1.column), qn(f2.column))) +>>> cursor.executemany(query, [(i, i**2) for i in range(-5, 6)]) and None or None >>> Square.objects.order_by('root') [, , , , , , , , , , ] #4765: executemany with params=[] does nothing ->>> cursor.executemany('INSERT INTO BACKENDS_SQUARE (ROOT, SQUARE) VALUES (%s, %s)', []) and None or None +>>> cursor.executemany(query, []) and None or None >>> Square.objects.count() 11 diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index 104557be34..e14c4e1ae0 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -2945,6 +2945,37 @@ is default behavior.
  • + +# Label Suffix ################################################################ + +You can specify the 'label_suffix' argument to a Form class to modify the +punctuation symbol used at the end of a label. By default, the colon (:) is +used, and is only appended to the label if the label doesn't already end with a +punctuation symbol: ., !, ? or :. If you specify a different suffix, it will +be appended regardless of the last character of the label. + +>>> class FavoriteForm(Form): +... color = CharField(label='Favorite color?') +... animal = CharField(label='Favorite animal') +... +>>> f = FavoriteForm(auto_id=False) +>>> print f.as_ul() +
  • Favorite color?
  • +
  • Favorite animal:
  • +>>> f = FavoriteForm(auto_id=False, label_suffix='?') +>>> print f.as_ul() +
  • Favorite color?
  • +
  • Favorite animal?
  • +>>> f = FavoriteForm(auto_id=False, label_suffix='') +>>> print f.as_ul() +
  • Favorite color?
  • +
  • Favorite animal
  • +>>> f = FavoriteForm(auto_id=False, label_suffix=u'\u2192') +>>> f.as_ul() +u'
  • Favorite color?
  • \n
  • Favorite animal\u2192
  • ' + + + # Initial data ################################################################ You can specify initial data for a field by using the 'initial' argument to a @@ -3805,6 +3836,61 @@ ValidationError: [u'This field is required.'] >>> f.cleaned_data {'field1': u'some text,JP,2007-04-25 06:24:00'} + +# IPAddressField ################################################################## + +>>> f = IPAddressField() +>>> f.clean('') +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean(None) +Traceback (most recent call last): +... +ValidationError: [u'This field is required.'] +>>> f.clean('127.0.0.1') +u'127.0.0.1' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('127.0.0.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('1.2.3.4.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('256.125.1.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] + +>>> f = IPAddressField(required=False) +>>> f.clean('') +u'' +>>> f.clean(None) +u'' +>>> f.clean('127.0.0.1') +u'127.0.0.1' +>>> f.clean('foo') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('127.0.0.') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('1.2.3.4.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] +>>> f.clean('256.125.1.5') +Traceback (most recent call last): +... +ValidationError: [u'Enter a valid IPv4 address.'] + ################################# # Tests of underlying functions # ################################# diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 11dd092156..0c851d5f78 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -771,6 +771,10 @@ class Templates(unittest.TestCase): # Check that timezone is respected 'timesince06' : ('{{ a|timesince:b }}', {'a':NOW_tz + timedelta(hours=8), 'b':NOW_tz}, '8 hours'), + # Check times in the future. + 'timesince07' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=1, seconds=10)}, '0 minutes'), + 'timesince08' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(days=1, minutes=1)}, '0 minutes'), + ### TIMEUNTIL TAG ################################################## # Default compare with datetime.now() 'timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), @@ -781,6 +785,10 @@ class Templates(unittest.TestCase): 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'), 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'), + # Check times in the past. + 'timeuntil07' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(minutes=1, seconds=10)}, '0 minutes'), + 'timeuntil08' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(days=1, minutes=1)}, '0 minutes'), + ### URL TAG ######################################################## # Successes 'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index 258aea697e..fe0b226adc 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -6,6 +6,8 @@ from unittest import TestCase from django.utils import html +from timesince import timesince_tests + class TestUtilsHtml(TestCase): def check_output(self, function, value, output=None): @@ -113,3 +115,11 @@ class TestUtilsHtml(TestCase): ) for value, output in items: self.check_output(f, value, output) + +__test__ = { + 'timesince_tests': timesince_tests, +} + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/tests/regressiontests/utils/timesince.py b/tests/regressiontests/utils/timesince.py new file mode 100644 index 0000000000..30200be6e9 --- /dev/null +++ b/tests/regressiontests/utils/timesince.py @@ -0,0 +1,77 @@ +timesince_tests = """ +>>> from datetime import datetime, timedelta +>>> from django.utils.timesince import timesince + +>>> t = datetime(2007, 8, 14, 13, 46, 0) + +>>> onemicrosecond = timedelta(microseconds=1) +>>> onesecond = timedelta(seconds=1) +>>> oneminute = timedelta(minutes=1) +>>> onehour = timedelta(hours=1) +>>> oneday = timedelta(days=1) +>>> oneweek = timedelta(days=7) +>>> onemonth = timedelta(days=30) +>>> oneyear = timedelta(days=365) + +# equal datetimes. +>>> timesince(t, t) +u'0 minutes' + +# Microseconds and seconds are ignored. +>>> timesince(t, t+onemicrosecond) +u'0 minutes' +>>> timesince(t, t+onesecond) +u'0 minutes' + +# Test other units. +>>> timesince(t, t+oneminute) +u'1 minute' +>>> timesince(t, t+onehour) +u'1 hour' +>>> timesince(t, t+oneday) +u'1 day' +>>> timesince(t, t+oneweek) +u'1 week' +>>> timesince(t, t+onemonth) +u'1 month' +>>> timesince(t, t+oneyear) +u'1 year' + +# Test multiple units. +>>> timesince(t, t+2*oneday+6*onehour) +u'2 days, 6 hours' +>>> timesince(t, t+2*oneweek+2*oneday) +u'2 weeks, 2 days' + +# If the two differing units aren't adjacent, only the first unit is displayed. +>>> timesince(t, t+2*oneweek+3*onehour+4*oneminute) +u'2 weeks' +>>> timesince(t, t+4*oneday+5*oneminute) +u'4 days' + +# When the second date occurs before the first, we should always get 0 minutes. +>>> timesince(t, t-onemicrosecond) +u'0 minutes' +>>> timesince(t, t-onesecond) +u'0 minutes' +>>> timesince(t, t-oneminute) +u'0 minutes' +>>> timesince(t, t-onehour) +u'0 minutes' +>>> timesince(t, t-oneday) +u'0 minutes' +>>> timesince(t, t-oneweek) +u'0 minutes' +>>> timesince(t, t-onemonth) +u'0 minutes' +>>> timesince(t, t-oneyear) +u'0 minutes' +>>> timesince(t, t-2*oneday-6*onehour) +u'0 minutes' +>>> timesince(t, t-2*oneweek-2*oneday) +u'0 minutes' +>>> timesince(t, t-2*oneweek-3*onehour-4*oneminute) +u'0 minutes' +>>> timesince(t, t-4*oneday-5*oneminute) +u'0 minutes' +""" diff --git a/tests/regressiontests/views/__init__.py b/tests/regressiontests/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/views/fixtures/testdata.json b/tests/regressiontests/views/fixtures/testdata.json new file mode 100644 index 0000000000..2e52fa0989 --- /dev/null +++ b/tests/regressiontests/views/fixtures/testdata.json @@ -0,0 +1,25 @@ +[ + { + "pk": 1, + "model": "views.article", + "fields": { + "author": 1, + "title": "An Article" + } + }, + { + "pk": 1, + "model": "views.author", + "fields": { + "name": "Boris" + } + }, + { + "pk": 1, + "model": "sites.site", + "fields": { + "domain": "testserver", + "name": "testserver" + } + } +] diff --git a/tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.mo b/tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000..7593e1eb48 Binary files /dev/null and b/tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.mo differ diff --git a/tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.po b/tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000..54ee2c4065 --- /dev/null +++ b/tests/regressiontests/views/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 16:45+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "this is to be translated" +msgstr "this is to be translated in english" \ No newline at end of file diff --git a/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo b/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000..01c002e9e0 Binary files /dev/null and b/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.mo differ diff --git a/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po b/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000..03c01b7656 --- /dev/null +++ b/tests/regressiontests/views/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,21 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 16:45+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: media/js/translate.js:1 +msgid "this is to be translated" +msgstr "esto tiene que ser traducido" \ No newline at end of file diff --git a/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo new file mode 100644 index 0000000000..4feca0b44d Binary files /dev/null and b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 0000000000..9cfaa23232 --- /dev/null +++ b/tests/regressiontests/views/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,20 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-15 19:15+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "this is to be translated" +msgstr "il faut le traduire" \ No newline at end of file diff --git a/tests/regressiontests/views/media/file.txt b/tests/regressiontests/views/media/file.txt new file mode 100644 index 0000000000..f1fc82c455 --- /dev/null +++ b/tests/regressiontests/views/media/file.txt @@ -0,0 +1 @@ +An example media file. \ No newline at end of file diff --git a/tests/regressiontests/views/models.py b/tests/regressiontests/views/models.py new file mode 100644 index 0000000000..613050fb16 --- /dev/null +++ b/tests/regressiontests/views/models.py @@ -0,0 +1,24 @@ +""" +Regression tests for Django built-in views +""" + +from django.db import models +from django.conf import settings + +class Author(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return '/views/authors/%s/' % self.id + + +class Article(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author) + + def __unicode__(self): + return self.title + diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py new file mode 100644 index 0000000000..fa766ddd7c --- /dev/null +++ b/tests/regressiontests/views/tests/__init__.py @@ -0,0 +1,3 @@ +from defaults import * +from i18n import * +from static import * \ No newline at end of file diff --git a/tests/regressiontests/views/tests/defaults.py b/tests/regressiontests/views/tests/defaults.py new file mode 100644 index 0000000000..bf490d7cf0 --- /dev/null +++ b/tests/regressiontests/views/tests/defaults.py @@ -0,0 +1,39 @@ +from os import path + +from django.conf import settings +from django.test import TestCase +from django.contrib.contenttypes.models import ContentType + +from regressiontests.views.models import Author, Article + +class DefaultsTests(TestCase): + """Test django views in django/views/defaults.py""" + fixtures = ['testdata.json'] + + def test_shorcut_with_absolute_url(self): + "Can view a shortcut an Author object that has with a get_absolute_url method" + for obj in Author.objects.all(): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) + response = self.client.get(short_url) + self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), + status_code=302, target_status_code=404) + + def test_shortcut_no_absolute_url(self): + "Shortcuts for an object that has with a get_absolute_url method raises 404" + for obj in Article.objects.all(): + short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) + response = self.client.get(short_url) + self.assertEquals(response.status_code, 404) + + def test_page_not_found(self): + "A 404 status is returned by the page_not_found view" + non_existing_urls = ['/views/non_existing_url/', # this is in urls.py + '/views/other_non_existing_url/'] # this NOT in urls.py + for url in non_existing_urls: + response = self.client.get(url) + self.assertEquals(response.status_code, 404) + + def test_server_error(self): + "The server_error view raises a 500 status" + response = self.client.get('/views/server_error/') + self.assertEquals(response.status_code, 500) diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py new file mode 100644 index 0000000000..ebe97ab2dc --- /dev/null +++ b/tests/regressiontests/views/tests/i18n.py @@ -0,0 +1,30 @@ +from os import path +import gettext + +from django.conf import settings +from django.test import TestCase +from django.utils.translation import activate + +from regressiontests.views.urls import locale_dir + +class I18NTests(TestCase): + """ Tests django views in django/views/i18n.py """ + + def test_setlang(self): + """The set_language view can be used to change the session language""" + for lang_code, lang_name in settings.LANGUAGES: + post_data = dict(language=lang_code, next='/views/') + response = self.client.post('/views/i18n/setlang/', data=post_data) + self.assertRedirects(response, 'http://testserver/views/') + self.assertEquals(self.client.session['django_language'], lang_code) + + def test_jsi18n(self): + """The javascript_catalog can be deployed with language settings""" + for lang_code in ['es', 'fr', 'en']: + activate(lang_code) + catalog = gettext.translation('djangojs', locale_dir, [lang_code]) + trans_txt = catalog.ugettext('this is to be translated') + response = self.client.get('/views/jsi18n/') + # in response content must to be a line like that: + # catalog['this is to be translated'] = 'same_that_trans_txt' + self.assertContains(response, trans_txt, 1) diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py new file mode 100644 index 0000000000..0a67cf543e --- /dev/null +++ b/tests/regressiontests/views/tests/static.py @@ -0,0 +1,15 @@ +from os import path + +from django.test import TestCase +from regressiontests.views.urls import media_dir + +class StaticTests(TestCase): + """Tests django views in django/views/static.py""" + + def test_serve(self): + "The static view can serve static media" + media_files = ['file.txt',] + for filename in media_files: + response = self.client.get('/views/site_media/%s' % filename) + file = open(path.join(media_dir, filename)) + self.assertEquals(file.read(), response.content) \ No newline at end of file diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py new file mode 100644 index 0000000000..67d918a018 --- /dev/null +++ b/tests/regressiontests/views/urls.py @@ -0,0 +1,26 @@ +from os import path + +from django.conf.urls.defaults import * +import views + +base_dir = path.dirname(path.abspath(__file__)) +media_dir = path.join(base_dir, 'media') +locale_dir = path.join(base_dir, 'locale') + +js_info_dict = { + 'domain': 'djangojs', + 'packages': ('regressiontests.views',), +} + +urlpatterns = patterns('', + (r'^$', views.index_page), + (r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'), + (r'^non_existing_url/', 'django.views.defaults.page_not_found'), + (r'^server_error/', 'django.views.defaults.server_error'), + + (r'^i18n/', include('django.conf.urls.i18n')), + (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), + (r'^jsi18n_test/$', views.jsi18n_test), + + (r'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': media_dir}), +) diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py new file mode 100644 index 0000000000..720e43e0ac --- /dev/null +++ b/tests/regressiontests/views/views.py @@ -0,0 +1,12 @@ +from django.http import HttpResponse +from django.template import RequestContext +from django.shortcuts import render_to_response + +def index_page(request): + """ Dummy index page """ + return HttpResponse('Dummy page') + + +def jsi18n_test(request): + """ View for testing javascript message files """ + return render_to_response('js_i18n.html', {}) diff --git a/tests/urls.py b/tests/urls.py index dd475b0ea7..d7251007c5 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -11,4 +11,7 @@ urlpatterns = patterns('', # test urlconf for {% url %} template tag (r'^url_tag/', include('regressiontests.templates.urls')), + + # django built-in views + (r'^views/', include('regressiontests.views.urls')), )