Added model Meta option for swappable models, and made auth.User a swappable model

This commit is contained in:
Russell Keith-Magee 2012-06-04 14:17:28 +08:00
parent f29234167a
commit 7cc0baf89d
19 changed files with 265 additions and 89 deletions

View File

@ -488,6 +488,8 @@ PROFANITIES_LIST = ()
# AUTHENTICATION # # AUTHENTICATION #
################## ##################
AUTH_USER_MODEL = 'auth.User'
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
LOGIN_URL = '/accounts/login/' LOGIN_URL = '/accounts/login/'

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.contrib.admin.util import quote from django.contrib.admin.util import quote
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode from django.utils.encoding import smart_unicode
@ -10,14 +10,16 @@ ADDITION = 1
CHANGE = 2 CHANGE = 2
DELETION = 3 DELETION = 3
class LogEntryManager(models.Manager): class LogEntryManager(models.Manager):
def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): 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 = self.model(None, None, user_id, content_type_id, smart_unicode(object_id), object_repr[:200], action_flag, change_message)
e.save() e.save()
class LogEntry(models.Model): class LogEntry(models.Model):
action_time = models.DateTimeField(_('action time'), auto_now=True) 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) content_type = models.ForeignKey(ContentType, blank=True, null=True)
object_id = models.TextField(_('object id'), blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True)
object_repr = models.CharField(_('object repr'), max_length=200) object_repr = models.CharField(_('object repr'), max_length=200)

View File

@ -93,6 +93,7 @@ class GroupManager(models.Manager):
def get_by_natural_key(self, name): def get_by_natural_key(self, name):
return self.get(name=name) return self.get(name=name)
class Group(models.Model): class Group(models.Model):
""" """
Groups are a generic way of categorizing users to apply permissions, or 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): def _user_has_perm(user, perm, obj):
anon = user.is_anonymous()
active = user.is_active
for backend in auth.get_backends(): for backend in auth.get_backends():
if hasattr(backend, "has_perm"): if hasattr(backend, "has_perm"):
if obj is not None: if obj is not None:
@ -211,8 +210,6 @@ def _user_has_perm(user, perm, obj):
def _user_has_module_perms(user, app_label): def _user_has_module_perms(user, app_label):
anon = user.is_anonymous()
active = user.is_active
for backend in auth.get_backends(): for backend in auth.get_backends():
if hasattr(backend, "has_module_perms"): if hasattr(backend, "has_module_perms"):
if backend.has_module_perms(user, app_label): if backend.has_module_perms(user, app_label):
@ -220,7 +217,54 @@ def _user_has_module_perms(user, app_label):
return False 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 Users within the Django authentication system are represented by this
model. model.
@ -233,7 +277,6 @@ class User(models.Model):
first_name = models.CharField(_('first name'), max_length=30, blank=True) first_name = models.CharField(_('first name'), max_length=30, blank=True)
last_name = models.CharField(_('last 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) email = models.EmailField(_('e-mail address'), blank=True)
password = models.CharField(_('password'), max_length=128)
is_staff = models.BooleanField(_('staff status'), default=False, is_staff = models.BooleanField(_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin ' help_text=_('Designates whether the user can log into this admin '
'site.')) 'site.'))
@ -257,6 +300,7 @@ class User(models.Model):
class Meta: class Meta:
verbose_name = _('user') verbose_name = _('user')
verbose_name_plural = _('users') verbose_name_plural = _('users')
swappable = 'AUTH_USER_MODEL'
def __unicode__(self): def __unicode__(self):
return self.username return self.username
@ -267,20 +311,6 @@ class User(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return "/users/%s/" % urllib.quote(smart_str(self.username)) 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): def get_full_name(self):
""" """
Returns the first_name plus the last_name, with a space in between. 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) full_name = u'%s %s' % (self.first_name, self.last_name)
return full_name.strip() 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): def get_group_permissions(self, obj=None):
""" """
Returns a list of permission strings that this user has through his/her Returns a list of permission strings that this user has through his/her

View File

@ -1,16 +1,16 @@
from django.contrib.auth.models import User from django.conf import settings
from django.contrib.comments.managers import CommentManager from django.contrib.comments.managers import CommentManager
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models
from django.core import urlresolvers from django.core import urlresolvers
from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone 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): class BaseCommentAbstractModel(models.Model):
""" """
An abstract base class that any custom comment models probably should An abstract base class that any custom comment models probably should
@ -39,6 +39,7 @@ class BaseCommentAbstractModel(models.Model):
args=(self.content_type_id, self.object_pk) args=(self.content_type_id, self.object_pk)
) )
class Comment(BaseCommentAbstractModel): class Comment(BaseCommentAbstractModel):
""" """
A user comment about some object. 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 # 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 # user; otherwise at least user_name should have been set and the comment
# was posted by a non-authenticated user. # 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") blank=True, null=True, related_name="%(class)s_comments")
user_name = models.CharField(_("user's name"), max_length=50, blank=True) user_name = models.CharField(_("user's name"), max_length=50, blank=True)
user_email = models.EmailField(_("user's email address"), blank=True) user_email = models.EmailField(_("user's email address"), blank=True)
@ -115,6 +116,7 @@ class Comment(BaseCommentAbstractModel):
def _get_name(self): def _get_name(self):
return self.userinfo["name"] return self.userinfo["name"]
def _set_name(self, val): def _set_name(self, val):
if self.user_id: if self.user_id:
raise AttributeError(_("This comment was posted by an authenticated "\ raise AttributeError(_("This comment was posted by an authenticated "\
@ -124,6 +126,7 @@ class Comment(BaseCommentAbstractModel):
def _get_email(self): def _get_email(self):
return self.userinfo["email"] return self.userinfo["email"]
def _set_email(self, val): def _set_email(self, val):
if self.user_id: if self.user_id:
raise AttributeError(_("This comment was posted by an authenticated "\ raise AttributeError(_("This comment was posted by an authenticated "\
@ -133,6 +136,7 @@ class Comment(BaseCommentAbstractModel):
def _get_url(self): def _get_url(self):
return self.userinfo["url"] return self.userinfo["url"]
def _set_url(self, val): def _set_url(self, val):
self.user_url = val self.user_url = val
url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") 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 return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d
class CommentFlag(models.Model): class CommentFlag(models.Model):
""" """
Records a flag on a comment. This is intentionally flexible; right now, a 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; design users are only allowed to flag a comment with a given flag once;
if you want rating look elsewhere. 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") comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags")
flag = models.CharField(_('flag'), max_length=30, db_index=True) flag = models.CharField(_('flag'), max_length=30, db_index=True)
flag_date = models.DateTimeField(_('date'), default=None) flag_date = models.DateTimeField(_('date'), default=None)

View File

@ -4,6 +4,7 @@ from django.core.management.base import AppCommand
from django.core.management.sql import sql_all from django.core.management.sql import sql_all
from django.db import connections, DEFAULT_DB_ALIAS from django.db import connections, DEFAULT_DB_ALIAS
class Command(AppCommand): class Command(AppCommand):
help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)." help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)."

View File

@ -68,6 +68,7 @@ class Command(NoArgsCommand):
if router.allow_syncdb(db, m)]) if router.allow_syncdb(db, m)])
for app in models.get_apps() for app in models.get_apps()
] ]
def model_installed(model): def model_installed(model):
opts = model._meta opts = model._meta
converter = connection.introspection.table_name_converter converter = connection.introspection.table_name_converter
@ -101,7 +102,6 @@ class Command(NoArgsCommand):
cursor.execute(statement) cursor.execute(statement)
tables.append(connection.introspection.table_name_converter(model._meta.db_table)) tables.append(connection.introspection.table_name_converter(model._meta.db_table))
transaction.commit_unless_managed(using=db) transaction.commit_unless_managed(using=db)
# Send the post_syncdb signal, so individual apps can do whatever they need # Send the post_syncdb signal, so individual apps can do whatever they need

View File

@ -1,5 +1,6 @@
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
class Command(NoArgsCommand): class Command(NoArgsCommand):
help = "Validates all installed models." help = "Validates all installed models."

View File

@ -6,6 +6,7 @@ from django.core.management.base import CommandError
from django.db import models from django.db import models
from django.db.models import get_models from django.db.models import get_models
def sql_create(app, style, connection): def sql_create(app, style, connection):
"Returns a list of the CREATE TABLE SQL statements for the given app." "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 return final_output
def sql_delete(app, style, connection): def sql_delete(app, style, connection):
"Returns a list of the DROP TABLE SQL statements for the given app." "Returns a list of the DROP TABLE SQL statements for the given app."
@ -96,6 +98,7 @@ def sql_delete(app, style, connection):
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): def sql_flush(style, connection, only_django=False):
""" """
Returns a list of the SQL statements used to flush the database. Returns a list of the SQL statements used to flush the database.
@ -112,6 +115,7 @@ def sql_flush(style, connection, only_django=False):
) )
return statements return statements
def sql_custom(app, style, connection): def sql_custom(app, style, connection):
"Returns a list of the custom table modifying SQL statements for the given app." "Returns a list of the custom table modifying SQL statements for the given app."
output = [] output = []
@ -123,6 +127,7 @@ def sql_custom(app, style, connection):
return output return output
def sql_indexes(app, style, connection): def sql_indexes(app, style, connection):
"Returns a list of the CREATE INDEX SQL statements for all models in the given app." "Returns a list of the CREATE INDEX SQL statements for all models in the given app."
output = [] output = []
@ -130,10 +135,12 @@ def sql_indexes(app, style, connection):
output.extend(connection.creation.sql_indexes_for_model(model, style)) output.extend(connection.creation.sql_indexes_for_model(model, style))
return output return output
def sql_all(app, style, connection): def sql_all(app, style, connection):
"Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module." "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) return sql_create(app, style, connection) + sql_custom(app, style, connection) + sql_indexes(app, style, connection)
def custom_sql_for_model(model, style, connection): def custom_sql_for_model(model, style, connection):
opts = model._meta opts = model._meta
app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))

View File

@ -3,6 +3,7 @@ import sys
from django.core.management.color import color_style from django.core.management.color import color_style
from django.utils.itercompat import is_iterable from django.utils.itercompat import is_iterable
class ModelErrorCollection: class ModelErrorCollection:
def __init__(self, outfile=sys.stdout): def __init__(self, outfile=sys.stdout):
self.errors = [] self.errors = []
@ -13,6 +14,7 @@ class ModelErrorCollection:
self.errors.append((context, error)) self.errors.append((context, error))
self.outfile.write(self.style.ERROR("%s: %s\n" % (context, error))) self.outfile.write(self.style.ERROR("%s: %s\n" % (context, error)))
def get_validation_errors(outfile, app=None): def get_validation_errors(outfile, app=None):
""" """
Validates all models that are part of the specified app. If no app name is provided, Validates all models that are part of the specified app. If no app name is provided,
@ -121,6 +123,10 @@ def get_validation_errors(outfile, app=None):
if isinstance(f.rel.to, (str, unicode)): if isinstance(f.rel.to, (str, unicode)):
continue 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 # Make sure the related field specified by a ForeignKey is unique
if not f.rel.to._meta.get_field(f.rel.field_name).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__)) 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)): if isinstance(f.rel.to, (str, unicode)):
continue 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. # Check that the field is not set to unique. ManyToManyFields do not support unique.
if f.unique: if f.unique:
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
@ -276,7 +286,8 @@ def get_validation_errors(outfile, app=None):
# Check ordering attribute. # Check ordering attribute.
if opts.ordering: if opts.ordering:
for field_name in opts.ordering: for field_name in opts.ordering:
if field_name == '?': continue if field_name == '?':
continue
if field_name.startswith('-'): if field_name.startswith('-'):
field_name = field_name[1:] field_name = field_name[1:]
if opts.order_with_respect_to and field_name == '_order': if opts.order_with_respect_to and field_name == '_order':

View File

@ -34,7 +34,7 @@ class BaseDatabaseCreation(object):
(list_of_sql, pending_references_dict) (list_of_sql, pending_references_dict)
""" """
opts = model._meta opts = model._meta
if not opts.managed or opts.proxy: if not opts.managed or opts.proxy or opts.swapped:
return [], {} return [], {}
final_output = [] final_output = []
table_output = [] table_output = []
@ -137,11 +137,11 @@ class BaseDatabaseCreation(object):
""" """
from django.db.backends.util import truncate_name 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 [] return []
qn = self.connection.ops.quote_name qn = self.connection.ops.quote_name
final_output = [] final_output = []
opts = model._meta
if model in pending_references: if model in pending_references:
for rel_class, f in pending_references[model]: for rel_class, f in pending_references[model]:
rel_opts = rel_class._meta rel_opts = rel_class._meta
@ -166,7 +166,7 @@ class BaseDatabaseCreation(object):
""" """
Returns the CREATE INDEX SQL statements for a single model. 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 [] return []
output = [] output = []
for f in model._meta.local_fields: 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 Return the DROP TABLE and restraint dropping statements for a single
model. model.
""" """
if not model._meta.managed or model._meta.proxy: if not model._meta.managed or model._meta.proxy or model._meta.swapped:
return [] return []
# Drop the table now # Drop the table now
qn = self.connection.ops.quote_name qn = self.connection.ops.quote_name
@ -222,7 +222,7 @@ class BaseDatabaseCreation(object):
def sql_remove_table_constraints(self, model, references_to_delete, style): def sql_remove_table_constraints(self, model, references_to_delete, style):
from django.db.backends.util import truncate_name 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 [] return []
output = [] output = []
qn = self.connection.ops.quote_name qn = self.connection.ops.quote_name

View File

@ -230,6 +230,7 @@ class ModelBase(type):
if opts.order_with_respect_to: if opts.order_with_respect_to:
cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) 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) 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 # defer creating accessors on the foreign class until we are
# certain it has been created # certain it has been created
def make_foreign_order_accessors(field, model, cls): def make_foreign_order_accessors(field, model, cls):
@ -260,6 +261,7 @@ class ModelBase(type):
signals.class_prepared.send(sender=cls) signals.class_prepared.send(sender=cls)
class ModelState(object): class ModelState(object):
""" """
A class for storing instance state 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. # This impacts validation only; it has no effect on the actual save.
self.adding = True self.adding = True
class Model(object): class Model(object):
__metaclass__ = ModelBase __metaclass__ = ModelBase
_deferred = False _deferred = False
@ -585,7 +588,6 @@ class Model(object):
signals.post_save.send(sender=origin, instance=self, created=(not record_exists), signals.post_save.send(sender=origin, instance=self, created=(not record_exists),
update_fields=update_fields, raw=raw, using=using) update_fields=update_fields, raw=raw, using=using)
save_base.alters_data = True save_base.alters_data = True
def delete(self, using=None): def delete(self, using=None):
@ -915,6 +917,7 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
class Empty(object): class Empty(object):
pass pass
def simple_class_factory(model, attrs): def simple_class_factory(model, attrs):
"""Used to unpickle Models without deferred fields. """Used to unpickle Models without deferred fields.
@ -924,6 +927,7 @@ def simple_class_factory(model, attrs):
""" """
return model return model
def model_unpickle(model, attrs, factory): def model_unpickle(model, attrs, factory):
""" """
Used to unpickle Model subclasses with deferred fields. Used to unpickle Model subclasses with deferred fields.
@ -932,5 +936,6 @@ def model_unpickle(model, attrs, factory):
return cls.__new__(cls) return cls.__new__(cls)
model_unpickle.__safe_for_unpickle__ = True model_unpickle.__safe_for_unpickle__ = True
def subclass_exception(name, parents, module): def subclass_exception(name, parents, module):
return type(name, parents, {'__module__': module}) return type(name, parents, {'__module__': module})

View File

@ -20,6 +20,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
pending_lookups = {} pending_lookups = {}
def add_lazy_relation(cls, field, relation, operation): def add_lazy_relation(cls, field, relation, operation):
""" """
Adds a lookup on ``cls`` when a related field is defined using a string, 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) value = (cls, field, operation)
pending_lookups.setdefault(key, []).append(value) pending_lookups.setdefault(key, []).append(value)
def do_pending_lookups(sender, **kwargs): def do_pending_lookups(sender, **kwargs):
""" """
Handle any pending relations to the sending model. Sent from class_prepared. 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) signals.class_prepared.connect(do_pending_lookups)
#HACK #HACK
class RelatedField(object): class RelatedField(object):
def contribute_to_class(self, cls, name): def contribute_to_class(self, cls, name):
@ -219,6 +222,7 @@ class RelatedField(object):
# "related_name" option. # "related_name" option.
return self.rel.related_name or self.opts.object_name.lower() return self.rel.related_name or self.opts.object_name.lower()
class SingleRelatedObjectDescriptor(object): class SingleRelatedObjectDescriptor(object):
# This class provides the functionality that makes the related-object # This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have # 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(instance, self.cache_name, value)
setattr(value, self.related.field.get_cache_name(), instance) setattr(value, self.related.field.get_cache_name(), instance)
class ReverseSingleRelatedObjectDescriptor(object): class ReverseSingleRelatedObjectDescriptor(object):
# This class provides the functionality that makes the related-object # This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have # 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: if value is not None and not self.field.rel.multiple:
setattr(value, self.field.related.get_cache_name(), instance) setattr(value, self.field.related.get_cache_name(), instance)
class ForeignRelatedObjectsDescriptor(object): class ForeignRelatedObjectsDescriptor(object):
# This class provides the functionality that makes the related-object # This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have # managers available as attributes on a model class, for fields that have
@ -751,6 +757,7 @@ def create_many_related_manager(superclass, rel):
return ManyRelatedManager return ManyRelatedManager
class ManyRelatedObjectsDescriptor(object): class ManyRelatedObjectsDescriptor(object):
# This class provides the functionality that makes the related-object # This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have # managers available as attributes on a model class, for fields that have
@ -859,6 +866,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
manager.clear() manager.clear()
manager.add(*value) manager.add(*value)
class ManyToOneRel(object): class ManyToOneRel(object):
def __init__(self, to, field_name, related_name=None, limit_choices_to=None, def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
parent_link=False, on_delete=None): parent_link=False, on_delete=None):
@ -890,6 +898,7 @@ class ManyToOneRel(object):
self.field_name) self.field_name)
return data[0] return data[0]
class OneToOneRel(ManyToOneRel): class OneToOneRel(ManyToOneRel):
def __init__(self, to, field_name, related_name=None, limit_choices_to=None, def __init__(self, to, field_name, related_name=None, limit_choices_to=None,
parent_link=False, on_delete=None): parent_link=False, on_delete=None):
@ -899,6 +908,7 @@ class OneToOneRel(ManyToOneRel):
) )
self.multiple = False self.multiple = False
class ManyToManyRel(object): class ManyToManyRel(object):
def __init__(self, to, related_name=None, limit_choices_to=None, def __init__(self, to, related_name=None, limit_choices_to=None,
symmetrical=True, through=None): symmetrical=True, through=None):
@ -923,15 +933,17 @@ class ManyToManyRel(object):
""" """
return self.to._meta.pk return self.to._meta.pk
class ForeignKey(RelatedField, Field): class ForeignKey(RelatedField, Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {
'invalid': _('Model %(model)s with pk %(pk)r does not exist.') 'invalid': _('Model %(model)s with pk %(pk)r does not exist.')
} }
description = _("Foreign Key (type determined by related field)") description = _("Foreign Key (type determined by related field)")
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
try: try:
to_name = to._meta.object_name.lower() to._meta.object_name.lower()
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 ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, 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: else:
@ -1049,6 +1061,7 @@ class ForeignKey(RelatedField, Field):
return IntegerField().db_type(connection=connection) return IntegerField().db_type(connection=connection)
return rel_field.db_type(connection=connection) return rel_field.db_type(connection=connection)
class OneToOneField(ForeignKey): class OneToOneField(ForeignKey):
""" """
A OneToOneField is essentially the same as a ForeignKey, with the exception A OneToOneField is essentially the same as a ForeignKey, with the exception
@ -1057,6 +1070,7 @@ class OneToOneField(ForeignKey):
rather than returning a list. rather than returning a list.
""" """
description = _("One-to-one relationship") description = _("One-to-one relationship")
def __init__(self, to, to_field=None, **kwargs): def __init__(self, to, to_field=None, **kwargs):
kwargs['unique'] = True kwargs['unique'] = True
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@ -1076,12 +1090,14 @@ class OneToOneField(ForeignKey):
else: else:
setattr(instance, self.attname, data) setattr(instance, self.attname, data)
def create_many_to_many_intermediary_model(field, klass): def create_many_to_many_intermediary_model(field, klass):
from django.db import models from django.db import models
managed = True managed = True
if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
to_model = field.rel.to to_model = field.rel.to
to = to_model.split('.')[-1] to = to_model.split('.')[-1]
def set_managed(field, model, cls): def set_managed(field, model, cls):
field.rel.through._meta.managed = model._meta.managed or cls._meta.managed field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
add_lazy_relation(klass, field, to_model, set_managed) add_lazy_relation(klass, field, to_model, set_managed)
@ -1118,8 +1134,10 @@ def create_many_to_many_intermediary_model(field, klass):
to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace)
}) })
class ManyToManyField(RelatedField, Field): class ManyToManyField(RelatedField, Field):
description = _("Many-to-many relationship") description = _("Many-to-many relationship")
def __init__(self, to, **kwargs): def __init__(self, to, **kwargs):
try: try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
@ -1221,7 +1239,8 @@ class ManyToManyField(RelatedField, Field):
# The intermediate m2m model is not auto created if: # The intermediate m2m model is not auto created if:
# 1) There is a manually specified intermediate, or # 1) There is a manually specified intermediate, or
# 2) The class owning the m2m field is abstract. # 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) self.rel.through = create_many_to_many_intermediary_model(self, cls)
# Add the descriptor for the m2m relation # Add the descriptor for the m2m relation

View File

@ -13,6 +13,7 @@ import threading
__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
'load_app', 'app_cache_ready') 'load_app', 'app_cache_ready')
class AppCache(object): class AppCache(object):
""" """
A cache that stores installed applications and their models. Used to A cache that stores installed applications and their models. Used to

View File

@ -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', DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by', 'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace', 'order_with_respect_to', 'app_label', 'db_tablespace',
'abstract', 'managed', 'proxy', 'auto_created') 'abstract', 'managed', 'proxy', 'swappable', 'auto_created')
class Options(object): class Options(object):
def __init__(self, meta, app_label=None): def __init__(self, meta, app_label=None):
@ -50,6 +51,7 @@ class Options(object):
# in the end of the proxy_for_model chain. In particular, for # in the end of the proxy_for_model chain. In particular, for
# concrete models, the concrete_model is always the class itself. # concrete models, the concrete_model is always the class itself.
self.concrete_model = None self.concrete_model = None
self.swappable = None
self.parents = SortedDict() self.parents = SortedDict()
self.duplicate_targets = {} self.duplicate_targets = {}
self.auto_created = False self.auto_created = False
@ -213,6 +215,14 @@ class Options(object):
return raw return raw
verbose_name_raw = property(verbose_name_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): def _fields(self):
""" """
The getter for self.fields. This returns the list of field objects The getter for self.fields. This returns the list of field objects

View File

@ -45,7 +45,7 @@ class ContextList(list):
def __contains__(self, key): def __contains__(self, key):
try: try:
value = self[key] self[key]
except KeyError: except KeyError:
return False return False
return True return True
@ -187,9 +187,11 @@ class override_settings(object):
if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase): if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase):
original_pre_setup = test_func._pre_setup original_pre_setup = test_func._pre_setup
original_post_teardown = test_func._post_teardown original_post_teardown = test_func._post_teardown
def _pre_setup(innerself): def _pre_setup(innerself):
self.enable() self.enable()
original_pre_setup(innerself) original_pre_setup(innerself)
def _post_teardown(innerself): def _post_teardown(innerself):
original_post_teardown(innerself) original_post_teardown(innerself)
self.disable() self.disable()
@ -218,4 +220,3 @@ class override_settings(object):
new_value = getattr(settings, key, None) new_value = getattr(settings, key, None)
setting_changed.send(sender=settings._wrapped.__class__, setting_changed.send(sender=settings._wrapped.__class__,
setting=key, value=new_value) setting=key, value=new_value)

View File

@ -120,6 +120,15 @@ Default: Not defined
The site-specific user profile model used by this site. See The site-specific user profile model used by this site. See
:ref:`auth-profiles`. :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 .. setting:: CACHES
CACHES CACHES

View File

@ -1723,6 +1723,13 @@ Fields
group.permissions.remove(permission, permission, ...) group.permissions.remove(permission, permission, ...)
group.permissions.clear() group.permissions.clear()
.. _auth-custom-user:
Customizing the User model
==========================
TODO
.. _authentication-backends: .. _authentication-backends:
Other authentication sources Other authentication sources

View File

@ -24,6 +24,7 @@ class FieldErrors(models.Model):
field_ = models.CharField(max_length=10) field_ = models.CharField(max_length=10)
nullbool = models.BooleanField(null=True) nullbool = models.BooleanField(null=True)
class Target(models.Model): class Target(models.Model):
tgt_safe = models.CharField(max_length=10) tgt_safe = models.CharField(max_length=10)
clash1 = 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) clash1_set = models.CharField(max_length=10)
class Clash1(models.Model): class Clash1(models.Model):
src_safe = models.CharField(max_length=10) src_safe = models.CharField(max_length=10)
foreign = models.ForeignKey(Target) foreign = models.ForeignKey(Target)
m2m = models.ManyToManyField(Target) m2m = models.ManyToManyField(Target)
class Clash2(models.Model): class Clash2(models.Model):
src_safe = models.CharField(max_length=10) src_safe = models.CharField(max_length=10)
@ -46,6 +49,7 @@ class Clash2(models.Model):
m2m_1 = models.ManyToManyField(Target, related_name='id') m2m_1 = models.ManyToManyField(Target, related_name='id')
m2m_2 = models.ManyToManyField(Target, related_name='src_safe') m2m_2 = models.ManyToManyField(Target, related_name='src_safe')
class Target2(models.Model): class Target2(models.Model):
clash3 = models.CharField(max_length=10) clash3 = models.CharField(max_length=10)
foreign_tgt = models.ForeignKey(Target) foreign_tgt = models.ForeignKey(Target)
@ -54,6 +58,7 @@ class Target2(models.Model):
m2m_tgt = models.ManyToManyField(Target) m2m_tgt = models.ManyToManyField(Target)
clashm2m_set = models.ManyToManyField(Target) clashm2m_set = models.ManyToManyField(Target)
class Clash3(models.Model): class Clash3(models.Model):
src_safe = models.CharField(max_length=10) 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_1 = models.ManyToManyField(Target2, related_name='foreign_tgt')
m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt')
class ClashForeign(models.Model): class ClashForeign(models.Model):
foreign = models.ForeignKey(Target2) foreign = models.ForeignKey(Target2)
class ClashM2M(models.Model): class ClashM2M(models.Model):
m2m = models.ManyToManyField(Target2) m2m = models.ManyToManyField(Target2)
class SelfClashForeign(models.Model): class SelfClashForeign(models.Model):
src_safe = models.CharField(max_length=10) src_safe = models.CharField(max_length=10)
selfclashforeign = 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_1 = models.ForeignKey("SelfClashForeign", related_name='id')
foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe')
class ValidM2M(models.Model): class ValidM2M(models.Model):
src_safe = models.CharField(max_length=10) src_safe = models.CharField(max_length=10)
validm2m = 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_3 = models.ManyToManyField('self')
m2m_4 = models.ManyToManyField('self') m2m_4 = models.ManyToManyField('self')
class SelfClashM2M(models.Model): class SelfClashM2M(models.Model):
src_safe = models.CharField(max_length=10) src_safe = models.CharField(max_length=10)
selfclashm2m = 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_3 = models.ManyToManyField('self', symmetrical=False)
m2m_4 = models.ManyToManyField('self', symmetrical=False) m2m_4 = models.ManyToManyField('self', symmetrical=False)
class Model(models.Model): class Model(models.Model):
"But it's valid to call a model Model." "But it's valid to call a model Model."
year = models.PositiveIntegerField() # 1960 year = models.PositiveIntegerField() # 1960
make = models.CharField(max_length=10) # Aston Martin make = models.CharField(max_length=10) # Aston Martin
name = models.CharField(max_length=10) # DB 4 GT name = models.CharField(max_length=10) # DB 4 GT
class Car(models.Model): class Car(models.Model):
colour = models.CharField(max_length=5) colour = models.CharField(max_length=5)
model = models.ForeignKey(Model) model = models.ForeignKey(Model)
class MissingRelations(models.Model): class MissingRelations(models.Model):
rel1 = models.ForeignKey("Rel1") rel1 = models.ForeignKey("Rel1")
rel2 = models.ManyToManyField("Rel2") rel2 = models.ManyToManyField("Rel2")
class MissingManualM2MModel(models.Model): class MissingManualM2MModel(models.Model):
name = models.CharField(max_length=5) name = models.CharField(max_length=5)
missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
class Person(models.Model): class Person(models.Model):
name = models.CharField(max_length=5) name = models.CharField(max_length=5)
class Group(models.Model): class Group(models.Model):
name = models.CharField(max_length=5) name = models.CharField(max_length=5)
primary = models.ManyToManyField(Person, through="Membership", related_name="primary") primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary")
class GroupTwo(models.Model): class GroupTwo(models.Model):
name = models.CharField(max_length=5) name = models.CharField(max_length=5)
primary = models.ManyToManyField(Person, through="Membership") primary = models.ManyToManyField(Person, through="Membership")
secondary = models.ManyToManyField(Group, through="MembershipMissingFK") secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
class Membership(models.Model): class Membership(models.Model):
person = models.ForeignKey(Person) person = models.ForeignKey(Person)
group = models.ForeignKey(Group) group = models.ForeignKey(Group)
not_default_or_null = models.CharField(max_length=5) not_default_or_null = models.CharField(max_length=5)
class MembershipMissingFK(models.Model): class MembershipMissingFK(models.Model):
person = models.ForeignKey(Person) person = models.ForeignKey(Person)
class PersonSelfRefM2M(models.Model): class PersonSelfRefM2M(models.Model):
name = models.CharField(max_length=5) name = models.CharField(max_length=5)
friends = models.ManyToManyField('self', through="Relationship") friends = models.ManyToManyField('self', through="Relationship")
too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK")
class PersonSelfRefM2MExplicit(models.Model): class PersonSelfRefM2MExplicit(models.Model):
name = models.CharField(max_length=5) name = models.CharField(max_length=5)
friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True)
class Relationship(models.Model): class Relationship(models.Model):
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set")
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set")
date_added = models.DateTimeField() date_added = models.DateTimeField()
class ExplicitRelationship(models.Model): class ExplicitRelationship(models.Model):
first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set")
second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set")
date_added = models.DateTimeField() date_added = models.DateTimeField()
class RelationshipTripleFK(models.Model): class RelationshipTripleFK(models.Model):
first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2")
second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2")
third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far")
date_added = models.DateTimeField() date_added = models.DateTimeField()
class RelationshipDoubleFK(models.Model): class RelationshipDoubleFK(models.Model):
first = models.ForeignKey(Person, related_name="first_related_name") first = models.ForeignKey(Person, related_name="first_related_name")
second = models.ForeignKey(Person, related_name="second_related_name") second = models.ForeignKey(Person, related_name="second_related_name")
third = models.ForeignKey(Group, related_name="rel_to_set") third = models.ForeignKey(Group, related_name="rel_to_set")
date_added = models.DateTimeField() date_added = models.DateTimeField()
class AbstractModel(models.Model): class AbstractModel(models.Model):
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
class Meta: class Meta:
abstract = True abstract = True
class AbstractRelationModel(models.Model): class AbstractRelationModel(models.Model):
fk1 = models.ForeignKey('AbstractModel') fk1 = models.ForeignKey('AbstractModel')
fk2 = models.ManyToManyField('AbstractModel') fk2 = models.ManyToManyField('AbstractModel')
class UniqueM2M(models.Model): class UniqueM2M(models.Model):
""" Model to test for unique ManyToManyFields, which are invalid. """ """ Model to test for unique ManyToManyFields, which are invalid. """
unique_people = models.ManyToManyField(Person, unique=True) unique_people = models.ManyToManyField(Person, unique=True)
class NonUniqueFKTarget1(models.Model): class NonUniqueFKTarget1(models.Model):
""" Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """
tgt = models.ForeignKey('FKTarget', to_field='bad') tgt = models.ForeignKey('FKTarget', to_field='bad')
class UniqueFKTarget1(models.Model): class UniqueFKTarget1(models.Model):
""" Model to test for unique FK target in yet-to-be-defined model: expect no error """ """ Model to test for unique FK target in yet-to-be-defined model: expect no error """
tgt = models.ForeignKey('FKTarget', to_field='good') tgt = models.ForeignKey('FKTarget', to_field='good')
class FKTarget(models.Model): class FKTarget(models.Model):
bad = models.IntegerField() bad = models.IntegerField()
good = models.IntegerField(unique=True) good = models.IntegerField(unique=True)
class NonUniqueFKTarget2(models.Model): class NonUniqueFKTarget2(models.Model):
""" Model to test for non-unique FK target in previously seen model: expect an error """ """ Model to test for non-unique FK target in previously seen model: expect an error """
tgt = models.ForeignKey(FKTarget, to_field='bad') tgt = models.ForeignKey(FKTarget, to_field='bad')
class UniqueFKTarget2(models.Model): class UniqueFKTarget2(models.Model):
""" Model to test for unique FK target in previously seen model: expect no error """ """ Model to test for unique FK target in previously seen model: expect no error """
tgt = models.ForeignKey(FKTarget, to_field='good') tgt = models.ForeignKey(FKTarget, to_field='good')
class NonExistingOrderingWithSingleUnderscore(models.Model): class NonExistingOrderingWithSingleUnderscore(models.Model):
class Meta: class Meta:
ordering = ("does_not_exist",) ordering = ("does_not_exist",)
class InvalidSetNull(models.Model): class InvalidSetNull(models.Model):
fk = models.ForeignKey('self', on_delete=models.SET_NULL) fk = models.ForeignKey('self', on_delete=models.SET_NULL)
class InvalidSetDefault(models.Model): class InvalidSetDefault(models.Model):
fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT)
class UnicodeForeignKeys(models.Model): class UnicodeForeignKeys(models.Model):
"""Foreign keys which can translate to ascii should be OK, but fail if """Foreign keys which can translate to ascii should be OK, but fail if
they're not.""" they're not."""
@ -230,9 +268,11 @@ class UnicodeForeignKeys(models.Model):
# when adding the errors in core/management/validation.py # when adding the errors in core/management/validation.py
#bad = models.ForeignKey(u'★') #bad = models.ForeignKey(u'★')
class PrimaryKeyNull(models.Model): class PrimaryKeyNull(models.Model):
my_pk_field = models.IntegerField(primary_key=True, null=True) my_pk_field = models.IntegerField(primary_key=True, null=True)
class OrderByPKModel(models.Model): class OrderByPKModel(models.Model):
""" """
Model to test that ordering by pk passes validation. Model to test that ordering by pk passes validation.
@ -243,6 +283,42 @@ class OrderByPKModel(models.Model):
class Meta: class Meta:
ordering = ('pk',) 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. 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: "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. 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.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.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.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: if not connection.features.interprets_empty_strings_as_nulls:

View File

@ -6,6 +6,7 @@ from django.core.management.validation import get_validation_errors
from django.db.models.loading import cache, load_app from django.db.models.loading import cache, load_app
from django.utils import unittest from django.utils import unittest
from django.test.utils import override_settings
class InvalidModelTestCase(unittest.TestCase): class InvalidModelTestCase(unittest.TestCase):
@ -31,14 +32,18 @@ class InvalidModelTestCase(unittest.TestCase):
cache._get_models_cache = {} cache._get_models_cache = {}
sys.stdout = self.old_stdout 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): def test_invalid_models(self):
try: try:
module = load_app("modeltests.invalid_models.invalid_models") module = load_app("modeltests.invalid_models.invalid_models")
except Exception: except Exception:
self.fail('Unable to load invalid model module') 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) self.stdout.seek(0)
error_log = self.stdout.read() error_log = self.stdout.read()
actual = error_log.split('\n') actual = error_log.split('\n')