diff --git a/django/core/management.py b/django/core/management.py index 564697776d..3b973c28da 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -299,7 +299,7 @@ def init(): cursor = db.db.cursor() for sql in get_sql_create(core) + get_sql_create(auth) + get_sql_initial_data(core) + get_sql_initial_data(auth): cursor.execute(sql) - cursor.execute("INSERT INTO %s (domain, name) VALUES ('mysite.com', 'My Django site')" % core.Site._meta.db_table) + cursor.execute("INSERT INTO %s (domain, name) VALUES ('example.com', 'Example site')" % core.Site._meta.db_table) except Exception, e: sys.stderr.write("Error: The database couldn't be initialized.\n%s\n" % e) try: diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index f934f9dd6c..2e39f72e95 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -1360,9 +1360,10 @@ def function_get_sql_clause(opts, **kwargs): def function_get_in_bulk(opts, klass, *args, **kwargs): id_list = args and args[0] or kwargs['id_list'] assert id_list != [], "get_in_bulk() cannot be passed an empty list." - kwargs['where'] = ["%s.id IN (%s)" % (opts.db_table, ",".join(map(str, id_list)))] + kwargs['where'] = ["%s.%s IN (%s)" % (opts.db_table, opts.pk.column, ",".join(['%s'] * len(id_list)))] + kwargs['params'] = id_list obj_list = function_get_list(opts, klass, **kwargs) - return dict([(o.id, o) for o in obj_list]) + return dict([(getattr(o, opts.pk.column), o) for o in obj_list]) def function_get_latest(opts, klass, does_not_exist_exception, **kwargs): kwargs['order_by'] = ('-' + opts.get_latest_by,) diff --git a/django/core/template/loaders/app_directories.py b/django/core/template/loaders/app_directories.py index 5afb18e2f5..b8bd0d6169 100644 --- a/django/core/template/loaders/app_directories.py +++ b/django/core/template/loaders/app_directories.py @@ -1,6 +1,7 @@ # Wrapper for loading templates from "template" directories in installed app packages. from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION +from django.core.exceptions import ImproperlyConfigured from django.core.template import TemplateDoesNotExist import os @@ -8,8 +9,17 @@ import os app_template_dirs = [] for app in INSTALLED_APPS: i = app.rfind('.') - m, a = app[:i], app[i+1:] - mod = getattr(__import__(m, '', '', [a]), a) + if i == -1: + m, a = app, None + else: + m, a = app[:i], app[i+1:] + try: + if a is None: + mod = __import__(m, '', '', []) + else: + mod = getattr(__import__(m, '', '', [a]), a) + except ImportError, e: + raise ImproperlyConfigured, 'ImportError %s: %s' % (app, e.args[0]) template_dir = os.path.join(os.path.dirname(mod.__file__), 'templates') if os.path.isdir(template_dir): app_template_dirs.append(template_dir) diff --git a/django/utils/dateformat.py b/django/utils/dateformat.py index 3620558f09..9913c3f8c4 100644 --- a/django/utils/dateformat.py +++ b/django/utils/dateformat.py @@ -12,8 +12,9 @@ Usage: """ from django.utils.dates import MONTHS, MONTHS_AP, WEEKDAYS +from django.utils.tzinfo import LocalTimezone from calendar import isleap -import re +import re, time re_formatchars = re.compile(r'(? 11: + return 'PM' + return 'AM' def B(self): "Swatch Internet time" @@ -100,8 +103,12 @@ class TimeFormat(Formatter): class DateFormat(TimeFormat): year_days = [None, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] - def __init__(self, d): - self.data = d + def __init__(self, dt): + # Accepts either a datetime or date object. + self.data = dt + self.timezone = getattr(dt, 'tzinfo', None) + if hasattr(self.data, 'hour') and not self.timezone: + self.timezone = LocalTimezone(dt) def d(self): "Day of the month, 2 digits with leading zeros; i.e. '01' to '31'" @@ -119,6 +126,13 @@ class DateFormat(TimeFormat): "'1' if Daylight Savings Time, '0' otherwise." raise NotImplementedError + def I(self): + "'1' if Daylight Savings Time, '0' otherwise." + if self.timezone.dst(self.data): + return '1' + else: + return '0' + def j(self): "Day of the month without leading zeros; i.e. '1' to '31'" return self.data.day @@ -149,11 +163,12 @@ class DateFormat(TimeFormat): def O(self): "Difference to Greenwich time in hours; e.g. '+0200'" - raise NotImplementedError + tz = self.timezone.utcoffset(self.data) + return "%+03d%02d" % (tz.seconds // 3600, (tz.seconds // 60) % 60) def r(self): "RFC 822 formatted date; e.g. 'Thu, 21 Dec 2000 16:01:07 +0200'" - raise NotImplementedError + return self.format('D, j M Y H:i:s O') def S(self): "English ordinal suffix for the day of the month, 2 characters; i.e. 'st', 'nd', 'rd' or 'th'" @@ -174,11 +189,15 @@ class DateFormat(TimeFormat): def T(self): "Time zone of this machine; e.g. 'EST' or 'MDT'" - raise NotImplementedError + name = self.timezone.tzname(self.data) + if name is None: + name = self.format('O') + return name def U(self): "Seconds since the Unix epoch (January 1 1970 00:00:00 GMT)" - raise NotImplementedError + off = self.timezone.utcoffset(self.data) + return int(time.mktime(self.data.timetuple())) + off.seconds * 60 def w(self): "Day of the week, numeric, i.e. '0' (Sunday) to '6' (Saturday)" @@ -229,7 +248,7 @@ class DateFormat(TimeFormat): """Time zone offset in seconds (i.e. '-43200' to '43200'). The offset for timezones west of UTC is always negative, and for those east of UTC is always positive.""" - raise NotImplementedError + return self.timezone.utcoffset(self.data).seconds def format(value, format_string): "Convenience function" diff --git a/django/utils/timesince.py b/django/utils/timesince.py index c11cef0342..5b22fde58c 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -1,4 +1,5 @@ -import time, math, datetime +import datetime, math, time +from django.utils.tzinfo import LocalTimezone def timesince(d, now=None): """ @@ -6,7 +7,6 @@ def timesince(d, now=None): as a nicely formatted string, e.g "10 minutes" Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since """ - original = time.mktime(d.timetuple()) chunks = ( (60 * 60 * 24 * 365, 'year'), (60 * 60 * 24 * 30, 'month'), @@ -14,9 +14,17 @@ def timesince(d, now=None): (60 * 60, 'hour'), (60, 'minute') ) - if not now: - now = time.time() - since = now - original + if now: + t = time.mktime(now) + else: + t = time.localtime() + if d.tzinfo: + tz = LocalTimezone() + else: + tz = None + now = datetime.datetime(t[0], t[1], t[2], t[3], t[4], t[5], tzinfo=tz) + delta = now - d + since = delta.days * 24 * 60 * 60 + delta.seconds # Crazy iteration syntax because we need i to be current index for i, (seconds, name) in zip(range(len(chunks)), chunks): count = math.floor(since / seconds) diff --git a/django/utils/tzinfo.py b/django/utils/tzinfo.py new file mode 100644 index 0000000000..cc9f028e91 --- /dev/null +++ b/django/utils/tzinfo.py @@ -0,0 +1,52 @@ +"Implementation of tzinfo classes for use with datetime.datetime." + +import time +from datetime import timedelta, tzinfo + +class FixedOffset(tzinfo): + "Fixed offset in minutes east from UTC." + def __init__(self, offset): + self.__offset = timedelta(minutes=offset) + self.__name = "%+03d%02d" % (offset // 60, offset % 60) + + def __repr__(self): + return self.__name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return timedelta(0) + +class LocalTimezone(tzinfo): + "Proxy timezone information from time module." + def __init__(self, dt): + tzinfo.__init__(self, dt) + self._tzname = time.tzname[self._isdst(dt)] + + def __repr__(self): + return self._tzname + + def utcoffset(self, dt): + if self._isdst(dt): + return timedelta(seconds=-time.altzone) + else: + return timedelta(seconds=-time.timezone) + + def dst(self, dt): + if self._isdst(dt): + return timedelta(seconds=-time.altzone) - timedelta(seconds=-time.timezone) + else: + return timedelta(0) + + def tzname(self, dt): + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 diff --git a/django/views/decorators/auth.py b/django/views/decorators/auth.py index ae27fe33a1..f543a6aa48 100644 --- a/django/views/decorators/auth.py +++ b/django/views/decorators/auth.py @@ -1,12 +1,19 @@ +def user_passes_test(view_func, test_func): + """ + Decorator for views that checks that the user passes the given test, + redirecting to the log-in page if necessary. The test should be a callable + that takes the user object and returns True if the user passes. + """ + from django.views.auth.login import redirect_to_login + def _checklogin(request, *args, **kwargs): + if test_func(request.user): + return view_func(request, *args, **kwargs) + return redirect_to_login(request.path) + return _checklogin + def login_required(view_func): """ Decorator for views that checks that the user is logged in, redirecting to the log-in page if necessary. """ - from django.views.auth.login import redirect_to_login - def _checklogin(request, *args, **kwargs): - if request.user.is_anonymous(): - return redirect_to_login(request.path) - else: - return view_func(request, *args, **kwargs) - return _checklogin + return user_passes_test(lambda u: not u.is_anonymous()) diff --git a/docs/authentication.txt b/docs/authentication.txt new file mode 100644 index 0000000000..eaf3cc2b99 --- /dev/null +++ b/docs/authentication.txt @@ -0,0 +1,288 @@ +============================= +User authentication in Django +============================= + +Django comes with a user authentication system. It handles user accounts, +groups, permissions and cookie-based user sessions. This document explains how +things work. + +The basics +========== + +Django supports authentication out of the box. The ``django-admin.py init`` +command, used to initialize a database with Django's core database tables, +creates the infrastructure for the auth system. You don't have to do anything +else to use authentication. + +The auth system consists of: + + * Users + * Permissions: Binary (yes/no) flags designating whether a user may perform + a certain task. + * Groups: A generic way of applying labels and permissions to more than one + user. + * Messages: A simple way to queue messages for given users. + +Users +===== + +Users are represented by a standard Django model, which lives in +`django/models/auth.py`_. + +.. _django/models/auth.py: http://code.djangoproject.com/browser/django/trunk/django/models/auth.py + +API reference +------------- + +Fields +~~~~~~ + +``User`` objects have the following fields: + + * ``username`` -- Required. 30 characters or fewer. Alphanumeric characters + only (letters, digits and underscores). + * ``first_name`` -- Optional. 30 characters or fewer. + * ``last_name`` -- Optional. 30 characters or fewer. + * ``email`` -- Optional. E-mail address. + * ``password_md5`` -- Required. An MD5 hash of the password. (Django + doesn't store the raw password.) Raw passwords can be arbitrarily long + and can contain any character. + * ``is_staff`` -- Boolean. Designates whether this user can access the + admin site. + * ``is_active`` -- Boolean. Designates whether this user account is valid. + Set this to ``False`` instead of deleting accounts. + * ``is_superuser`` -- Boolean. Designates whether this user has permission + to do anything (according to the permission system). + * ``last_login`` -- A datetime of the user's last login. Is set to the + current date/time by default. + * ``date_joined`` -- A datetime designating when the account was created. + Is set to the current date/time by default when the account is created. + +Methods +~~~~~~~ + +``User`` objects have two many-to-many fields: ``groups`` and +``user_permissions``. Because of those relationships, ``User`` objects get +data-access methods like any other `Django model`_: + + * ``get_group_list(**kwargs)`` + * ``set_groups(id_list)`` + * ``get_permission_list(**kwargs)`` + * ``set_user_permissions(id_list)`` + +In addition to those automatic API methods, ``User`` objects have the following +methods: + + * ``is_anonymous()`` -- Always returns ``False``. This is a way of + comparing ``User`` objects to anonymous users. + + * ``get_full_name()`` -- Returns the ``first_name`` plus the ``last_name``, + with a space in between. + + * ``set_password(raw_password)`` -- Sets the user's password to the given + raw string, taking care of the MD5 hashing. Doesn't save the ``User`` + object. + + * ``check_password(raw_password)`` -- Returns ``True`` if the given raw + string is the correct password for the user. + + * ``get_group_permissions()`` -- Returns a list of permission strings that + the user has, through his/her groups. + + * ``get_all_permissions()`` -- Returns a list of permission strings that + the user has, both through group and user permissions. + + * ``has_perm(perm)`` -- Returns ``True`` if the user has the specified + permission. + + * ``has_perms(perm_list)`` -- Returns ``True`` if the user has each of the + specified permissions. + + * ``has_module_perms(package_name)`` -- Returns ``True`` if the user has + any permissions in the given package (the Django app label). + + * ``get_and_delete_messages()`` -- Returns a list of ``Message`` objects in + the user's queue and deletes the messages from the queue. + + * ``email_user(subject, message, from_email=None)`` -- Sends an e-mail to + the user. If ``from_email`` is ``None``, Django uses the + `DEFAULT_FROM_EMAIL`_ setting. + + * ``get_profile()`` -- Returns a site-specific profile for this user. + Raises ``django.models.auth.SiteProfileNotAvailable`` if the current site + doesn't allow profiles. + +.. _Django model: http://www.djangoproject.com/documentation/model_api/ +.. _DEFAULT_FROM_EMAIL: http://www.djangoproject.com/documentation/settings/#default-from-email + +Module functions +~~~~~~~~~~~~~~~~ + +The ``django.models.auth.users`` module has the following helper functions: + + * ``create_user(username, email, password)`` -- Creates, saves and returns + a ``User``. The ``username``, ``email`` and ``password`` are set as + given, and the ``User`` gets ``is_active=True``. + + * ``make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789')`` + -- Returns a random password with the given length and given string of + allowed characters. (Note that the default value of ``allowed_chars`` + doesn't contain ``"I"`` or letters that look like it, to avoid user + confusion. + +Basic usage +----------- + +Creating users +~~~~~~~~~~~~~~ + +The most basic way to create users is to use the standard Django +`database API`_. Just create and save a ``User`` object:: + + >>> from django.models.auth import users + >>> import md5 + >>> p = md5.new('johnpassword').hexdigest() + >>> u = users.User(username='john', first_name='John', last_name='lennon', + ... email='lennon@thebeatles.com', password_md5=p, is_staff=True, + ... is_active=True, is_superuser=False) + >>> u.save() + +Note that ``password_md5`` requires the raw MD5 hash. Because that's a pain, +there's a ``create_user`` helper function:: + + >>> from django.models.auth import users + >>> u = users.create_user('john', 'lennon@thebeatles.com', 'johnpassword') + +.. _database API: http://www.djangoproject.com/documentation/db_api/ + +Changing passwords +~~~~~~~~~~~~~~~~~~ + +Change a password with ``set_password()``:: + + >>> from django.models.auth import users + >>> u = users.get_object(username__exact='john') + >>> u.set_password('new password') + >>> u.save() + +Anonymous users +--------------- + +``django.parts.auth.anonymoususers.AnonymousUser`` is a class that implements +the ``django.models.auth.users.User`` interface, with these differences: + + * ``is_anonymous()`` returns ``True`` instead of ``False``. + * ``has_perm()`` always returns ``False``. + * ``set_password()``, ``check_password()``, ``set_groups()`` and + ``set_permissions()`` raise ``NotImplementedError``. + +In practice, you probably won't need to use ``AnonymousUser`` objects on your +own, but they're used by Web requests, as explained in the next section. + +Authentication in Web requests +============================== + +Until now, this document has dealt with the low-level APIs for manipulating +authentication-related objects. On a higher level, Django hooks this +authentication framework into its system of `request objects`_. + +In any Django view, ``request.user`` will give you a ``User`` object +representing the currently logged-in user. If a user isn't currently logged in, +``request.user`` will be set to an instance of ``AnonymousUser`` (see the +previous section). You can tell them apart with ``is_anonymous()``, like so:: + + if request.user.is_anonymous(): + # Do something for anonymous users. + else: + # Do something for logged-in users. + +.. _request objects: http://www.djangoproject.com/documentation/request_response/#httprequest-objects + +Limiting access to logged-in users +---------------------------------- + +The raw way +~~~~~~~~~~~ + +The simple, raw way to limit access to pages is to check +``request.user.is_anonymous()`` and either redirect to a login page:: + + from django.utils.httpwrappers import HttpResponseRedirect + + def my_view(request): + if request.user.is_anonymous(): + return HttpResponseRedirect('/login/?next=%s' % request.path) + # ... + +...or display an error message:: + + def my_view(request): + if request.user.is_anonymous(): + return render_to_response('myapp/login_error') + # ... + +The login_required decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As a shortcut, you can use the convenient ``login_required`` decorator:: + + from django.views.decorators.auth import login_required + + def my_view(request): + # ... + my_view = login_required(my_view) + +Here's the same thing, using Python 2.4's decorator syntax:: + + from django.views.decorators.auth import login_required + + @login_required + def my_view(request): + # ... + +``login_required`` does the following: + + * If the user isn't logged in, redirect to ``/accounts/login/``, passing + the current absolute URL in the query string as ``next``. For example: + ``/accounts/login/?next=/polls/3/``. + * If the user is logged in, execute the view normally. The view code is + free to assume the user is logged in. + +Limiting access to logged-in users that pass a test +--------------------------------------------------- + +To limit access based on certain permissions or another test, you'd do the same +thing as described in the previous section. + +The simple way is to run your test on ``request.user`` in the view directly. +For example, this view checks to make sure the user is logged in and has the +permission ``polls.can_vote``:: + + def my_view(request): + if request.user.is_anonymous() or not request.user.has_perm('polls.can_vote'): + return HttpResponse("You can't vote in this poll.") + # ... + +As a shortcut, you can use the convenient ``user_passes_test`` decorator:: + + from django.views.decorators.auth import user_passes_test + + @user_passes_test(lambda u: u.has_perm('polls.can_vote')) + def my_view(request): + # ... + +``user_passes_test`` takes a required argument: a callable that takes a +``User`` object and returns ``True`` if the user is allowed to view the page. +Note that ``user_passes_test`` does not automatically check that the ``User`` +is not anonymous. + + + +Permissions +=========== + +Groups +====== + +Messages +======== diff --git a/docs/templates.txt b/docs/templates.txt index e543b59763..debec88a74 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -517,18 +517,18 @@ Built-in tag reference n Month without leading zeros. ``'1'`` to ``'12'`` N Month abbreviation in Associated Press ``'Jan.'``, ``'Feb.'``, ``'March'``, ``'May'`` style. Proprietary extension. - O Not implemented. + O Difference to Greenwich time in hours. ``'+0200'`` P Time, in 12-hour hours, minutes and ``'1 a.m.'``, ``'1:30 p.m.'``, ``'midnight'``, ``'noon'``, ``'12:30 p.m.'`` 'a.m.'/'p.m.', with minutes left off if they're zero and the special-case strings 'midnight' and 'noon' if appropriate. Proprietary extension. - r Not implemented. + r RFC 822 formatted date. ``'Thu, 21 Dec 2000 16:01:07 +0200'`` s Seconds, 2 digits with leading zeros. ``'00'`` to ``'59'`` S English ordinal suffix for day of the ``'st'``, ``'nd'``, ``'rd'`` or ``'th'`` month, 2 characters. t Not implemented. - T Not implemented. + T Time zone of this machine. ``'EST'``, ``'MDT'`` U Not implemented. w Day of the week, digits without ``'0'`` (Sunday) to ``'6'`` (Saturday) leading zeros. @@ -537,7 +537,10 @@ Built-in tag reference y Year, 2 digits. ``'99'`` Y Year, 4 digits. ``'1999'`` z Day of the year. ``0`` to ``365`` - Z Not implemented. + Z Time zone offset in seconds. The ``-43200`` to ``43200`` + offset for timezones west of UTC is + always negative, and for those east of + UTC is always positive. ================ ====================================== ===================== Example:: @@ -610,6 +613,11 @@ Built-in tag reference {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} + Note that if you use ``{% ssi %}``, you'll need to define + `ALLOWED_INCLUDE_ROOTS`_ in your Django settings, as a security measure. + +.. _ALLOWED_INCLUDE_ROOTS: http://www.djangoproject.com/documentation/settings/#allowed-include-roots + ``templatetag`` Output one of the bits used to compose template tags. diff --git a/tests/othertests/dateformat.py b/tests/othertests/dateformat.py new file mode 100644 index 0000000000..fe8bc8256d --- /dev/null +++ b/tests/othertests/dateformat.py @@ -0,0 +1,75 @@ +""" +>>> format(my_birthday, '') +'' +>>> format(my_birthday, 'a') +'p.m.' +>>> format(my_birthday, 'A') +'PM' +>>> format(my_birthday, 'j') +'7' +>>> format(my_birthday, 'l') +'Saturday' +>>> format(my_birthday, 'L') +'False' +>>> format(my_birthday, 'm') +'07' +>>> format(my_birthday, 'M') +'Jul' +>>> format(my_birthday, 'n') +'7' +>>> format(my_birthday, 'N') +'July' +>>> format(my_birthday, 'O') +'+0100' +>>> format(my_birthday, 'P') +'10 p.m.' +>>> format(my_birthday, 'r') +'Sat, 7 Jul 1979 22:00:00 +0100' +>>> format(my_birthday, 's') +'00' +>>> format(my_birthday, 'S') +'th' +>>> format(my_birthday, 't') +Traceback (most recent call last): + ... +NotImplementedError +>>> format(my_birthday, 'T') +'CET' +>>> format(my_birthday, 'U') +'300445200' +>>> format(my_birthday, 'w') +'6' +>>> format(my_birthday, 'W') +'27' +>>> format(my_birthday, 'y') +'79' +>>> format(my_birthday, 'Y') +'1979' +>>> format(my_birthday, 'z') +'188' +>>> format(my_birthday, 'Z') +'3600' + +>>> format(summertime, 'I') +'1' +>>> format(summertime, 'O') +'+0200' +>>> format(wintertime, 'I') +'0' +>>> format(wintertime, 'O') +'+0100' + +>>> format(my_birthday, 'Y z \\C\\E\\T') +'1979 188 CET' +""" + +from django.utils import dateformat +format = dateformat.format +import datetime, os, time + +os.environ['TZ'] = 'Europe/Copenhagen' +time.tzset() + +my_birthday = datetime.datetime(1979, 7, 7, 22, 00) +summertime = datetime.datetime(2005, 10, 30, 1, 00) +wintertime = datetime.datetime(2005, 10, 30, 4, 00) diff --git a/tests/testapp/models/custom_pk.py b/tests/testapp/models/custom_pk.py index 234b5c3308..5b0eb45462 100644 --- a/tests/testapp/models/custom_pk.py +++ b/tests/testapp/models/custom_pk.py @@ -53,6 +53,8 @@ EmployeeDoesNotExist: Employee does not exist for {'pk': 'foo'} >>> fran.save() >>> employees.get_list(last_name__exact='Jones') [Dan Jones, Fran Jones] +>>> employees.get_in_bulk(['ABC123', 'XYZ456']) +{'XYZ456': Fran Jones, 'ABC123': Dan Jones} >>> b = businesses.Business(name='Sears') >>> b.save() @@ -62,4 +64,6 @@ True [Dan Jones, Fran Jones] >>> fran.get_business_list() [Sears] +>>> businesses.get_in_bulk(['Sears']) +{'Sears': Sears} """