summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Pellerin <jpellerin@gmail.com>2006-06-28 17:07:26 +0000
committerJason Pellerin <jpellerin@gmail.com>2006-06-28 17:07:26 +0000
commitda5b0586d359390e2102703707afae65befe7f6d (patch)
tree53ab9ed62c0b7a8451355a34d6f1e0fab2f98af0
parent54b6e969571576e9ec616df8b074f744009b35cc (diff)
Merge trunk to [3226]
git-svn-id: http://code.djangoproject.com/svn/django/branches/multiple-db-support@3228 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/conf/global_settings.py6
-rw-r--r--django/contrib/admin/views/decorators.py16
-rw-r--r--django/contrib/auth/__init__.py69
-rw-r--r--django/contrib/auth/backends.py21
-rw-r--r--django/contrib/auth/forms.py16
-rw-r--r--django/contrib/auth/middleware.py8
-rw-r--r--django/contrib/auth/models.py23
-rw-r--r--django/contrib/auth/views.py7
-rw-r--r--django/contrib/comments/views/comments.py4
-rw-r--r--django/contrib/markup/templatetags/markup.py2
-rw-r--r--django/core/serializers/__init__.py76
-rw-r--r--django/core/serializers/base.py159
-rw-r--r--django/core/serializers/xml_serializer.py218
-rw-r--r--django/db/models/base.py8
-rw-r--r--django/db/models/fields/__init__.py2
-rw-r--r--django/db/models/loading.py13
-rw-r--r--django/db/models/manager.py7
-rw-r--r--django/db/models/query.py9
-rw-r--r--docs/authentication.txt105
-rw-r--r--docs/db-api.txt18
-rw-r--r--docs/serialization.txt85
-rw-r--r--tests/modeltests/basic/models.py5
-rw-r--r--tests/modeltests/lookup/models.py4
-rw-r--r--tests/modeltests/serializers/__init__.py0
-rw-r--r--tests/modeltests/serializers/models.py94
25 files changed, 918 insertions, 57 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 640b290a6f..a1dff3c815 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -281,3 +281,9 @@ COMMENTS_FIRST_FEW = 0
# A tuple of IP addresses that have been banned from participating in various
# Django-powered features.
BANNED_IPS = ()
+
+##################
+# AUTHENTICATION #
+##################
+
+AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py
index d984077dfb..250c585220 100644
--- a/django/contrib/admin/views/decorators.py
+++ b/django/contrib/admin/views/decorators.py
@@ -1,6 +1,7 @@
from django import http, template
from django.conf import settings
-from django.contrib.auth.models import User, SESSION_KEY
+from django.contrib.auth.models import User
+from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response
from django.utils.translation import gettext_lazy
import base64, datetime, md5
@@ -69,10 +70,10 @@ def staff_member_required(view_func):
return _display_login_form(request, message)
# Check the password.
- username = request.POST.get('username', '')
- try:
- user = User.objects.get(username=username, is_staff=True)
- except User.DoesNotExist:
+ username = request.POST.get('username', None)
+ password = request.POST.get('password', None)
+ user = authenticate(username=username, password=password)
+ if user is None:
message = ERROR_MESSAGE
if '@' in username:
# Mistakenly entered e-mail address instead of username? Look it up.
@@ -86,8 +87,9 @@ def staff_member_required(view_func):
# The user data is correct; log in the user in and continue.
else:
- if user.check_password(request.POST.get('password', '')):
- request.session[SESSION_KEY] = user.id
+ if user.is_staff:
+ login(request, user)
+ # TODO: set last_login with an event.
user.last_login = datetime.datetime.now()
user.save()
if request.POST.has_key('post_data'):
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index ac7b40aca6..dde7ea5c9c 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -1,2 +1,71 @@
+from django.core.exceptions import ImproperlyConfigured
+
+SESSION_KEY = '_auth_user_id'
+BACKEND_SESSION_KEY = '_auth_user_backend'
LOGIN_URL = '/accounts/login/'
REDIRECT_FIELD_NAME = 'next'
+
+def load_backend(path):
+ i = path.rfind('.')
+ module, attr = path[:i], path[i+1:]
+ try:
+ mod = __import__(module, '', '', [attr])
+ except ImportError, e:
+ raise ImproperlyConfigured, 'Error importing authentication backend %s: "%s"' % (module, e)
+ try:
+ cls = getattr(mod, attr)
+ except AttributeError:
+ raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
+ return cls()
+
+def get_backends():
+ from django.conf import settings
+ backends = []
+ for backend_path in settings.AUTHENTICATION_BACKENDS:
+ backends.append(load_backend(backend_path))
+ return backends
+
+def authenticate(**credentials):
+ """
+ If the given credentials, return a user object.
+ """
+ for backend in get_backends():
+ try:
+ user = backend.authenticate(**credentials)
+ except TypeError:
+ # this backend doesn't accept these credentials as arguments, try the next one.
+ continue
+ if user is None:
+ continue
+ # annotate the user object with the path of the backend
+ user.backend = str(backend.__class__)
+ return user
+
+def login(request, user):
+ """
+ Persist a user id and a backend in the request. This way a user doesn't
+ have to reauthenticate on every request.
+ """
+ if user is None:
+ user = request.user
+ # TODO: It would be nice to support different login methods, like signed cookies.
+ request.session[SESSION_KEY] = user.id
+ request.session[BACKEND_SESSION_KEY] = user.backend
+
+def logout(request):
+ """
+ Remove the authenticated user's id from request.
+ """
+ del request.session[SESSION_KEY]
+ del request.session[BACKEND_SESSION_KEY]
+
+def get_user(request):
+ from django.contrib.auth.models import AnonymousUser
+ try:
+ user_id = request.session[SESSION_KEY]
+ backend_path = request.session[BACKEND_SESSION_KEY]
+ backend = load_backend(backend_path)
+ user = backend.get_user(user_id) or AnonymousUser()
+ except KeyError:
+ user = AnonymousUser()
+ return user
diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py
new file mode 100644
index 0000000000..3b46b65b0a
--- /dev/null
+++ b/django/contrib/auth/backends.py
@@ -0,0 +1,21 @@
+from django.contrib.auth.models import User, check_password
+
+class ModelBackend:
+ """
+ Authenticate against django.contrib.auth.models.User
+ """
+ # TODO: Model, login attribute name and password attribute name should be
+ # configurable.
+ def authenticate(self, username=None, password=None):
+ try:
+ user = User.objects.get(username=username)
+ if user.check_password(password):
+ return user
+ except User.DoesNotExist:
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ return None
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 800c14375b..ef81268e2a 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,4 +1,5 @@
from django.contrib.auth.models import User
+from django.contrib.auth import authenticate
from django.contrib.sites.models import Site
from django.template import Context, loader
from django.core import validators
@@ -20,8 +21,7 @@ class AuthenticationForm(forms.Manipulator):
self.fields = [
forms.TextField(field_name="username", length=15, maxlength=30, is_required=True,
validator_list=[self.isValidUser, self.hasCookiesEnabled]),
- forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True,
- validator_list=[self.isValidPasswordForUser]),
+ forms.PasswordField(field_name="password", length=15, maxlength=30, is_required=True),
]
self.user_cache = None
@@ -30,16 +30,10 @@ class AuthenticationForm(forms.Manipulator):
raise validators.ValidationError, _("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
def isValidUser(self, field_data, all_data):
- try:
- self.user_cache = User.objects.get(username=field_data)
- except User.DoesNotExist:
- raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
-
- def isValidPasswordForUser(self, field_data, all_data):
+ username = field_data
+ password = all_data.get('password', None)
+ self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
- return
- if not self.user_cache.check_password(field_data):
- self.user_cache = None
raise validators.ValidationError, _("Please enter a correct username and password. Note that both fields are case-sensitive.")
elif not self.user_cache.is_active:
raise validators.ValidationError, _("This account is inactive.")
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index a1a0b2e834..a6a60780a7 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -4,12 +4,8 @@ class LazyUser(object):
def __get__(self, request, obj_type=None):
if self._user is None:
- from django.contrib.auth.models import User, AnonymousUser, SESSION_KEY
- try:
- user_id = request.session[SESSION_KEY]
- self._user = User.objects.get(pk=user_id)
- except (KeyError, User.DoesNotExist):
- self._user = AnonymousUser()
+ from django.contrib.auth import get_user
+ self._user = get_user(request)
return self._user
class AuthenticationMiddleware(object):
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 65392695ae..e37f5a4497 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -4,7 +4,19 @@ from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
import datetime
-SESSION_KEY = '_auth_user_id'
+def check_password(raw_password, enc_password):
+ """
+ Returns a boolean of whether the raw_password was correct. Handles
+ encryption formats behind the scenes.
+ """
+ algo, salt, hsh = enc_password.split('$')
+ if algo == 'md5':
+ import md5
+ return hsh == md5.new(salt+raw_password).hexdigest()
+ elif algo == 'sha1':
+ import sha
+ return hsh == sha.new(salt+raw_password).hexdigest()
+ raise ValueError, "Got unknown password algorithm type in password."
class SiteProfileNotAvailable(Exception):
pass
@@ -141,14 +153,7 @@ class User(models.Model):
self.set_password(raw_password)
self.save()
return is_correct
- algo, salt, hsh = self.password.split('$')
- if algo == 'md5':
- import md5
- return hsh == md5.new(salt+raw_password).hexdigest()
- elif algo == 'sha1':
- import sha
- return hsh == sha.new(salt+raw_password).hexdigest()
- raise ValueError, "Got unknown password algorithm type in password."
+ return check_password(raw_password, self.password)
def get_group_permissions(self):
"Returns a list of permission strings that this user has through his/her groups."
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index 6d908ee025..97ecd6a3fd 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -3,7 +3,6 @@ from django.contrib.auth.forms import PasswordResetForm, PasswordChangeForm
from django import forms
from django.shortcuts import render_to_response
from django.template import RequestContext
-from django.contrib.auth.models import SESSION_KEY
from django.contrib.sites.models import Site
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
@@ -19,7 +18,8 @@ def login(request, template_name='registration/login.html'):
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '://' in redirect_to or ' ' in redirect_to:
redirect_to = '/accounts/profile/'
- request.session[SESSION_KEY] = manipulator.get_user_id()
+ from django.contrib.auth import login
+ login(request, manipulator.get_user())
request.session.delete_test_cookie()
return HttpResponseRedirect(redirect_to)
else:
@@ -33,8 +33,9 @@ def login(request, template_name='registration/login.html'):
def logout(request, next_page=None, template_name='registration/logged_out.html'):
"Logs out the user and displays 'You are logged out' message."
+ from django.contrib.auth import logout
try:
- del request.session[SESSION_KEY]
+ logout(request)
except KeyError:
return render_to_response(template_name, {'title': _('Logged out')}, context_instance=RequestContext(request))
else:
diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py
index 316f3e719b..c32a82f4d8 100644
--- a/django/contrib/comments/views/comments.py
+++ b/django/contrib/comments/views/comments.py
@@ -5,7 +5,6 @@ from django.http import Http404
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render_to_response
from django.template import RequestContext
-from django.contrib.auth.models import SESSION_KEY
from django.contrib.comments.models import Comment, FreeComment, PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.forms import AuthenticationForm
@@ -219,7 +218,8 @@ def post_comment(request):
# If user gave correct username/password and wasn't already logged in, log them in
# so they don't have to enter a username/password again.
if manipulator.get_user() and new_data.has_key('password') and manipulator.get_user().check_password(new_data['password']):
- request.session[SESSION_KEY] = manipulator.get_user_id()
+ from django.contrib.auth import login
+ login(request, manipulator.get_user())
if errors or request.POST.has_key('preview'):
class CommentFormWrapper(forms.FormWrapper):
def __init__(self, manipulator, new_data, errors, rating_choices):
diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py
index dc8a9da031..9cbf1b3807 100644
--- a/django/contrib/markup/templatetags/markup.py
+++ b/django/contrib/markup/templatetags/markup.py
@@ -27,7 +27,7 @@ def textile(value):
raise template.TemplateSyntaxError, "Error in {% textile %} filter: The Python textile library isn't installed."
return value
else:
- return textile.textile(value)
+ return textile.textile(value, encoding=settings.DEFAULT_CHARSET, output=settings.DEFAULT_CHARSET)
def markdown(value):
try:
diff --git a/django/core/serializers/__init__.py b/django/core/serializers/__init__.py
new file mode 100644
index 0000000000..72c4407b59
--- /dev/null
+++ b/django/core/serializers/__init__.py
@@ -0,0 +1,76 @@
+"""
+Interfaces for serializing Django objects.
+
+Usage::
+
+ >>> from django.core import serializers
+ >>> json = serializers.serialize("json", some_query_set)
+ >>> objects = list(serializers.deserialize("json", json))
+
+To add your own serializers, use the SERIALIZATION_MODULES setting::
+
+ SERIALIZATION_MODULES = {
+ "csv" : "path.to.csv.serializer",
+ "txt" : "path.to.txt.serializer",
+ }
+
+"""
+
+from django.conf import settings
+
+# Built-in serializers
+BUILTIN_SERIALIZERS = {
+ "xml" : "django.core.serializers.xml_serializer",
+}
+
+_serializers = {}
+
+def register_serializer(format, serializer_module):
+ """Register a new serializer by passing in a module name."""
+ module = __import__(serializer_module, '', '', [''])
+ _serializers[format] = module
+
+def unregister_serializer(format):
+ """Unregister a given serializer"""
+ del _serializers[format]
+
+def get_serializer(format):
+ if not _serializers:
+ _load_serializers()
+ return _serializers[format].Serializer
+
+def get_deserializer(format):
+ if not _serializers:
+ _load_serializers()
+ return _serializers[format].Deserializer
+
+def serialize(format, queryset, **options):
+ """
+ Serialize a queryset (or any iterator that returns database objects) using
+ a certain serializer.
+ """
+ s = get_serializer(format)()
+ s.serialize(queryset, **options)
+ return s.getvalue()
+
+def deserialize(format, stream_or_string):
+ """
+ Deserialize a stream or a string. Returns an iterator that yields ``(obj,
+ m2m_relation_dict)``, where ``obj`` is a instantiated -- but *unsaved* --
+ object, and ``m2m_relation_dict`` is a dictionary of ``{m2m_field_name :
+ list_of_related_objects}``.
+ """
+ d = get_deserializer(format)
+ return d(stream_or_string)
+
+def _load_serializers():
+ """
+ Register built-in and settings-defined serializers. This is done lazily so
+ that user code has a chance to (e.g.) set up custom settings without
+ needing to be careful of import order.
+ """
+ for format in BUILTIN_SERIALIZERS:
+ register_serializer(format, BUILTIN_SERIALIZERS[format])
+ if hasattr(settings, "SERIALIZATION_MODULES"):
+ for format in settings.SERIALIZATION_MODULES:
+ register_serializer(format, settings.SERIALIZATION_MODULES[format]) \ No newline at end of file
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
new file mode 100644
index 0000000000..5c84861326
--- /dev/null
+++ b/django/core/serializers/base.py
@@ -0,0 +1,159 @@
+"""
+Module for abstract serializer/unserializer base classes.
+"""
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+from django.db import models
+
+class SerializationError(Exception):
+ """Something bad happened during serialization."""
+ pass
+
+class DeserializationError(Exception):
+ """Something bad happened during deserialization."""
+ pass
+
+class Serializer(object):
+ """
+ Abstract serializer base class.
+ """
+
+ def serialize(self, queryset, **options):
+ """
+ Serialize a queryset.
+ """
+ self.options = options
+
+ self.stream = options.get("stream", StringIO())
+
+ self.start_serialization()
+ for obj in queryset:
+ self.start_object(obj)
+ for field in obj._meta.fields:
+ if field.rel is None:
+ self.handle_field(obj, field)
+ else:
+ self.handle_fk_field(obj, field)
+ for field in obj._meta.many_to_many:
+ self.handle_m2m_field(obj, field)
+ self.end_object(obj)
+ self.end_serialization()
+ return self.getvalue()
+
+ def get_string_value(self, obj, field):
+ """
+ Convert a field's value to a string.
+ """
+ if isinstance(field, models.DateTimeField):
+ value = getattr(obj, field.name).strftime("%Y-%m-%d %H:%M:%S")
+ elif isinstance(field, models.FileField):
+ value = getattr(obj, "get_%s_url" % field.name, lambda: None)()
+ else:
+ value = field.flatten_data(follow=None, obj=obj).get(field.name, "")
+ return str(value)
+
+ def start_serialization(self):
+ """
+ Called when serializing of the queryset starts.
+ """
+ raise NotImplementedError
+
+ def end_serialization(self):
+ """
+ Called when serializing of the queryset ends.
+ """
+ pass
+
+ def start_object(self, obj):
+ """
+ Called when serializing of an object starts.
+ """
+ raise NotImplementedError
+
+ def end_object(self, obj):
+ """
+ Called when serializing of an object ends.
+ """
+ pass
+
+ def handle_field(self, obj, field):
+ """
+ Called to handle each individual (non-relational) field on an object.
+ """
+ raise NotImplementedError
+
+ def handle_fk_field(self, obj, field):
+ """
+ Called to handle a ForeignKey field.
+ """
+ raise NotImplementedError
+
+ def handle_m2m_field(self, obj, field):
+ """
+ Called to handle a ManyToManyField.
+ """
+ raise NotImplementedError
+
+ def getvalue(self):
+ """
+ Return the fully serialized queryset.
+ """
+ return self.stream.getvalue()
+
+class Deserializer(object):
+ """
+ Abstract base deserializer class.
+ """
+
+ def __init__(self, stream_or_string, **options):
+ """
+ Init this serializer given a stream or a string
+ """
+ self.options = options
+ if isinstance(stream_or_string, basestring):
+ self.stream = StringIO(stream_or_string)
+ else:
+ self.stream = stream_or_string
+ # hack to make sure that the models have all been loaded before
+ # deserialization starts (otherwise subclass calls to get_model()
+ # and friends might fail...)
+ models.get_apps()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ """Iteration iterface -- return the next item in the stream"""
+ raise NotImplementedError
+
+class DeserializedObject(object):
+ """
+ A deserialzed model.
+
+ Basically a container for holding the pre-saved deserialized data along
+ with the many-to-many data saved with the object.
+
+ Call ``save()`` to save the object (with the many-to-many data) to the
+ database; call ``save(save_m2m=False)`` to save just the object fields
+ (and not touch the many-to-many stuff.)
+ """
+
+ def __init__(self, obj, m2m_data=None):
+ self.object = obj
+ self.m2m_data = m2m_data
+
+ def __repr__(self):
+ return "<DeserializedObject: %s>" % str(self.object)
+
+ def save(self, save_m2m=True):
+ self.object.save()
+ if self.m2m_data and save_m2m:
+ for accessor_name, object_list in self.m2m_data.items():
+ setattr(self.object, accessor_name, object_list)
+
+ # prevent a second (possibly accidental) call to save() from saving
+ # the m2m data twice.
+ self.m2m_data = None
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
new file mode 100644
index 0000000000..ab8769f237
--- /dev/null
+++ b/django/core/serializers/xml_serializer.py
@@ -0,0 +1,218 @@
+"""
+XML serializer.
+"""
+
+from xml.dom import pulldom
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.core.serializers import base
+from django.db import models
+
+class Serializer(base.Serializer):
+ """
+ Serializes a QuerySet to XML.
+ """
+
+ def start_serialization(self):
+ """
+ Start serialization -- open the XML document and the root element.
+ """
+ self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", "utf-8"))
+ self.xml.startDocument()
+ self.xml.startElement("django-objects", {"version" : "1.0"})
+
+ def end_serialization(self):
+ """
+ End serialization -- end the document.
+ """
+ self.xml.endElement("django-objects")
+ self.xml.endDocument()
+
+ def start_object(self, obj):
+ """
+ Called as each object is handled.
+ """
+ if not hasattr(obj, "_meta"):
+ raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
+
+ self.xml.startElement("object", {
+ "pk" : str(obj._get_pk_val()),
+ "model" : str(obj._meta),
+ })
+
+ def end_object(self, obj):
+ """
+ Called after handling all fields for an object.
+ """
+ self.xml.endElement("object")
+
+ def handle_field(self, obj, field):
+ """
+ Called to handle each field on an object (except for ForeignKeys and
+ ManyToManyFields)
+ """
+ self.xml.startElement("field", {
+ "name" : field.name,
+ "type" : field.get_internal_type()
+ })
+
+ # Get a "string version" of the object's data (this is handled by the
+ # serializer base class). None is handled specially.
+ value = self.get_string_value(obj, field)
+ if value is None:
+ self.xml.addQuickElement("None")
+ else:
+ self.xml.characters(str(value))
+
+ self.xml.endElement("field")
+
+ def handle_fk_field(self, obj, field):
+ """
+ Called to handle a ForeignKey (we need to treat them slightly
+ differently from regular fields).
+ """
+ self._start_relational_field(field)
+ related = getattr(obj, field.name)
+ if related is not None:
+ self.xml.characters(str(related._get_pk_val()))
+ else:
+ self.xml.addQuickElement("None")
+ self.xml.endElement("field")
+
+ def handle_m2m_field(self, obj, field):
+ """
+ Called to handle a ManyToManyField. Related objects are only
+ serialized as references to the object's PK (i.e. the related *data*
+ is not dumped, just the relation).
+ """
+ self._start_relational_field(field)
+ for relobj in getattr(obj, field.name).iterator():
+ self.xml.addQuickElement("object", attrs={"pk" : str(relobj._get_pk_val())})
+ self.xml.endElement("field")
+
+ def _start_relational_field(self, field):
+ """
+ Helper to output the <field> element for relational fields
+ """
+ self.xml.startElement("field", {
+ "name" : field.name,
+ "rel" : field.rel.__class__.__name__,
+ "to" : str(field.rel.to._meta),
+ })
+
+class Deserializer(base.Deserializer):
+ """
+ Deserialize XML.
+ """
+
+ def __init__(self, stream_or_string, **options):
+ super(Deserializer, self).__init__(stream_or_string, **options)
+ self.encoding = self.options.get("encoding", "utf-8")
+ self.event_stream = pulldom.parse(self.stream)
+
+ def next(self):
+ for event, node in self.event_stream:
+ if event == "START_ELEMENT" and node.nodeName == "object":
+ self.event_stream.expandNode(node)
+ return self._handle_object(node)
+ raise StopIteration
+
+ def _handle_object(self, node):
+ """
+ Convert an <object> node to a DeserializedObject.
+ """
+ # Look up the model using the model loading mechanism. If this fails, bail.
+ Model = self._get_model_from_node(node, "model")
+
+ # Start building a data dictionary from the object. If the node is
+ # missing the pk attribute, bail.
+ pk = node.getAttribute("pk")
+ if not pk:
+ raise base.DeserializationError("<object> node is missing the 'pk' attribute")
+ data = {Model._meta.pk.name : pk}
+
+ # Also start building a dict of m2m data (this is saved as
+ # {m2m_accessor_attribute : [list_of_related_objects]})
+ m2m_data = {}
+
+ # Deseralize each field.
+ for field_node in node.getElementsByTagName("field"):
+ # If the field is missing the name attribute, bail (are you
+ # sensing a pattern here?)
+ field_name = field_node.getAttribute("name")
+ if not field_name:
+ raise base.DeserializationError("<field> node is missing the 'name' attribute")
+
+ # Get the field from the Model. This will raise a
+ # FieldDoesNotExist if, well, the field doesn't exist, which will
+ # be propagated correctly.
+ field = Model._meta.get_field(field_name)
+
+ # As is usually the case, relation fields get the special treatment.
+ if field.rel and isinstance(field.rel, models.ManyToManyRel):
+ m2m_data[field.name] = self._handle_m2m_field_node(field_node)
+ elif field.rel and isinstance(field.rel, models.ManyToOneRel):
+ data[field.name] = self._handle_fk_field_node(field_node)
+ else:
+ value = field.to_python(getInnerText(field_node).strip().encode(self.encoding))
+ data[field.name] = value
+
+ # Return a DeserializedObject so that the m2m data has a place to live.
+ return base.DeserializedObject(Model(**data), m2m_data)
+
+ def _handle_fk_field_node(self, node):
+ """
+ Handle a <field> node for a ForeignKey
+ """
+ # Try to set the foreign key by looking up the foreign related object.
+ # If it doesn't exist, set the field to None (which might trigger
+ # validation error, but that's expected).
+ RelatedModel = self._get_model_from_node(node, "to")
+ return RelatedModel.objects.get(pk=getInnerText(node).strip().encode(self.encoding))
+
+ def _handle_m2m_field_node(self, node):
+ """
+ Handle a <field> node for a ManyToManyField
+ """
+ # Load the related model
+ RelatedModel = self._get_model_from_node(node, "to")
+
+ # Look up all the related objects. Using the in_bulk() lookup ensures
+ # that missing related objects don't cause an exception
+ related_ids = [c.getAttribute("pk").encode(self.encoding) for c in node.getElementsByTagName("object")]
+ return RelatedModel._default_manager.in_bulk(related_ids).values()
+
+ def _get_model_from_node(self, node, attr):
+ """
+ Helper to look up a model from a <object model=...> or a <field
+ rel=... to=...> node.
+ """
+ model_identifier = node.getAttribute(attr)
+ if not model_identifier:
+ raise base.DeserializationError(
+ "<%s> node is missing the required '%s' attribute" \
+ % (node.nodeName, attr))
+ try:
+ Model = models.get_model(*model_identifier.split("."))
+ except TypeError:
+ Model = None
+ if Model is None:
+ raise base.DeserializationError(
+ "<%s> node has invalid model identifier: '%s'" % \
+ (node.nodeName, model_identifier))
+ return Model
+
+
+def getInnerText(node):
+ """
+ Get all the inner text of a DOM node (recursively).
+ """
+ # inspired by http://mail.python.org/pipermail/xml-sig/2005-March/011022.html
+ inner_text = []
+ for child in node.childNodes:
+ if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE:
+ inner_text.append(child.data)
+ elif child.nodeType == child.ELEMENT_NODE:
+ inner_text.extend(getInnerText(child))
+ else:
+ pass
+ return "".join(inner_text) \ No newline at end of file
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 73abd018ff..09e8eb3fc0 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -399,10 +399,10 @@ def method_set_order(ordered_obj, self, id_list):
cursor = connection.cursor()
# Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s"
sql = "UPDATE %s SET %s = %%s WHERE %s = %%s AND %s = %%s" % \
- (backend.quote_name(ordered_obj.db_table), backend.quote_name('_order'),
- backend.quote_name(ordered_obj.order_with_respect_to.column),
- backend.quote_name(ordered_obj.pk.column))
- rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name)
+ (backend.quote_name(ordered_obj._meta.db_table), backend.quote_name('_order'),
+ backend.quote_name(ordered_obj._meta.order_with_respect_to.column),
+ backend.quote_name(ordered_obj._meta.pk.column))
+ rel_val = getattr(self, ordered_obj._meta.order_with_respect_to.rel.field_name)
cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)])
transaction.commit_unless_managed()
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index bc6042ae59..8b000d3c2a 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -411,7 +411,7 @@ class DateField(Field):
def get_db_prep_lookup(self, lookup_type, value):
if lookup_type == 'range':
value = [str(v) for v in value]
- elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne'):
+ elif lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne') and hasattr(value, 'strftime'):
value = value.strftime('%Y-%m-%d')
else:
value = str(value)
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index 10ff3bb8d8..3d34845147 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -32,18 +32,25 @@ def get_apps():
_app_errors[app_name] = e
return _app_list
-def get_app(app_label):
- "Returns the module containing the models for the given app_label."
+def get_app(app_label, emptyOK = False):
+ "Returns the module containing the models for the given app_label. If the app has no models in it and 'emptyOK' is True, returns None."
get_apps() # Run get_apps() to populate the _app_list cache. Slightly hackish.
for app_name in settings.INSTALLED_APPS:
if app_label == app_name.split('.')[-1]:
- return load_app(app_name)
+ mod = load_app(app_name)
+ if mod is None:
+ if emptyOK:
+ return None
+ else:
+ return mod
raise ImproperlyConfigured, "App with label %s could not be found" % app_label
def load_app(app_name):
"Loads the app with the provided fully qualified name, and returns the model module."
global _app_list
mod = __import__(app_name, '', '', ['models'])
+ if not hasattr(mod, 'models'):
+ return None
if mod.models not in _app_list:
_app_list.append(mod.models)
return mod.models
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index f679c5492c..46a1710c1c 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -69,8 +69,11 @@ class Manager(object):
def get(self, *args, **kwargs):
return self.get_query_set().get(*args, **kwargs)
- def get_or_create(self, *args, **kwargs):
- return self.get_query_set().get_or_create(*args, **kwargs)
+ def get_or_create(self, **kwargs):
+ return self.get_query_set().get_or_create(**kwargs)
+
+ def create(self, **kwargs):
+ return self.get_query_set().create(**kwargs)
def filter(self, *args, **kwargs):
return self.get_query_set().filter(*args, **kwargs)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index e826efa779..efbe68cb66 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -204,6 +204,15 @@ class QuerySet(object):
raise self.model.DoesNotExist, "%s matching query does not exist." % self.model._meta.object_name
assert len(obj_list) == 1, "get() returned more than one %s -- it returned %s! Lookup parameters were %s" % (self.model._meta.object_name, len(obj_list), kwargs)
return obj_list[0]
+
+ def create(self, **kwargs):
+ """
+ Create a new object with the given kwargs, saving it to the database
+ and returning the created object.
+ """
+ obj = self.model(**kwargs)
+ obj.save()
+ return obj
def get_or_create(self, **kwargs):
"""
diff --git a/docs/authentication.txt b/docs/authentication.txt
index 79a4ed0875..3edbc21f7a 100644
--- a/docs/authentication.txt
+++ b/docs/authentication.txt
@@ -267,17 +267,25 @@ previous section). You can tell them apart with ``is_anonymous()``, like so::
How to log a user in
--------------------
-To log a user in, do the following within a view::
+Depending on your task, you'll probably want to make sure to validate the
+user's username and password before you log them in. The easiest way to do so
+is to use the built-in ``authenticate`` and ``login`` functions from within a
+view::
- from django.contrib.auth.models import SESSION_KEY
- request.session[SESSION_KEY] = some_user.id
+ from django.contrib.auth import authenticate, login
+ username = request.POST['username']
+ password = request.POST['password']
+ user = authenticate(username=username, password=password)
+ if user is not None:
+ login(request, user)
-Because this uses sessions, you'll need to make sure you have
-``SessionMiddleware`` enabled. See the `session documentation`_ for more
-information.
+``authenticate`` checks the username and password. If they are valid it
+returns a user object, otherwise it returns ``None``. ``login`` makes it so
+your users don't have send a username and password for every request. Because
+the ``login`` function uses sessions, you'll need to make sure you have
+``SessionMiddleware`` enabled. See the `session documentation`_ for
+more information.
-This assumes ``some_user`` is your ``User`` instance. Depending on your task,
-you'll probably want to make sure to validate the user's username and password.
Limiting access to logged-in users
----------------------------------
@@ -672,3 +680,84 @@ Finally, note that this messages framework only works with users in the user
database. To send messages to anonymous users, use the `session framework`_.
.. _session framework: http://www.djangoproject.com/documentation/sessions/
+
+Other Authentication Sources
+============================
+
+Django supports other authentication sources as well. You can even use
+multiple sources at the same time.
+
+Using multiple backends
+-----------------------
+
+The list of backends to use is controlled by the ``AUTHENTICATION_BACKENDS``
+setting. This should be a tuple of python path names. It defaults to
+``('django.contrib.auth.backends.ModelBackend',)``. To add additional backends
+just add them to your settings.py file. Ordering matters, so if the same
+username and password is valid in multiple backends, the first one in the
+list will return a user object, and the remaining ones won't even get a chance.
+
+Writing an authentication backend
+---------------------------------
+
+An authentication backend is a class that implements 2 methods:
+``get_user(id)`` and ``authenticate(**credentials)``. The ``get_user`` method
+takes an id, which could be a username, and database id, whatever, and returns
+a user object. The ``authenticate`` method takes credentials as keyword
+arguments. Many times it will just look like this::
+
+ class MyBackend:
+ def authenticate(username=None, password=None):
+ # check the username/password and return a user
+
+but it could also authenticate a token like so::
+
+ class MyBackend:
+ def authenticate(token=None):
+ # check the token and return a user
+
+Regardless, ``authenticate`` should check the credentials it gets, and if they
+are valid, it should return a user object that matches those credentials.
+
+The Django admin system is tightly coupled to the Django User object described
+at the beginning of this document. For now, the best way to deal with this is
+to create a Django User object for each user that exists for your backend
+(i.e. in your LDAP directory, your external SQL database, etc.) You can either
+write a script to do this in advance, or your ``authenticate`` method can do
+it the first time a user logs in. Here's an example backend that
+authenticates against a username and password variable defined in your
+``settings.py`` file and creates a Django user object the first time they
+authenticate::
+
+ from django.conf import settings
+ from django.contrib.auth.models import User, check_password
+
+ class SettingsBackend:
+ """
+ Authenticate against vars in settings.py Use the login name, and a hash
+ of the password. For example:
+
+ ADMIN_LOGIN = 'admin'
+ ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de'
+ """
+ def authenticate(self, username=None, password=None):
+ login_valid = (settings.ADMIN_LOGIN == username)
+ pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
+ if login_valid and pwd_valid:
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ # Create a new user. Note that we can set password to anything
+ # as it won't be checked, the password from settings.py will.
+ user = User(username=username, password='get from settings.py')
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+ return user
+ return None
+
+ def get_user(self, user_id):
+ try:
+ return User.objects.get(pk=user_id)
+ except User.DoesNotExist:
+ return None
diff --git a/docs/db-api.txt b/docs/db-api.txt
index 5108949184..15b70ee028 100644
--- a/docs/db-api.txt
+++ b/docs/db-api.txt
@@ -60,6 +60,10 @@ the database until you explicitly call ``save()``.
The ``save()`` method has no return value.
+To create an object and save it all in one step see the `create`__ method.
+
+__ `create(**kwargs)`_
+
Auto-incrementing primary keys
------------------------------
@@ -705,6 +709,20 @@ The ``DoesNotExist`` exception inherits from
except ObjectDoesNotExist:
print "Either the entry or blog doesn't exist."
+``create(**kwargs)``
+~~~~~~~~~~~~~~~~~~~~
+
+A convenience method for creating an object and saving it all in one step. Thus::
+
+ p = Person.objects.create(first_name="Bruce", last_name="Springsteen")
+
+and::
+
+ p = Person(first_name="Bruce", last_name="Springsteen")
+ p.save()
+
+are equivalent.
+
``get_or_create(**kwargs)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/serialization.txt b/docs/serialization.txt
new file mode 100644
index 0000000000..41954b7a0d
--- /dev/null
+++ b/docs/serialization.txt
@@ -0,0 +1,85 @@
+==========================
+Serializing Django objects
+==========================
+
+.. note::
+
+ This API is currently under heavy development and may change --
+ perhaps drastically -- in the future.
+
+ You have been warned.
+
+Django's serialization framework provides a mechanism for "translating" Django
+objects into other formats. Usually these other formats will be text-based and
+used for sending Django objects over a wire, but it's possible for a
+serializer to handle any format (text-based or not).
+
+Serializing data
+----------------
+
+At the highest level, serializing data is a very simple operation::
+
+ from django.core import serializers
+ data = serializers.serialize("xml", SomeModel.objects.all())
+
+The arguments to the ``serialize`` function are the format to serialize the
+data to (see `Serialization formats`_) and a QuerySet_ to serialize.
+(Actually, the second argument can be any iterator that yields Django objects,
+but it'll almost always be a QuerySet).
+
+.. _QuerySet: ../db_api/#retrieving-objects
+
+You can also use a serializer object directly::
+
+ xml_serializer = serializers.get_serializer("xml")
+ xml_serializer.serialize(queryset)
+ data = xml_serializer.getvalue()
+
+This is useful if you want to serialize data directly to a file-like object
+(which includes a HTTPResponse_)::
+
+ out = open("file.xml", "w")
+ xml_serializer.serialize(SomeModel.objects.all(), stream=out)
+
+.. _HTTPResponse: ../request_response/#httpresponse-objects
+
+Deserializing data
+------------------
+
+Deserializing data is also a fairly simple operation::
+
+ for obj in serializers.deserialize("xml", data):
+ do_something_with(obj)
+
+As you can see, the ``deserialize`` function takes the same format argument as
+``serialize``, a string or stream of data, and returns an iterator.
+
+However, here it gets slightly complicated. The objects returned by the
+``deserialize`` iterator *aren't* simple Django objects. Instead, they are
+special ``DeserializedObject`` instances that wrap a created -- but unsaved --
+object and any associated relationship data.
+
+Calling ``DeserializedObject.save()`` saves the object to the database.
+
+This ensures that deserializing is a non-destructive operation even if the
+data in your serialized representation doesn't match what's currently in the
+database. Usually, working with these ``DeserializedObject`` instances looks
+something like::
+
+ for deserialized_object in serializers.deserialize("xml", data):
+ if object_should_be_saved(deserialized_object):
+ obj.save()
+
+In other words, the usual use is to examine the deserialized objects to make
+sure that they are "appropriate" for saving before doing so. Of course, if you trust your data source you could just save the object and move on.
+
+The Django object itself can be inspected as ``deserialized_object.object``.
+
+Serialization formats
+---------------------
+
+Django "ships" with a few included serializers, and there's a simple API for creating and registering your own...
+
+.. note::
+
+ ... which will be documented once the API is stable :)
diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py
index a4de0f9a81..78d943eb97 100644
--- a/tests/modeltests/basic/models.py
+++ b/tests/modeltests/basic/models.py
@@ -347,4 +347,9 @@ API_TESTS += """
>>> a101 = Article.objects.get(pk=101)
>>> a101.headline
'Article 101'
+
+# You can create saved objects in a single step
+>>> a10 = Article.objects.create(headline="Article 10", pub_date=datetime(2005, 7, 31, 12, 30, 45))
+>>> Article.objects.get(headline="Article 10")
+<Article: Article 10>
"""
diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py
index e0c4850ba2..a2c0a14158 100644
--- a/tests/modeltests/lookup/models.py
+++ b/tests/modeltests/lookup/models.py
@@ -58,6 +58,10 @@ Article 4
>>> Article.objects.filter(headline__startswith='Blah blah').count()
0L
+# Date and date/time lookups can also be done with strings.
+>>> Article.objects.filter(pub_date__exact='2005-07-27 00:00:00').count()
+3L
+
# in_bulk() takes a list of IDs and returns a dictionary mapping IDs
# to objects.
>>> Article.objects.in_bulk([1, 2])
diff --git a/tests/modeltests/serializers/__init__.py b/tests/modeltests/serializers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/modeltests/serializers/__init__.py
diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py
new file mode 100644
index 0000000000..8c9483beba
--- /dev/null
+++ b/tests/modeltests/serializers/models.py
@@ -0,0 +1,94 @@
+"""
+XXX. Serialization
+
+``django.core.serializers`` provides interfaces to converting Django querysets
+to and from "flat" data (i.e. strings).
+"""
+
+from django.db import models
+
+class Category(models.Model):
+ name = models.CharField(maxlength=20)
+
+ class Meta:
+ ordering = ('name',)
+
+ def __str__(self):
+ return self.name
+
+class Author(models.Model):
+ name = models.CharField(maxlength=20)
+
+ class Meta:
+ ordering = ('name',)
+
+ def __str__(self):
+ return self.name
+
+class Article(models.Model):
+ author = models.ForeignKey(Author)
+ headline = models.CharField(maxlength=50)
+ pub_date = models.DateTimeField()
+ categories = models.ManyToManyField(Category)
+
+ class Meta:
+ ordering = ('pub_date',)
+
+ def __str__(self):
+ return self.headline
+
+API_TESTS = """
+# Create some data:
+>>> from datetime import datetime
+>>> sports = Category(name="Sports")
+>>> music = Category(name="Music")
+>>> op_ed = Category(name="Op-Ed")
+>>> sports.save(); music.save(); op_ed.save()
+
+>>> joe = Author(name="Joe")
+>>> jane = Author(name="Jane")
+>>> joe.save(); jane.save()
+
+>>> a1 = Article(
+... author = jane,
+... headline = "Poker has no place on ESPN",
+... pub_date = datetime(2006, 6, 16, 11, 00))
+>>> a2 = Article(
+... author = joe,
+... headline = "Time to reform copyright",
+... pub_date = datetime(2006, 6, 16, 13, 00))
+>>> a1.save(); a2.save()
+>>> a1.categories = [sports, op_ed]
+>>> a2.categories = [music, op_ed]
+
+# Serialize a queryset to XML
+>>> from django.core import serializers
+>>> xml = serializers.serialize("xml", Article.objects.all())
+
+# The output is valid XML
+>>> from xml.dom import minidom
+>>> dom = minidom.parseString(xml)
+
+# Deserializing has a similar interface, except that special DeserializedObject
+# instances are returned. This is because data might have changed in the
+# database since the data was serialized (we'll simulate that below).
+>>> for obj in serializers.deserialize("xml", xml):
+... print obj
+<DeserializedObject: Poker has no place on ESPN>
+<DeserializedObject: Time to reform copyright>
+
+# Deserializing data with different field values doesn't change anything in the
+# database until we call save():
+>>> xml = xml.replace("Poker has no place on ESPN", "Poker has no place on television")
+>>> objs = list(serializers.deserialize("xml", xml))
+
+# Even those I deserialized, the database hasn't been touched
+>>> Article.objects.all()
+[<Article: Poker has no place on ESPN>, <Article: Time to reform copyright>]
+
+# But when I save, the data changes as you might except.
+>>> objs[0].save()
+>>> Article.objects.all()
+[<Article: Poker has no place on television>, <Article: Time to reform copyright>]
+
+"""