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 "" % 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 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 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(" 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(" 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 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 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 or a 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") + """ 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 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 + + + +# 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() +[, ] + +# But when I save, the data changes as you might except. +>>> objs[0].save() +>>> Article.objects.all() +[, ] + +"""