Added model Meta option for swappable models, and made auth.User a swappable model
This commit is contained in:
parent
f29234167a
commit
7cc0baf89d
|
@ -488,6 +488,8 @@ PROFANITIES_LIST = ()
|
|||
# AUTHENTICATION #
|
||||
##################
|
||||
|
||||
AUTH_USER_MODEL = 'auth.User'
|
||||
|
||||
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
|
||||
|
||||
LOGIN_URL = '/accounts/login/'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.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)
|
||||
|
||||
|
||||
class BaseCommentAbstractModel(models.Model):
|
||||
"""
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -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)."
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.core.management.base import NoArgsCommand
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = "Validates all installed models."
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
@ -96,6 +98,7 @@ def sql_delete(app, style, connection):
|
|||
|
||||
return output[::-1] # Reverse it, to deal with table dependencies.
|
||||
|
||||
|
||||
def sql_flush(style, connection, only_django=False):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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'))
|
||||
|
|
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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':
|
||||
|
|
|
@ -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 = []
|
||||
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
@ -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,6 +866,7 @@ 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):
|
||||
|
@ -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,15 +933,17 @@ 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()
|
||||
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:
|
||||
|
@ -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,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)
|
||||
})
|
||||
|
||||
|
||||
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)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -24,6 +24,7 @@ class FieldErrors(models.Model):
|
|||
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
|
||||
|
||||
|
||||
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:
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue