From 7cc0baf89d490c92ef3f1dc909b8090191a1294b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 4 Jun 2012 14:17:28 +0800 Subject: [PATCH] Added model Meta option for swappable models, and made auth.User a swappable model --- django/conf/global_settings.py | 2 + django/contrib/admin/models.py | 6 +- django/contrib/auth/models.py | 92 ++++++++++--------- django/contrib/comments/models.py | 17 ++-- django/core/management/commands/sqlall.py | 1 + django/core/management/commands/syncdb.py | 2 +- django/core/management/commands/validate.py | 1 + django/core/management/sql.py | 11 ++- django/core/management/validation.py | 17 +++- django/db/backends/creation.py | 16 ++-- django/db/models/base.py | 15 ++- django/db/models/fields/related.py | 37 ++++++-- django/db/models/loading.py | 1 + django/db/models/options.py | 16 +++- django/test/utils.py | 7 +- docs/ref/settings.txt | 9 ++ docs/topics/auth.txt | 7 ++ .../invalid_models/invalid_models/models.py | 88 +++++++++++++++++- tests/modeltests/invalid_models/tests.py | 9 +- 19 files changed, 265 insertions(+), 89 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 4711baad66..a78b9187d8 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -488,6 +488,8 @@ PROFANITIES_LIST = () # AUTHENTICATION # ################## +AUTH_USER_MODEL = 'auth.User' + AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) LOGIN_URL = '/accounts/login/' diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index 0e5b8a79da..9e8ffe47c3 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -1,6 +1,6 @@ from django.db import models +from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User from django.contrib.admin.util import quote from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_unicode @@ -10,14 +10,16 @@ ADDITION = 1 CHANGE = 2 DELETION = 3 + class LogEntryManager(models.Manager): def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): e = self.model(None, None, user_id, content_type_id, smart_unicode(object_id), object_repr[:200], action_flag, change_message) e.save() + class LogEntry(models.Model): action_time = models.DateTimeField(_('action time'), auto_now=True) - user = models.ForeignKey(User) + user = models.ForeignKey(settings.AUTH_USER_MODEL) content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True) object_repr = models.CharField(_('object repr'), max_length=200) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 090b6d40c8..ec2ee19d3b 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -93,6 +93,7 @@ class GroupManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) + class Group(models.Model): """ Groups are a generic way of categorizing users to apply permissions, or @@ -197,8 +198,6 @@ def _user_get_all_permissions(user, obj): def _user_has_perm(user, perm, obj): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_perm"): if obj is not None: @@ -211,8 +210,6 @@ def _user_has_perm(user, perm, obj): def _user_has_module_perms(user, app_label): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_module_perms"): if backend.has_module_perms(user, app_label): @@ -220,7 +217,54 @@ def _user_has_module_perms(user, app_label): return False -class User(models.Model): +class AbstractBaseUser(models.Model): + password = models.CharField(_('password'), max_length=128) + + class Meta: + abstract = True + + def is_anonymous(self): + """ + Always returns False. This is a way of comparing User objects to + anonymous users. + """ + return False + + def is_authenticated(self): + """ + Always return True. This is a way to tell if the user has been + authenticated in templates. + """ + return True + + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + """ + Returns a boolean of whether the raw_password was correct. Handles + hashing formats behind the scenes. + """ + def setter(raw_password): + self.set_password(raw_password) + self.save() + return check_password(raw_password, self.password, setter) + + def set_unusable_password(self): + # Sets a value that will never be a valid hash + self.password = make_password(None) + + def has_usable_password(self): + return is_password_usable(self.password) + + def get_full_name(self): + raise NotImplementedError() + + def get_short_name(self): + raise NotImplementedError() + + +class User(AbstractBaseUser): """ Users within the Django authentication system are represented by this model. @@ -233,7 +277,6 @@ class User(models.Model): first_name = models.CharField(_('first name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True) email = models.EmailField(_('e-mail address'), blank=True) - password = models.CharField(_('password'), max_length=128) is_staff = models.BooleanField(_('staff status'), default=False, help_text=_('Designates whether the user can log into this admin ' 'site.')) @@ -257,6 +300,7 @@ class User(models.Model): class Meta: verbose_name = _('user') verbose_name_plural = _('users') + swappable = 'AUTH_USER_MODEL' def __unicode__(self): return self.username @@ -267,20 +311,6 @@ class User(models.Model): def get_absolute_url(self): return "/users/%s/" % urllib.quote(smart_str(self.username)) - def is_anonymous(self): - """ - Always returns False. This is a way of comparing User objects to - anonymous users. - """ - return False - - def is_authenticated(self): - """ - Always return True. This is a way to tell if the user has been - authenticated in templates. - """ - return True - def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. @@ -288,26 +318,6 @@ class User(models.Model): full_name = u'%s %s' % (self.first_name, self.last_name) return full_name.strip() - def set_password(self, raw_password): - self.password = make_password(raw_password) - - def check_password(self, raw_password): - """ - Returns a boolean of whether the raw_password was correct. Handles - hashing formats behind the scenes. - """ - def setter(raw_password): - self.set_password(raw_password) - self.save() - return check_password(raw_password, self.password, setter) - - def set_unusable_password(self): - # Sets a value that will never be a valid hash - self.password = make_password(None) - - def has_usable_password(self): - return is_password_usable(self.password) - def get_group_permissions(self, obj=None): """ Returns a list of permission strings that this user has through his/her @@ -428,7 +438,7 @@ class AnonymousUser(object): return not self.__eq__(other) def __hash__(self): - return 1 # instances always return the same hash value + return 1 # instances always return the same hash value def save(self): raise NotImplementedError diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index 475b3c8dea..bece36e318 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -1,15 +1,15 @@ -from django.contrib.auth.models import User +from django.conf import settings from django.contrib.comments.managers import CommentManager from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site -from django.db import models from django.core import urlresolvers +from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from django.conf import settings -COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) +COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) + class BaseCommentAbstractModel(models.Model): """ @@ -39,6 +39,7 @@ class BaseCommentAbstractModel(models.Model): args=(self.content_type_id, self.object_pk) ) + class Comment(BaseCommentAbstractModel): """ A user comment about some object. @@ -47,7 +48,7 @@ class Comment(BaseCommentAbstractModel): # Who posted this comment? If ``user`` is set then it was an authenticated # user; otherwise at least user_name should have been set and the comment # was posted by a non-authenticated user. - user = models.ForeignKey(User, verbose_name=_('user'), + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), blank=True, null=True, related_name="%(class)s_comments") user_name = models.CharField(_("user's name"), max_length=50, blank=True) user_email = models.EmailField(_("user's email address"), blank=True) @@ -115,6 +116,7 @@ class Comment(BaseCommentAbstractModel): def _get_name(self): return self.userinfo["name"] + def _set_name(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -124,6 +126,7 @@ class Comment(BaseCommentAbstractModel): def _get_email(self): return self.userinfo["email"] + def _set_email(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -133,6 +136,7 @@ class Comment(BaseCommentAbstractModel): def _get_url(self): return self.userinfo["url"] + def _set_url(self, val): self.user_url = val url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") @@ -153,6 +157,7 @@ class Comment(BaseCommentAbstractModel): } return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d + class CommentFlag(models.Model): """ Records a flag on a comment. This is intentionally flexible; right now, a @@ -166,7 +171,7 @@ class CommentFlag(models.Model): design users are only allowed to flag a comment with a given flag once; if you want rating look elsewhere. """ - user = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags") + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags") comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags") flag = models.CharField(_('flag'), max_length=30, db_index=True) flag_date = models.DateTimeField(_('date'), default=None) diff --git a/django/core/management/commands/sqlall.py b/django/core/management/commands/sqlall.py index ab702bfffb..15966ece66 100644 --- a/django/core/management/commands/sqlall.py +++ b/django/core/management/commands/sqlall.py @@ -4,6 +4,7 @@ from django.core.management.base import AppCommand from django.core.management.sql import sql_all from django.db import connections, DEFAULT_DB_ALIAS + class Command(AppCommand): help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)." diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index 88caea152c..973441dc1c 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -68,6 +68,7 @@ class Command(NoArgsCommand): if router.allow_syncdb(db, m)]) for app in models.get_apps() ] + def model_installed(model): opts = model._meta converter = connection.introspection.table_name_converter @@ -101,7 +102,6 @@ class Command(NoArgsCommand): cursor.execute(statement) tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - transaction.commit_unless_managed(using=db) # Send the post_syncdb signal, so individual apps can do whatever they need diff --git a/django/core/management/commands/validate.py b/django/core/management/commands/validate.py index 760d41c5bf..0dec3ea8b9 100644 --- a/django/core/management/commands/validate.py +++ b/django/core/management/commands/validate.py @@ -1,5 +1,6 @@ from django.core.management.base import NoArgsCommand + class Command(NoArgsCommand): help = "Validates all installed models." diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 53d9736293..c10c100214 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -6,6 +6,7 @@ from django.core.management.base import CommandError from django.db import models from django.db.models import get_models + def sql_create(app, style, connection): "Returns a list of the CREATE TABLE SQL statements for the given app." @@ -52,6 +53,7 @@ def sql_create(app, style, connection): return final_output + def sql_delete(app, style, connection): "Returns a list of the DROP TABLE SQL statements for the given app." @@ -80,7 +82,7 @@ def sql_delete(app, style, connection): opts = model._meta for f in opts.local_fields: if f.rel and f.rel.to not in to_delete: - references_to_delete.setdefault(f.rel.to, []).append( (model, f) ) + references_to_delete.setdefault(f.rel.to, []).append((model, f)) to_delete.add(model) @@ -94,7 +96,8 @@ def sql_delete(app, style, connection): cursor.close() connection.close() - return output[::-1] # Reverse it, to deal with table dependencies. + return output[::-1] # Reverse it, to deal with table dependencies. + def sql_flush(style, connection, only_django=False): """ @@ -112,6 +115,7 @@ def sql_flush(style, connection, only_django=False): ) return statements + def sql_custom(app, style, connection): "Returns a list of the custom table modifying SQL statements for the given app." output = [] @@ -123,6 +127,7 @@ def sql_custom(app, style, connection): return output + def sql_indexes(app, style, connection): "Returns a list of the CREATE INDEX SQL statements for all models in the given app." output = [] @@ -130,10 +135,12 @@ def sql_indexes(app, style, connection): output.extend(connection.creation.sql_indexes_for_model(model, style)) return output + def sql_all(app, style, connection): "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module." return sql_create(app, style, connection) + sql_custom(app, style, connection) + sql_indexes(app, style, connection) + def custom_sql_for_model(model, style, connection): opts = model._meta app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 272ac5be41..f613009e98 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -3,6 +3,7 @@ import sys from django.core.management.color import color_style from django.utils.itercompat import is_iterable + class ModelErrorCollection: def __init__(self, outfile=sys.stdout): self.errors = [] @@ -13,6 +14,7 @@ class ModelErrorCollection: self.errors.append((context, error)) self.outfile.write(self.style.ERROR("%s: %s\n" % (context, error))) + def get_validation_errors(outfile, app=None): """ Validates all models that are part of the specified app. If no app name is provided, @@ -54,7 +56,7 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name) if isinstance(f, models.DecimalField): decimalp_ok, mdigits_ok = False, False - decimalp_msg ='"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' + decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' try: decimal_places = int(f.decimal_places) if decimal_places < 0: @@ -121,6 +123,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, (str, unicode)): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Make sure the related field specified by a ForeignKey is unique if not f.rel.to._meta.get_field(f.rel.field_name).unique: e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__)) @@ -163,6 +169,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, (str, unicode)): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Check that the field is not set to unique. ManyToManyFields do not support unique. if f.unique: e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) @@ -174,7 +184,7 @@ def get_validation_errors(outfile, app=None): seen_from, seen_to, seen_self = False, False, 0 for inter_field in f.rel.through._meta.fields: rel_to = getattr(inter_field.rel, 'to', None) - if from_model == to_model: # relation to self + if from_model == to_model: # relation to self if rel_to == from_model: seen_self += 1 if seen_self > 2: @@ -276,7 +286,8 @@ def get_validation_errors(outfile, app=None): # Check ordering attribute. if opts.ordering: for field_name in opts.ordering: - if field_name == '?': continue + if field_name == '?': + continue if field_name.startswith('-'): field_name = field_name[1:] if opts.order_with_respect_to and field_name == '_order': diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index ba90cb970b..eb50dc0cbb 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -34,7 +34,7 @@ class BaseDatabaseCreation(object): (list_of_sql, pending_references_dict) """ opts = model._meta - if not opts.managed or opts.proxy: + if not opts.managed or opts.proxy or opts.swapped: return [], {} final_output = [] table_output = [] @@ -86,9 +86,9 @@ class BaseDatabaseCreation(object): full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] - for i, line in enumerate(table_output): # Combine and add commas. + for i, line in enumerate(table_output): # Combine and add commas. full_statement.append( - ' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + ' %s%s' % (line, i < len(table_output) - 1 and ',' or '')) full_statement.append(')') if opts.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( @@ -137,11 +137,11 @@ class BaseDatabaseCreation(object): """ from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + opts = model._meta + if not opts.managed or opts.proxy or opts.swapped: return [] qn = self.connection.ops.quote_name final_output = [] - opts = model._meta if model in pending_references: for rel_class, f in pending_references[model]: rel_opts = rel_class._meta @@ -166,7 +166,7 @@ class BaseDatabaseCreation(object): """ Returns the CREATE INDEX SQL statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] for f in model._meta.local_fields: @@ -205,7 +205,7 @@ class BaseDatabaseCreation(object): Return the DROP TABLE and restraint dropping statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] # Drop the table now qn = self.connection.ops.quote_name @@ -222,7 +222,7 @@ class BaseDatabaseCreation(object): def sql_remove_table_constraints(self, model, references_to_delete, style): from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] qn = self.connection.ops.quote_name diff --git a/django/db/models/base.py b/django/db/models/base.py index 13238fc9dc..c35979cd6a 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -3,7 +3,7 @@ import sys from functools import update_wrapper from future_builtins import zip -import django.db.models.manager # Imported to register signal handler. +import django.db.models.manager # Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS) @@ -230,6 +230,7 @@ class ModelBase(type): if opts.order_with_respect_to: cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) + # defer creating accessors on the foreign class until we are # certain it has been created def make_foreign_order_accessors(field, model, cls): @@ -260,6 +261,7 @@ class ModelBase(type): signals.class_prepared.send(sender=cls) + class ModelState(object): """ A class for storing instance state @@ -271,6 +273,7 @@ class ModelState(object): # This impacts validation only; it has no effect on the actual save. self.adding = True + class Model(object): __metaclass__ = ModelBase _deferred = False @@ -585,7 +588,6 @@ class Model(object): signals.post_save.send(sender=origin, instance=self, created=(not record_exists), update_fields=update_fields, raw=raw, using=using) - save_base.alters_data = True def delete(self, using=None): @@ -609,7 +611,7 @@ class Model(object): order = not is_next and '-' or '' param = smart_str(getattr(self, field.attname)) q = Q(**{'%s__%s' % (field.name, op): param}) - q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk}) + q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk}) qs = self.__class__._default_manager.using(self._state.db).filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order) try: return qs[0] @@ -802,7 +804,7 @@ class Model(object): field = opts.get_field(field_name) field_label = capfirst(field.verbose_name) # Insert the error into the error dict, very sneaky - return field.error_messages['unique'] % { + return field.error_messages['unique'] % { 'model_name': unicode(model_name), 'field_label': unicode(field_label) } @@ -810,7 +812,7 @@ class Model(object): else: field_labels = map(lambda f: capfirst(opts.get_field(f).verbose_name), unique_check) field_labels = get_text_list(field_labels, _('and')) - return _(u"%(model_name)s with this %(field_label)s already exists.") % { + return _(u"%(model_name)s with this %(field_label)s already exists.") % { 'model_name': unicode(model_name), 'field_label': unicode(field_labels) } @@ -915,6 +917,7 @@ def get_absolute_url(opts, func, self, *args, **kwargs): class Empty(object): pass + def simple_class_factory(model, attrs): """Used to unpickle Models without deferred fields. @@ -924,6 +927,7 @@ def simple_class_factory(model, attrs): """ return model + def model_unpickle(model, attrs, factory): """ Used to unpickle Model subclasses with deferred fields. @@ -932,5 +936,6 @@ def model_unpickle(model, attrs, factory): return cls.__new__(cls) model_unpickle.__safe_for_unpickle__ = True + def subclass_exception(name, parents, module): return type(name, parents, {'__module__': module}) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index c4f95a12d2..ee71f509aa 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -20,6 +20,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self' pending_lookups = {} + def add_lazy_relation(cls, field, relation, operation): """ Adds a lookup on ``cls`` when a related field is defined using a string, @@ -76,6 +77,7 @@ def add_lazy_relation(cls, field, relation, operation): value = (cls, field, operation) pending_lookups.setdefault(key, []).append(value) + def do_pending_lookups(sender, **kwargs): """ Handle any pending relations to the sending model. Sent from class_prepared. @@ -86,6 +88,7 @@ def do_pending_lookups(sender, **kwargs): signals.class_prepared.connect(do_pending_lookups) + #HACK class RelatedField(object): def contribute_to_class(self, cls, name): @@ -219,6 +222,7 @@ class RelatedField(object): # "related_name" option. return self.rel.related_name or self.opts.object_name.lower() + class SingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -305,6 +309,7 @@ class SingleRelatedObjectDescriptor(object): setattr(instance, self.cache_name, value) setattr(value, self.related.field.get_cache_name(), instance) + class ReverseSingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -429,6 +434,7 @@ class ReverseSingleRelatedObjectDescriptor(object): if value is not None and not self.field.rel.multiple: setattr(value, self.field.related.get_cache_name(), instance) + class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -659,7 +665,7 @@ def create_many_related_manager(superclass, rel): for obj in objs: if isinstance(obj, self.model): if not router.allow_relation(obj, self.instance): - raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % + raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % (obj, self.instance._state.db, obj._state.db)) new_ids.add(obj.pk) elif isinstance(obj, Model): @@ -751,6 +757,7 @@ def create_many_related_manager(superclass, rel): return ManyRelatedManager + class ManyRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -859,12 +866,13 @@ class ReverseManyRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) + class ManyToOneRel(object): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): try: to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT self.to, self.field_name = to, field_name self.related_name = related_name @@ -890,6 +898,7 @@ class ManyToOneRel(object): self.field_name) return data[0] + class OneToOneRel(ManyToOneRel): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): @@ -899,6 +908,7 @@ class OneToOneRel(ManyToOneRel): ) self.multiple = False + class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None): @@ -923,16 +933,18 @@ class ManyToManyRel(object): """ return self.to._meta.pk + class ForeignKey(RelatedField, Field): empty_strings_allowed = False default_error_messages = { 'invalid': _('Model %(model)s with pk %(pk)r does not exist.') } description = _("Foreign Key (type determined by related field)") + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: - to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + to._meta.object_name.lower() + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) @@ -1049,6 +1061,7 @@ class ForeignKey(RelatedField, Field): return IntegerField().db_type(connection=connection) return rel_field.db_type(connection=connection) + class OneToOneField(ForeignKey): """ A OneToOneField is essentially the same as a ForeignKey, with the exception @@ -1057,6 +1070,7 @@ class OneToOneField(ForeignKey): rather than returning a list. """ description = _("One-to-one relationship") + def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -1076,12 +1090,14 @@ class OneToOneField(ForeignKey): else: setattr(instance, self.attname, data) + def create_many_to_many_intermediary_model(field, klass): from django.db import models managed = True if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: to_model = field.rel.to to = to_model.split('.')[-1] + def set_managed(field, model, cls): field.rel.through._meta.managed = model._meta.managed or cls._meta.managed add_lazy_relation(klass, field, to_model, set_managed) @@ -1118,12 +1134,14 @@ def create_many_to_many_intermediary_model(field, klass): to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) }) + class ManyToManyField(RelatedField, Field): description = _("Many-to-many relationship") + def __init__(self, to, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) # Python 2.6 and earlier require dictionary keys to be of str type, # not unicode and class names must be ASCII (in Python 2.x), so we @@ -1134,7 +1152,7 @@ class ManyToManyField(RelatedField, Field): kwargs['rel'] = ManyToManyRel(to, related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - symmetrical=kwargs.pop('symmetrical', to==RECURSIVE_RELATIONSHIP_CONSTANT), + symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None)) self.db_table = kwargs.pop('db_table', None) @@ -1165,7 +1183,7 @@ class ManyToManyField(RelatedField, Field): if hasattr(self, cache_attr): return getattr(self, cache_attr) for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.model: setattr(self, cache_attr, getattr(f, attr)) return getattr(self, cache_attr) @@ -1176,7 +1194,7 @@ class ManyToManyField(RelatedField, Field): return getattr(self, cache_attr) found = False for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: if related.model == related.parent_model: # If this is an m2m-intermediate to self, # the first foreign key you find will be @@ -1221,7 +1239,8 @@ class ManyToManyField(RelatedField, Field): # The intermediate m2m model is not auto created if: # 1) There is a manually specified intermediate, or # 2) The class owning the m2m field is abstract. - if not self.rel.through and not cls._meta.abstract: + # 3) The class owning the m2m field has been swapped out. + if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped: self.rel.through = create_many_to_many_intermediary_model(self, cls) # Add the descriptor for the m2m relation diff --git a/django/db/models/loading.py b/django/db/models/loading.py index c34468643f..69e9126abc 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -13,6 +13,7 @@ import threading __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') + class AppCache(object): """ A cache that stores installed applications and their models. Used to diff --git a/django/db/models/options.py b/django/db/models/options.py index 44f8891942..ac9b3cbf66 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -17,7 +17,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy', 'auto_created') + 'abstract', 'managed', 'proxy', 'swappable', 'auto_created') + class Options(object): def __init__(self, meta, app_label=None): @@ -27,8 +28,8 @@ class Options(object): self.verbose_name_plural = None self.db_table = '' self.ordering = [] - self.unique_together = [] - self.permissions = [] + self.unique_together = [] + self.permissions = [] self.object_name, self.app_label = None, app_label self.get_latest_by = None self.order_with_respect_to = None @@ -50,6 +51,7 @@ class Options(object): # in the end of the proxy_for_model chain. In particular, for # concrete models, the concrete_model is always the class itself. self.concrete_model = None + self.swappable = None self.parents = SortedDict() self.duplicate_targets = {} self.auto_created = False @@ -213,6 +215,14 @@ class Options(object): return raw verbose_name_raw = property(verbose_name_raw) + def _swapped(self): + """ + Has this model been swapped out for another? + """ + model_label = '%s.%s' % (self.app_label, self.object_name) + return self.swappable and getattr(settings, self.swappable, None) not in (None, model_label) + swapped = property(_swapped) + def _fields(self): """ The getter for self.fields. This returns the list of field objects diff --git a/django/test/utils.py b/django/test/utils.py index 805117df80..e00de87697 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -27,7 +27,7 @@ class Approximate(object): def __eq__(self, other): if self.val == other: return True - return round(abs(self.val-other), self.places) == 0 + return round(abs(self.val - other), self.places) == 0 class ContextList(list): @@ -45,7 +45,7 @@ class ContextList(list): def __contains__(self, key): try: - value = self[key] + self[key] except KeyError: return False return True @@ -187,9 +187,11 @@ class override_settings(object): if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase): original_pre_setup = test_func._pre_setup original_post_teardown = test_func._post_teardown + def _pre_setup(innerself): self.enable() original_pre_setup(innerself) + def _post_teardown(innerself): original_post_teardown(innerself) self.disable() @@ -218,4 +220,3 @@ class override_settings(object): new_value = getattr(settings, key, None) setting_changed.send(sender=settings._wrapped.__class__, setting=key, value=new_value) - diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 398c90661b..b5bbf35c9b 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -120,6 +120,15 @@ Default: Not defined The site-specific user profile model used by this site. See :ref:`auth-profiles`. +.. setting:: AUTH_USER_MODEL + +AUTH_USER_MODEL +--------------- + +Default: 'auth.User' + +The model to use to represent a User. See :ref:`auth-custom-user`. + .. setting:: CACHES CACHES diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index c3a2636aaf..24d0f4e571 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1723,6 +1723,13 @@ Fields group.permissions.remove(permission, permission, ...) group.permissions.clear() +.. _auth-custom-user: + +Customizing the User model +========================== + +TODO + .. _authentication-backends: Other authentication sources diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py index ed69fb60ee..faf17efa46 100644 --- a/tests/modeltests/invalid_models/invalid_models/models.py +++ b/tests/modeltests/invalid_models/invalid_models/models.py @@ -19,11 +19,12 @@ class FieldErrors(models.Model): decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10) filefield = models.FileField() choices = models.CharField(max_length=10, choices='bad') - choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)]) + choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)]) index = models.CharField(max_length=10, db_index='bad') field_ = models.CharField(max_length=10) nullbool = models.BooleanField(null=True) + class Target(models.Model): tgt_safe = models.CharField(max_length=10) clash1 = models.CharField(max_length=10) @@ -31,12 +32,14 @@ class Target(models.Model): clash1_set = models.CharField(max_length=10) + class Clash1(models.Model): src_safe = models.CharField(max_length=10) foreign = models.ForeignKey(Target) m2m = models.ManyToManyField(Target) + class Clash2(models.Model): src_safe = models.CharField(max_length=10) @@ -46,6 +49,7 @@ class Clash2(models.Model): m2m_1 = models.ManyToManyField(Target, related_name='id') m2m_2 = models.ManyToManyField(Target, related_name='src_safe') + class Target2(models.Model): clash3 = models.CharField(max_length=10) foreign_tgt = models.ForeignKey(Target) @@ -54,6 +58,7 @@ class Target2(models.Model): m2m_tgt = models.ManyToManyField(Target) clashm2m_set = models.ManyToManyField(Target) + class Clash3(models.Model): src_safe = models.CharField(max_length=10) @@ -63,12 +68,15 @@ class Clash3(models.Model): m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt') m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') + class ClashForeign(models.Model): foreign = models.ForeignKey(Target2) + class ClashM2M(models.Model): m2m = models.ManyToManyField(Target2) + class SelfClashForeign(models.Model): src_safe = models.CharField(max_length=10) selfclashforeign = models.CharField(max_length=10) @@ -77,6 +85,7 @@ class SelfClashForeign(models.Model): foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') + class ValidM2M(models.Model): src_safe = models.CharField(max_length=10) validm2m = models.CharField(max_length=10) @@ -92,6 +101,7 @@ class ValidM2M(models.Model): m2m_3 = models.ManyToManyField('self') m2m_4 = models.ManyToManyField('self') + class SelfClashM2M(models.Model): src_safe = models.CharField(max_length=10) selfclashm2m = models.CharField(max_length=10) @@ -106,120 +116,148 @@ class SelfClashM2M(models.Model): m2m_3 = models.ManyToManyField('self', symmetrical=False) m2m_4 = models.ManyToManyField('self', symmetrical=False) + class Model(models.Model): "But it's valid to call a model Model." - year = models.PositiveIntegerField() #1960 - make = models.CharField(max_length=10) #Aston Martin - name = models.CharField(max_length=10) #DB 4 GT + year = models.PositiveIntegerField() # 1960 + make = models.CharField(max_length=10) # Aston Martin + name = models.CharField(max_length=10) # DB 4 GT + class Car(models.Model): colour = models.CharField(max_length=5) model = models.ForeignKey(Model) + class MissingRelations(models.Model): rel1 = models.ForeignKey("Rel1") rel2 = models.ManyToManyField("Rel2") + class MissingManualM2MModel(models.Model): name = models.CharField(max_length=5) missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") + class Person(models.Model): name = models.CharField(max_length=5) + class Group(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership", related_name="primary") secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") + class GroupTwo(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership") secondary = models.ManyToManyField(Group, through="MembershipMissingFK") + class Membership(models.Model): person = models.ForeignKey(Person) group = models.ForeignKey(Group) not_default_or_null = models.CharField(max_length=5) + class MembershipMissingFK(models.Model): person = models.ForeignKey(Person) + class PersonSelfRefM2M(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="Relationship") too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") + class PersonSelfRefM2MExplicit(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) + class Relationship(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") date_added = models.DateTimeField() + class ExplicitRelationship(models.Model): first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") date_added = models.DateTimeField() + class RelationshipTripleFK(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") date_added = models.DateTimeField() + class RelationshipDoubleFK(models.Model): first = models.ForeignKey(Person, related_name="first_related_name") second = models.ForeignKey(Person, related_name="second_related_name") third = models.ForeignKey(Group, related_name="rel_to_set") date_added = models.DateTimeField() + class AbstractModel(models.Model): name = models.CharField(max_length=10) + class Meta: abstract = True + class AbstractRelationModel(models.Model): fk1 = models.ForeignKey('AbstractModel') fk2 = models.ManyToManyField('AbstractModel') + class UniqueM2M(models.Model): """ Model to test for unique ManyToManyFields, which are invalid. """ unique_people = models.ManyToManyField(Person, unique=True) + class NonUniqueFKTarget1(models.Model): """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ tgt = models.ForeignKey('FKTarget', to_field='bad') + class UniqueFKTarget1(models.Model): """ Model to test for unique FK target in yet-to-be-defined model: expect no error """ tgt = models.ForeignKey('FKTarget', to_field='good') + class FKTarget(models.Model): bad = models.IntegerField() good = models.IntegerField(unique=True) + class NonUniqueFKTarget2(models.Model): """ Model to test for non-unique FK target in previously seen model: expect an error """ tgt = models.ForeignKey(FKTarget, to_field='bad') + class UniqueFKTarget2(models.Model): """ Model to test for unique FK target in previously seen model: expect no error """ tgt = models.ForeignKey(FKTarget, to_field='good') + class NonExistingOrderingWithSingleUnderscore(models.Model): class Meta: ordering = ("does_not_exist",) + class InvalidSetNull(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_NULL) + class InvalidSetDefault(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) + class UnicodeForeignKeys(models.Model): """Foreign keys which can translate to ascii should be OK, but fail if they're not.""" @@ -230,9 +268,11 @@ class UnicodeForeignKeys(models.Model): # when adding the errors in core/management/validation.py #bad = models.ForeignKey(u'★') + class PrimaryKeyNull(models.Model): my_pk_field = models.IntegerField(primary_key=True, null=True) + class OrderByPKModel(models.Model): """ Model to test that ordering by pk passes validation. @@ -243,6 +283,42 @@ class OrderByPKModel(models.Model): class Meta: ordering = ('pk',) + +class SwappableModel(models.Model): + """A model that can be, but isn't swapped out. + + References to this model *shoudln't* raise any validation error. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + +class SwappedModel(models.Model): + """A model that is swapped out. + + References to this model *should* raise a validation error. + Requires TEST_SWAPPED_MODEL to be defined in the test environment; + this is guaranteed by the test runner using @override_settings. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL' + + +class HardReferenceModel(models.Model): + fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1') + fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2') + fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3') + fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4') + m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1') + m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2') + m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3') + m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4') + + model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. @@ -351,6 +427,10 @@ invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. +invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. """ if not connection.features.interprets_empty_strings_as_nulls: diff --git a/tests/modeltests/invalid_models/tests.py b/tests/modeltests/invalid_models/tests.py index 9d7adb045c..7cee0b366a 100644 --- a/tests/modeltests/invalid_models/tests.py +++ b/tests/modeltests/invalid_models/tests.py @@ -6,6 +6,7 @@ from django.core.management.validation import get_validation_errors from django.db.models.loading import cache, load_app from django.utils import unittest +from django.test.utils import override_settings class InvalidModelTestCase(unittest.TestCase): @@ -31,14 +32,18 @@ class InvalidModelTestCase(unittest.TestCase): cache._get_models_cache = {} sys.stdout = self.old_stdout + # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be + # set to *something* in order for the test to work. However, it's + # easier to set this up as an override than to require every developer + # to specify a value in their test settings. + @override_settings(TEST_SWAPPED_MODEL='invalid_models.Target') def test_invalid_models(self): - try: module = load_app("modeltests.invalid_models.invalid_models") except Exception: self.fail('Unable to load invalid model module') - count = get_validation_errors(self.stdout, module) + get_validation_errors(self.stdout, module) self.stdout.seek(0) error_log = self.stdout.read() actual = error_log.split('\n')