mirror of
https://github.com/django/django.git
synced 2025-07-06 10:49:17 +00:00
queryset-refactor: Model inheritance support.
This adds both types of model inheritance: abstract base classes (ABCs) and multi-table inheritance. See the documentation and tests / examples for details. Still a few known bugs here, so don't file tickets (I know about them). Not quite ready for prime-time usage, but it mostly works as expected. git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7126 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
2d0588548e
commit
da6570bf08
@ -26,7 +26,7 @@ def django_table_list(only_existing=False):
|
|||||||
for app in models.get_apps():
|
for app in models.get_apps():
|
||||||
for model in models.get_models(app):
|
for model in models.get_models(app):
|
||||||
tables.append(model._meta.db_table)
|
tables.append(model._meta.db_table)
|
||||||
tables.extend([f.m2m_db_table() for f in model._meta.many_to_many])
|
tables.extend([f.m2m_db_table() for f in model._meta.local_many_to_many])
|
||||||
if only_existing:
|
if only_existing:
|
||||||
existing = table_list()
|
existing = table_list()
|
||||||
tables = [t for t in tables if t in existing]
|
tables = [t for t in tables if t in existing]
|
||||||
@ -54,12 +54,12 @@ def sequence_list():
|
|||||||
|
|
||||||
for app in apps:
|
for app in apps:
|
||||||
for model in models.get_models(app):
|
for model in models.get_models(app):
|
||||||
for f in model._meta.fields:
|
for f in model._meta.local_fields:
|
||||||
if isinstance(f, models.AutoField):
|
if isinstance(f, models.AutoField):
|
||||||
sequence_list.append({'table': model._meta.db_table, 'column': f.column})
|
sequence_list.append({'table': model._meta.db_table, 'column': f.column})
|
||||||
break # Only one AutoField is allowed per model, so don't bother continuing.
|
break # Only one AutoField is allowed per model, so don't bother continuing.
|
||||||
|
|
||||||
for f in model._meta.many_to_many:
|
for f in model._meta.local_many_to_many:
|
||||||
sequence_list.append({'table': f.m2m_db_table(), 'column': None})
|
sequence_list.append({'table': f.m2m_db_table(), 'column': None})
|
||||||
|
|
||||||
return sequence_list
|
return sequence_list
|
||||||
@ -147,7 +147,7 @@ def sql_delete(app, style):
|
|||||||
if cursor and table_name_converter(model._meta.db_table) in table_names:
|
if cursor and table_name_converter(model._meta.db_table) in table_names:
|
||||||
# The table exists, so it needs to be dropped
|
# The table exists, so it needs to be dropped
|
||||||
opts = model._meta
|
opts = model._meta
|
||||||
for f in opts.fields:
|
for f in opts.local_fields:
|
||||||
if f.rel and f.rel.to not in to_delete:
|
if f.rel and f.rel.to not in to_delete:
|
||||||
references_to_delete.setdefault(f.rel.to, []).append( (model, f) )
|
references_to_delete.setdefault(f.rel.to, []).append( (model, f) )
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ def sql_delete(app, style):
|
|||||||
# Output DROP TABLE statements for many-to-many tables.
|
# Output DROP TABLE statements for many-to-many tables.
|
||||||
for model in app_models:
|
for model in app_models:
|
||||||
opts = model._meta
|
opts = model._meta
|
||||||
for f in opts.many_to_many:
|
for f in opts.local_many_to_many:
|
||||||
if isinstance(f.rel, generic.GenericRel):
|
if isinstance(f.rel, generic.GenericRel):
|
||||||
continue
|
continue
|
||||||
if cursor and table_name_converter(f.m2m_db_table()) in table_names:
|
if cursor and table_name_converter(f.m2m_db_table()) in table_names:
|
||||||
@ -256,7 +256,7 @@ def sql_model_create(model, style, known_models=set()):
|
|||||||
pending_references = {}
|
pending_references = {}
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
inline_references = connection.features.inline_fk_references
|
inline_references = connection.features.inline_fk_references
|
||||||
for f in opts.fields:
|
for f in opts.local_fields:
|
||||||
col_type = f.db_type()
|
col_type = f.db_type()
|
||||||
tablespace = f.db_tablespace or opts.db_tablespace
|
tablespace = f.db_tablespace or opts.db_tablespace
|
||||||
if col_type is None:
|
if col_type is None:
|
||||||
@ -351,7 +351,7 @@ def many_to_many_sql_for_model(model, style):
|
|||||||
final_output = []
|
final_output = []
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
inline_references = connection.features.inline_fk_references
|
inline_references = connection.features.inline_fk_references
|
||||||
for f in opts.many_to_many:
|
for f in opts.local_many_to_many:
|
||||||
if not isinstance(f.rel, generic.GenericRel):
|
if not isinstance(f.rel, generic.GenericRel):
|
||||||
tablespace = f.db_tablespace or opts.db_tablespace
|
tablespace = f.db_tablespace or opts.db_tablespace
|
||||||
if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
|
if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
|
||||||
@ -458,7 +458,7 @@ def sql_indexes_for_model(model, style):
|
|||||||
output = []
|
output = []
|
||||||
|
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
for f in model._meta.fields:
|
for f in model._meta.local_fields:
|
||||||
if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
|
if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
|
||||||
unique = f.unique and 'UNIQUE ' or ''
|
unique = f.unique and 'UNIQUE ' or ''
|
||||||
tablespace = f.db_tablespace or model._meta.db_tablespace
|
tablespace = f.db_tablespace or model._meta.db_tablespace
|
||||||
|
@ -32,7 +32,7 @@ def get_validation_errors(outfile, app=None):
|
|||||||
opts = cls._meta
|
opts = cls._meta
|
||||||
|
|
||||||
# Do field-specific validation.
|
# Do field-specific validation.
|
||||||
for f in opts.fields:
|
for f in opts.local_fields:
|
||||||
if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
|
if f.name == 'id' and not f.primary_key and opts.pk.name == 'id':
|
||||||
e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name)
|
e.add(opts, '"%s": You can\'t use "id" as a field name, because each model automatically gets an "id" field if none of the fields have primary_key=True. You need to either remove/rename your "id" field or add primary_key=True to a field.' % f.name)
|
||||||
if f.name.endswith('_'):
|
if f.name.endswith('_'):
|
||||||
@ -69,8 +69,8 @@ def get_validation_errors(outfile, app=None):
|
|||||||
if db_version < (5, 0, 3) and isinstance(f, (models.CharField, models.CommaSeparatedIntegerField, models.SlugField)) and f.max_length > 255:
|
if db_version < (5, 0, 3) and isinstance(f, (models.CharField, models.CommaSeparatedIntegerField, models.SlugField)) and f.max_length > 255:
|
||||||
e.add(opts, '"%s": %s cannot have a "max_length" greater than 255 when you are using a version of MySQL prior to 5.0.3 (you are using %s).' % (f.name, f.__class__.__name__, '.'.join([str(n) for n in db_version[:3]])))
|
e.add(opts, '"%s": %s cannot have a "max_length" greater than 255 when you are using a version of MySQL prior to 5.0.3 (you are using %s).' % (f.name, f.__class__.__name__, '.'.join([str(n) for n in db_version[:3]])))
|
||||||
|
|
||||||
# Check to see if the related field will clash with any
|
# Check to see if the related field will clash with any existing
|
||||||
# existing fields, m2m fields, m2m related objects or related objects
|
# fields, m2m fields, m2m related objects or related objects
|
||||||
if f.rel:
|
if f.rel:
|
||||||
if f.rel.to not in models.get_models():
|
if f.rel.to not in models.get_models():
|
||||||
e.add(opts, "'%s' has relation with model %s, which has not been installed" % (f.name, f.rel.to))
|
e.add(opts, "'%s' has relation with model %s, which has not been installed" % (f.name, f.rel.to))
|
||||||
@ -87,7 +87,7 @@ def get_validation_errors(outfile, app=None):
|
|||||||
e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
if r.name == rel_query_name:
|
if r.name == rel_query_name:
|
||||||
e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
for r in rel_opts.many_to_many:
|
for r in rel_opts.local_many_to_many:
|
||||||
if r.name == rel_name:
|
if r.name == rel_name:
|
||||||
e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
if r.name == rel_query_name:
|
if r.name == rel_query_name:
|
||||||
@ -104,9 +104,10 @@ def get_validation_errors(outfile, app=None):
|
|||||||
if r.get_accessor_name() == rel_query_name:
|
if r.get_accessor_name() == rel_query_name:
|
||||||
e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
|
e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
|
||||||
|
|
||||||
for i, f in enumerate(opts.many_to_many):
|
for i, f in enumerate(opts.local_many_to_many):
|
||||||
# Check to see if the related m2m field will clash with any
|
# Check to see if the related m2m field will clash with any
|
||||||
# existing fields, m2m fields, m2m related objects or related objects
|
# existing fields, m2m fields, m2m related objects or related
|
||||||
|
# objects
|
||||||
if f.rel.to not in models.get_models():
|
if f.rel.to not in models.get_models():
|
||||||
e.add(opts, "'%s' has m2m relation with model %s, which has not been installed" % (f.name, f.rel.to))
|
e.add(opts, "'%s' has m2m relation with model %s, which has not been installed" % (f.name, f.rel.to))
|
||||||
# it is a string and we could not find the model it refers to
|
# it is a string and we could not find the model it refers to
|
||||||
@ -117,17 +118,17 @@ def get_validation_errors(outfile, app=None):
|
|||||||
rel_opts = f.rel.to._meta
|
rel_opts = f.rel.to._meta
|
||||||
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
|
||||||
rel_query_name = f.related_query_name()
|
rel_query_name = f.related_query_name()
|
||||||
# If rel_name is none, there is no reverse accessor.
|
# If rel_name is none, there is no reverse accessor (this only
|
||||||
# (This only occurs for symmetrical m2m relations to self).
|
# occurs for symmetrical m2m relations to self). If this is the
|
||||||
# If this is the case, there are no clashes to check for this field, as
|
# case, there are no clashes to check for this field, as there are
|
||||||
# there are no reverse descriptors for this field.
|
# no reverse descriptors for this field.
|
||||||
if rel_name is not None:
|
if rel_name is not None:
|
||||||
for r in rel_opts.fields:
|
for r in rel_opts.fields:
|
||||||
if r.name == rel_name:
|
if r.name == rel_name:
|
||||||
e.add(opts, "Accessor for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Accessor for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
if r.name == rel_query_name:
|
if r.name == rel_query_name:
|
||||||
e.add(opts, "Reverse query name for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Reverse query name for m2m field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
for r in rel_opts.many_to_many:
|
for r in rel_opts.local_many_to_many:
|
||||||
if r.name == rel_name:
|
if r.name == rel_name:
|
||||||
e.add(opts, "Accessor for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
e.add(opts, "Accessor for m2m field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
|
||||||
if r.name == rel_query_name:
|
if r.name == rel_query_name:
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
import types
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from itertools import izip
|
||||||
|
|
||||||
import django.db.models.manipulators
|
import django.db.models.manipulators
|
||||||
import django.db.models.manager
|
import django.db.models.manager
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||||
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
|
from django.db.models.fields import AutoField, ImageField, FieldDoesNotExist
|
||||||
from django.db.models.fields.related import OneToOneRel, ManyToOneRel
|
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
|
||||||
from django.db.models.query import delete_objects, Q
|
from django.db.models.query import delete_objects, Q
|
||||||
from django.db.models.options import Options, AdminOptions
|
from django.db.models.options import Options, AdminOptions
|
||||||
from django.db import connection, transaction
|
from django.db import connection, transaction
|
||||||
@ -14,10 +19,6 @@ from django.utils.datastructures import SortedDict
|
|||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
from django.utils.encoding import smart_str, force_unicode, smart_unicode
|
from django.utils.encoding import smart_str, force_unicode, smart_unicode
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from itertools import izip
|
|
||||||
import types
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
class ModelBase(type):
|
class ModelBase(type):
|
||||||
"Metaclass for all models"
|
"Metaclass for all models"
|
||||||
@ -25,29 +26,46 @@ class ModelBase(type):
|
|||||||
# If this isn't a subclass of Model, don't do anything special.
|
# If this isn't a subclass of Model, don't do anything special.
|
||||||
try:
|
try:
|
||||||
parents = [b for b in bases if issubclass(b, Model)]
|
parents = [b for b in bases if issubclass(b, Model)]
|
||||||
if not parents:
|
|
||||||
return super(ModelBase, cls).__new__(cls, name, bases, attrs)
|
|
||||||
except NameError:
|
except NameError:
|
||||||
# 'Model' isn't defined yet, meaning we're looking at Django's own
|
# 'Model' isn't defined yet, meaning we're looking at Django's own
|
||||||
# Model class, defined below.
|
# Model class, defined below.
|
||||||
|
parents = []
|
||||||
|
if not parents:
|
||||||
return super(ModelBase, cls).__new__(cls, name, bases, attrs)
|
return super(ModelBase, cls).__new__(cls, name, bases, attrs)
|
||||||
|
|
||||||
# Create the class.
|
# Create the class.
|
||||||
new_class = type.__new__(cls, name, bases, {'__module__': attrs.pop('__module__')})
|
new_class = type.__new__(cls, name, bases, {'__module__': attrs.pop('__module__')})
|
||||||
new_class.add_to_class('_meta', Options(attrs.pop('Meta', None)))
|
meta = attrs.pop('Meta', None)
|
||||||
|
# FIXME: Promote Meta to a newstyle class before attaching it to the
|
||||||
|
# model.
|
||||||
|
## if meta:
|
||||||
|
## new_class.Meta = meta
|
||||||
|
new_class.add_to_class('_meta', Options(meta))
|
||||||
|
# FIXME: Need to be smarter here. Exception is an old-style class in
|
||||||
|
# Python <= 2.4, new-style in Python 2.5+. This construction is only
|
||||||
|
# really correct for old-style classes.
|
||||||
new_class.add_to_class('DoesNotExist', types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {}))
|
new_class.add_to_class('DoesNotExist', types.ClassType('DoesNotExist', (ObjectDoesNotExist,), {}))
|
||||||
new_class.add_to_class('MultipleObjectsReturned',
|
new_class.add_to_class('MultipleObjectsReturned', types.ClassType('MultipleObjectsReturned', (MultipleObjectsReturned, ), {}))
|
||||||
types.ClassType('MultipleObjectsReturned', (MultipleObjectsReturned, ), {}))
|
|
||||||
|
|
||||||
# Build complete list of parents
|
# Do the appropriate setup for any model parents.
|
||||||
|
abstract_parents = []
|
||||||
for base in parents:
|
for base in parents:
|
||||||
# Things without _meta aren't functional models, so they're
|
if not hasattr(base, '_meta'):
|
||||||
# uninteresting parents.
|
# Things without _meta aren't functional models, so they're
|
||||||
if hasattr(base, '_meta'):
|
# uninteresting parents.
|
||||||
new_class._meta.parents.append(base)
|
continue
|
||||||
new_class._meta.parents.extend(base._meta.parents)
|
if not base._meta.abstract:
|
||||||
|
attr_name = '%s_ptr' % base._meta.module_name
|
||||||
|
field = OneToOneField(base, name=attr_name, auto_created=True)
|
||||||
|
new_class.add_to_class(attr_name, field)
|
||||||
|
new_class._meta.parents[base] = field
|
||||||
|
else:
|
||||||
|
abstract_parents.append(base)
|
||||||
|
|
||||||
|
if getattr(new_class, '_default_manager', None) is not None:
|
||||||
|
# We have a parent who set the default manager. We need to override
|
||||||
|
# this.
|
||||||
|
new_class._default_manager = None
|
||||||
if getattr(new_class._meta, 'app_label', None) is None:
|
if getattr(new_class._meta, 'app_label', None) is None:
|
||||||
# Figure out the app_label by looking one level up.
|
# Figure out the app_label by looking one level up.
|
||||||
# For 'django.contrib.sites.models', this would be 'sites'.
|
# For 'django.contrib.sites.models', this would be 'sites'.
|
||||||
@ -63,21 +81,26 @@ class ModelBase(type):
|
|||||||
for obj_name, obj in attrs.items():
|
for obj_name, obj in attrs.items():
|
||||||
new_class.add_to_class(obj_name, obj)
|
new_class.add_to_class(obj_name, obj)
|
||||||
|
|
||||||
# Add Fields inherited from parents
|
for parent in abstract_parents:
|
||||||
for parent in new_class._meta.parents:
|
names = [f.name for f in new_class._meta.local_fields + new_class._meta.many_to_many]
|
||||||
for field in parent._meta.fields:
|
for field in parent._meta.local_fields:
|
||||||
# Only add parent fields if they aren't defined for this class.
|
if field.name in names:
|
||||||
try:
|
raise TypeError('Local field %r in class %r clashes with field of similar name from abstract base class %r'
|
||||||
new_class._meta.get_field(field.name)
|
% (field.name, name, parent.__name__))
|
||||||
except FieldDoesNotExist:
|
new_class.add_to_class(field.name, field)
|
||||||
field.contribute_to_class(new_class, field.name)
|
|
||||||
|
if new_class._meta.abstract:
|
||||||
|
# Abstract base models can't be instantiated and don't appear in
|
||||||
|
# the list of models for an app. We do the final setup for them a
|
||||||
|
# little differently from normal models.
|
||||||
|
return new_class
|
||||||
|
|
||||||
new_class._prepare()
|
new_class._prepare()
|
||||||
|
|
||||||
register_models(new_class._meta.app_label, new_class)
|
register_models(new_class._meta.app_label, new_class)
|
||||||
|
|
||||||
# Because of the way imports happen (recursively), we may or may not be
|
# Because of the way imports happen (recursively), we may or may not be
|
||||||
# the first class for this model to register with the framework. There
|
# the first time this model tries to register with the framework. There
|
||||||
# should only be one class for each model, so we must always return the
|
# should only be one class for each model, so we always return the
|
||||||
# registered version.
|
# registered version.
|
||||||
return get_model(new_class._meta.app_label, name, False)
|
return get_model(new_class._meta.app_label, name, False)
|
||||||
|
|
||||||
@ -113,8 +136,10 @@ class ModelBase(type):
|
|||||||
class Model(object):
|
class Model(object):
|
||||||
__metaclass__ = ModelBase
|
__metaclass__ = ModelBase
|
||||||
|
|
||||||
def _get_pk_val(self):
|
def _get_pk_val(self, meta=None):
|
||||||
return getattr(self, self._meta.pk.attname)
|
if not meta:
|
||||||
|
meta = self._meta
|
||||||
|
return getattr(self, meta.pk.attname)
|
||||||
|
|
||||||
def _set_pk_val(self, value):
|
def _set_pk_val(self, value):
|
||||||
return setattr(self, self._meta.pk.attname, value)
|
return setattr(self, self._meta.pk.attname, value)
|
||||||
@ -207,19 +232,30 @@ class Model(object):
|
|||||||
raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0]
|
raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0]
|
||||||
dispatcher.send(signal=signals.post_init, sender=self.__class__, instance=self)
|
dispatcher.send(signal=signals.post_init, sender=self.__class__, instance=self)
|
||||||
|
|
||||||
def save(self, raw=False):
|
def save(self, raw=False, cls=None):
|
||||||
dispatcher.send(signal=signals.pre_save, sender=self.__class__,
|
if not cls:
|
||||||
instance=self, raw=raw)
|
dispatcher.send(signal=signals.pre_save, sender=self.__class__,
|
||||||
|
instance=self, raw=raw)
|
||||||
|
cls = self.__class__
|
||||||
|
meta = self._meta
|
||||||
|
signal = True
|
||||||
|
else:
|
||||||
|
meta = cls._meta
|
||||||
|
signal = False
|
||||||
|
|
||||||
non_pks = [f for f in self._meta.fields if not f.primary_key]
|
for parent, field in meta.parents.items():
|
||||||
|
self.save(raw, parent)
|
||||||
|
setattr(self, field.attname, self._get_pk_val(parent._meta))
|
||||||
|
|
||||||
|
non_pks = [f for f in self._meta.local_fields if not f.primary_key]
|
||||||
|
|
||||||
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
|
# First, try an UPDATE. If that doesn't update anything, do an INSERT.
|
||||||
pk_val = self._get_pk_val()
|
pk_val = self._get_pk_val(meta)
|
||||||
# Note: the comparison with '' is required for compatibility with
|
# Note: the comparison with '' is required for compatibility with
|
||||||
# oldforms-style model creation.
|
# oldforms-style model creation.
|
||||||
pk_set = pk_val is not None and smart_unicode(pk_val) != u''
|
pk_set = pk_val is not None and smart_unicode(pk_val) != u''
|
||||||
record_exists = True
|
record_exists = True
|
||||||
manager = self.__class__._default_manager
|
manager = cls._default_manager
|
||||||
if pk_set:
|
if pk_set:
|
||||||
# Determine whether a record with the primary key already exists.
|
# Determine whether a record with the primary key already exists.
|
||||||
if manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by():
|
if manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by():
|
||||||
@ -231,16 +267,16 @@ class Model(object):
|
|||||||
record_exists = False
|
record_exists = False
|
||||||
if not pk_set or not record_exists:
|
if not pk_set or not record_exists:
|
||||||
if not pk_set:
|
if not pk_set:
|
||||||
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in self._meta.fields if not isinstance(f, AutoField)]
|
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields if not isinstance(f, AutoField)]
|
||||||
else:
|
else:
|
||||||
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in self._meta.fields]
|
values = [(f.name, f.get_db_prep_save(raw and getattr(self, f.attname) or f.pre_save(self, True))) for f in meta.local_fields]
|
||||||
|
|
||||||
if self._meta.order_with_respect_to:
|
if meta.order_with_respect_to:
|
||||||
field = self._meta.order_with_respect_to
|
field = meta.order_with_respect_to
|
||||||
values.append(('_order', manager.filter(**{field.name: getattr(self, field.attname)}).count()))
|
values.append(('_order', manager.filter(**{field.name: getattr(self, field.attname)}).count()))
|
||||||
record_exists = False
|
record_exists = False
|
||||||
|
|
||||||
update_pk = bool(self._meta.has_auto_field and not pk_set)
|
update_pk = bool(meta.has_auto_field and not pk_set)
|
||||||
if values:
|
if values:
|
||||||
# Create a new record.
|
# Create a new record.
|
||||||
result = manager._insert(_return_id=update_pk, **dict(values))
|
result = manager._insert(_return_id=update_pk, **dict(values))
|
||||||
@ -250,12 +286,13 @@ class Model(object):
|
|||||||
_raw_values=True, pk=connection.ops.pk_default_value())
|
_raw_values=True, pk=connection.ops.pk_default_value())
|
||||||
|
|
||||||
if update_pk:
|
if update_pk:
|
||||||
setattr(self, self._meta.pk.attname, result)
|
setattr(self, meta.pk.attname, result)
|
||||||
transaction.commit_unless_managed()
|
transaction.commit_unless_managed()
|
||||||
|
|
||||||
# Run any post-save hooks.
|
if signal:
|
||||||
dispatcher.send(signal=signals.post_save, sender=self.__class__,
|
# Run any post-save hooks.
|
||||||
instance=self, created=(not record_exists), raw=raw)
|
dispatcher.send(signal=signals.post_save, sender=self.__class__,
|
||||||
|
instance=self, created=(not record_exists), raw=raw)
|
||||||
|
|
||||||
save.alters_data = True
|
save.alters_data = True
|
||||||
|
|
||||||
|
@ -75,15 +75,19 @@ class Field(object):
|
|||||||
# database level.
|
# database level.
|
||||||
empty_strings_allowed = True
|
empty_strings_allowed = True
|
||||||
|
|
||||||
# Tracks each time a Field instance is created. Used to retain order.
|
# These track each time a Field instance is created. Used to retain order.
|
||||||
|
# The auto_creation_counter is used for fields that Django implicitly
|
||||||
|
# creates, creation_counter is used for all user-specified fields.
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
|
auto_creation_counter = -1
|
||||||
|
|
||||||
def __init__(self, verbose_name=None, name=None, primary_key=False,
|
def __init__(self, verbose_name=None, name=None, primary_key=False,
|
||||||
max_length=None, unique=False, blank=False, null=False, db_index=False,
|
max_length=None, unique=False, blank=False, null=False,
|
||||||
core=False, rel=None, default=NOT_PROVIDED, editable=True, serialize=True,
|
db_index=False, core=False, rel=None, default=NOT_PROVIDED,
|
||||||
prepopulate_from=None, unique_for_date=None, unique_for_month=None,
|
editable=True, serialize=True, prepopulate_from=None,
|
||||||
unique_for_year=None, validator_list=None, choices=None, radio_admin=None,
|
unique_for_date=None, unique_for_month=None, unique_for_year=None,
|
||||||
help_text='', db_column=None, db_tablespace=None):
|
validator_list=None, choices=None, radio_admin=None, help_text='',
|
||||||
|
db_column=None, db_tablespace=None, auto_created=False):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.verbose_name = verbose_name
|
self.verbose_name = verbose_name
|
||||||
self.primary_key = primary_key
|
self.primary_key = primary_key
|
||||||
@ -109,9 +113,13 @@ class Field(object):
|
|||||||
# Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
|
# Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
|
||||||
self.db_index = db_index
|
self.db_index = db_index
|
||||||
|
|
||||||
# Increase the creation counter, and save our local copy.
|
# Adjust the appropriate creation counter, and save our local copy.
|
||||||
self.creation_counter = Field.creation_counter
|
if auto_created:
|
||||||
Field.creation_counter += 1
|
self.creation_counter = Field.auto_creation_counter
|
||||||
|
Field.auto_creation_counter -= 1
|
||||||
|
else:
|
||||||
|
self.creation_counter = Field.creation_counter
|
||||||
|
Field.creation_counter += 1
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
# This is needed because bisect does not take a comparison function.
|
# This is needed because bisect does not take a comparison function.
|
||||||
|
@ -494,8 +494,9 @@ class OneToOneRel(ManyToOneRel):
|
|||||||
# ignored here. We accept them as parameters only to match the calling
|
# ignored here. We accept them as parameters only to match the calling
|
||||||
# signature of ManyToOneRel.__init__().
|
# signature of ManyToOneRel.__init__().
|
||||||
super(OneToOneRel, self).__init__(to, field_name, num_in_admin,
|
super(OneToOneRel, self).__init__(to, field_name, num_in_admin,
|
||||||
edit_inline, related_name, limit_choices_to, lookup_overrides,
|
edit_inline=edit_inline, related_name=related_name,
|
||||||
raw_id_admin)
|
limit_choices_to=limit_choices_to,
|
||||||
|
lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin)
|
||||||
self.multiple = False
|
self.multiple = False
|
||||||
|
|
||||||
class ManyToManyRel(object):
|
class ManyToManyRel(object):
|
||||||
|
@ -5,7 +5,7 @@ from django.db.models.fields import FieldDoesNotExist
|
|||||||
|
|
||||||
def ensure_default_manager(sender):
|
def ensure_default_manager(sender):
|
||||||
cls = sender
|
cls = sender
|
||||||
if not hasattr(cls, '_default_manager'):
|
if not hasattr(cls, '_default_manager') or cls._default_manager is None:
|
||||||
# Create the default manager, if needed.
|
# Create the default manager, if needed.
|
||||||
try:
|
try:
|
||||||
cls._meta.get_field('objects')
|
cls._meta.get_field('objects')
|
||||||
@ -31,7 +31,7 @@ class Manager(object):
|
|||||||
# TODO: Use weakref because of possible memory leak / circular reference.
|
# TODO: Use weakref because of possible memory leak / circular reference.
|
||||||
self.model = model
|
self.model = model
|
||||||
setattr(model, name, ManagerDescriptor(self))
|
setattr(model, name, ManagerDescriptor(self))
|
||||||
if not hasattr(model, '_default_manager') or self.creation_counter < model._default_manager.creation_counter:
|
if not hasattr(model, '_default_manager') or model._default_manager is None or self.creation_counter < model._default_manager.creation_counter:
|
||||||
model._default_manager = self
|
model._default_manager = self
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import re
|
||||||
|
from bisect import bisect
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.related import RelatedObject
|
from django.db.models.related import RelatedObject
|
||||||
from django.db.models.fields.related import ManyToManyRel
|
from django.db.models.fields.related import ManyToManyRel
|
||||||
@ -7,19 +10,19 @@ from django.db.models.loading import get_models, app_cache_ready
|
|||||||
from django.db.models import Manager
|
from django.db.models import Manager
|
||||||
from django.utils.translation import activate, deactivate_all, get_language, string_concat
|
from django.utils.translation import activate, deactivate_all, get_language, string_concat
|
||||||
from django.utils.encoding import force_unicode, smart_str
|
from django.utils.encoding import force_unicode, smart_str
|
||||||
from bisect import bisect
|
from django.utils.datastructures import SortedDict
|
||||||
import re
|
|
||||||
|
|
||||||
# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
|
# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
|
||||||
get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
|
get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
|
||||||
|
|
||||||
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
|
DEFAULT_NAMES = ('verbose_name', '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')
|
||||||
|
|
||||||
class Options(object):
|
class Options(object):
|
||||||
def __init__(self, meta):
|
def __init__(self, meta):
|
||||||
self.fields, self.many_to_many = [], []
|
self.local_fields, self.local_many_to_many = [], []
|
||||||
self.module_name, self.verbose_name = None, None
|
self.module_name, self.verbose_name = None, None
|
||||||
self.verbose_name_plural = None
|
self.verbose_name_plural = None
|
||||||
self.db_table = ''
|
self.db_table = ''
|
||||||
@ -35,7 +38,8 @@ class Options(object):
|
|||||||
self.pk = None
|
self.pk = None
|
||||||
self.has_auto_field, self.auto_field = False, None
|
self.has_auto_field, self.auto_field = False, None
|
||||||
self.one_to_one_field = None
|
self.one_to_one_field = None
|
||||||
self.parents = []
|
self.abstract = False
|
||||||
|
self.parents = SortedDict()
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
cls._meta = self
|
cls._meta = self
|
||||||
@ -82,9 +86,16 @@ class Options(object):
|
|||||||
self.order_with_respect_to = None
|
self.order_with_respect_to = None
|
||||||
|
|
||||||
if self.pk is None:
|
if self.pk is None:
|
||||||
auto = AutoField(verbose_name='ID', primary_key=True)
|
if self.parents:
|
||||||
auto.creation_counter = -1
|
# Promote the first parent link in lieu of adding yet another
|
||||||
model.add_to_class('id', auto)
|
# field.
|
||||||
|
field = self.parents.value_for_index(0)
|
||||||
|
field.primary_key = True
|
||||||
|
self.pk = field
|
||||||
|
else:
|
||||||
|
auto = AutoField(verbose_name='ID', primary_key=True,
|
||||||
|
auto_created=True)
|
||||||
|
model.add_to_class('id', auto)
|
||||||
|
|
||||||
# If the db_table wasn't provided, use the app_label + module_name.
|
# If the db_table wasn't provided, use the app_label + module_name.
|
||||||
if not self.db_table:
|
if not self.db_table:
|
||||||
@ -97,13 +108,23 @@ class Options(object):
|
|||||||
# Move many-to-many related fields from self.fields into
|
# Move many-to-many related fields from self.fields into
|
||||||
# self.many_to_many.
|
# self.many_to_many.
|
||||||
if field.rel and isinstance(field.rel, ManyToManyRel):
|
if field.rel and isinstance(field.rel, ManyToManyRel):
|
||||||
self.many_to_many.insert(bisect(self.many_to_many, field), field)
|
self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
|
||||||
else:
|
else:
|
||||||
self.fields.insert(bisect(self.fields, field), field)
|
self.local_fields.insert(bisect(self.local_fields, field), field)
|
||||||
if not self.pk and field.primary_key:
|
if not self.pk and field.primary_key:
|
||||||
self.pk = field
|
self.pk = field
|
||||||
field.serialize = False
|
field.serialize = False
|
||||||
|
|
||||||
|
# All of these internal caches need to be updated the next time they
|
||||||
|
# are used.
|
||||||
|
# TODO: Do this more neatly. (Also, use less caches!)
|
||||||
|
if hasattr(self, '_field_cache'):
|
||||||
|
del self._field_cache
|
||||||
|
if hasattr(self, '_m2m_cache'):
|
||||||
|
del self._m2m_cache
|
||||||
|
if hasattr(self, '_name_map'):
|
||||||
|
del self._name_map
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Options for %s>' % self.object_name
|
return '<Options for %s>' % self.object_name
|
||||||
|
|
||||||
@ -123,8 +144,76 @@ class Options(object):
|
|||||||
return raw
|
return raw
|
||||||
verbose_name_raw = property(verbose_name_raw)
|
verbose_name_raw = property(verbose_name_raw)
|
||||||
|
|
||||||
|
def _fields(self):
|
||||||
|
"""
|
||||||
|
The getter for self.fields. This returns the list of field objects
|
||||||
|
available to this model (including through parent models).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._field_cache
|
||||||
|
except AttributeError:
|
||||||
|
self._fill_fields_cache()
|
||||||
|
return self._field_cache.keys()
|
||||||
|
fields = property(_fields)
|
||||||
|
|
||||||
|
def get_fields_with_model(self):
|
||||||
|
"""
|
||||||
|
Returns a list of (field, model) pairs for all fields. The "model"
|
||||||
|
element is None for fields on the current model. Mostly of use when
|
||||||
|
constructing queries so that we know which model a field belongs to.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._field_cache
|
||||||
|
except AttributeError:
|
||||||
|
self._fill_fields_cache()
|
||||||
|
return self._field_cache.items()
|
||||||
|
|
||||||
|
def _fill_fields_cache(self):
|
||||||
|
cache = SortedDict()
|
||||||
|
for parent in self.parents:
|
||||||
|
for field, model in parent._meta.get_fields_with_model():
|
||||||
|
if model:
|
||||||
|
cache[field] = model
|
||||||
|
else:
|
||||||
|
cache[field] = parent
|
||||||
|
for field in self.local_fields:
|
||||||
|
cache[field] = None
|
||||||
|
self._field_cache = cache
|
||||||
|
|
||||||
|
def _many_to_many(self):
|
||||||
|
try:
|
||||||
|
self._m2m_cache
|
||||||
|
except AttributeError:
|
||||||
|
self._fill_m2m_cache()
|
||||||
|
return self._m2m_cache.keys()
|
||||||
|
many_to_many = property(_many_to_many)
|
||||||
|
|
||||||
|
def get_m2m_with_model(self):
|
||||||
|
"""
|
||||||
|
The many-to-many version of get_fields_with_model().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._m2m_cache
|
||||||
|
except AttributeError:
|
||||||
|
self._fill_m2m_cache()
|
||||||
|
return self._m2m_cache.items()
|
||||||
|
|
||||||
|
def _fill_m2m_cache(self):
|
||||||
|
cache = SortedDict()
|
||||||
|
for parent in self.parents:
|
||||||
|
for field, model in parent._meta.get_m2m_with_model():
|
||||||
|
if model:
|
||||||
|
cache[field] = model
|
||||||
|
else:
|
||||||
|
cache[field] = parent
|
||||||
|
for field in self.local_many_to_many:
|
||||||
|
cache[field] = None
|
||||||
|
self._m2m_cache = cache
|
||||||
|
|
||||||
def get_field(self, name, many_to_many=True):
|
def get_field(self, name, many_to_many=True):
|
||||||
"Returns the requested field by name. Raises FieldDoesNotExist on error."
|
"""
|
||||||
|
Returns the requested field by name. Raises FieldDoesNotExist on error.
|
||||||
|
"""
|
||||||
to_search = many_to_many and (self.fields + self.many_to_many) or self.fields
|
to_search = many_to_many and (self.fields + self.many_to_many) or self.fields
|
||||||
for f in to_search:
|
for f in to_search:
|
||||||
if f.name == name:
|
if f.name == name:
|
||||||
@ -133,8 +222,9 @@ class Options(object):
|
|||||||
|
|
||||||
def get_field_by_name(self, name, only_direct=False):
|
def get_field_by_name(self, name, only_direct=False):
|
||||||
"""
|
"""
|
||||||
Returns the (field_object, direct, m2m), where field_object is the
|
Returns the (field_object, model, direct, m2m), where field_object is
|
||||||
Field instance for the given name, direct is True if the field exists
|
the Field instance for the given name, model is the model containing
|
||||||
|
this field (None for local fields), direct is True if the field exists
|
||||||
on this model, and m2m is True for many-to-many relations. When
|
on this model, and m2m is True for many-to-many relations. When
|
||||||
'direct' is False, 'field_object' is the corresponding RelatedObject
|
'direct' is False, 'field_object' is the corresponding RelatedObject
|
||||||
for this field (since the field doesn't have an instance associated
|
for this field (since the field doesn't have an instance associated
|
||||||
@ -151,7 +241,7 @@ class Options(object):
|
|||||||
cache = self.init_name_map()
|
cache = self.init_name_map()
|
||||||
result = cache.get(name)
|
result = cache.get(name)
|
||||||
|
|
||||||
if not result or (not result[1] and only_direct):
|
if not result or (only_direct and not result[2]):
|
||||||
raise FieldDoesNotExist('%s has no field named %r'
|
raise FieldDoesNotExist('%s has no field named %r'
|
||||||
% (self.object_name, name))
|
% (self.object_name, name))
|
||||||
return result
|
return result
|
||||||
@ -173,15 +263,16 @@ class Options(object):
|
|||||||
"""
|
"""
|
||||||
Initialises the field name -> field object mapping.
|
Initialises the field name -> field object mapping.
|
||||||
"""
|
"""
|
||||||
cache = dict([(f.name, (f, True, False)) for f in self.fields])
|
cache = dict([(f.name, (f, m, True, False)) for f, m in
|
||||||
for f in self.many_to_many:
|
self.get_fields_with_model()])
|
||||||
cache[f.name] = (f, True, True)
|
for f, model in self.get_m2m_with_model():
|
||||||
for f in self.get_all_related_many_to_many_objects():
|
cache[f.name] = (f, model, True, True)
|
||||||
cache[f.field.related_query_name()] = (f, False, True)
|
for f, model in self.get_all_related_m2m_objects_with_model():
|
||||||
for f in self.get_all_related_objects():
|
cache[f.field.related_query_name()] = (f, model, False, True)
|
||||||
cache[f.field.related_query_name()] = (f, False, False)
|
for f, model in self.get_all_related_objects_with_model():
|
||||||
|
cache[f.field.related_query_name()] = (f, model, False, False)
|
||||||
if self.order_with_respect_to:
|
if self.order_with_respect_to:
|
||||||
cache['_order'] = OrderWrt(), True, False
|
cache['_order'] = OrderWrt(), None, True, False
|
||||||
if app_cache_ready():
|
if app_cache_ready():
|
||||||
self._name_map = cache
|
self._name_map = cache
|
||||||
return cache
|
return cache
|
||||||
@ -195,17 +286,81 @@ class Options(object):
|
|||||||
def get_delete_permission(self):
|
def get_delete_permission(self):
|
||||||
return 'delete_%s' % self.object_name.lower()
|
return 'delete_%s' % self.object_name.lower()
|
||||||
|
|
||||||
def get_all_related_objects(self):
|
def get_all_related_objects(self, local_only=False):
|
||||||
try: # Try the cache first.
|
try:
|
||||||
return self._all_related_objects
|
self._related_objects_cache
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
rel_objs = []
|
self._fill_related_objects_cache()
|
||||||
for klass in get_models():
|
if local_only:
|
||||||
for f in klass._meta.fields:
|
return [k for k, v in self._related_objects_cache.items() if not v]
|
||||||
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
|
return self._related_objects_cache.keys()
|
||||||
rel_objs.append(RelatedObject(f.rel.to, klass, f))
|
|
||||||
self._all_related_objects = rel_objs
|
def get_all_related_objects_with_model(self):
|
||||||
return rel_objs
|
"""
|
||||||
|
Returns a list of (related-object, model) pairs. Similar to
|
||||||
|
get_fields_with_model().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._related_objects_cache
|
||||||
|
except AttributeError:
|
||||||
|
self._fill_related_objects_cache()
|
||||||
|
return self._related_objects_cache.items()
|
||||||
|
|
||||||
|
def _fill_related_objects_cache(self):
|
||||||
|
cache = SortedDict()
|
||||||
|
parent_list = self.get_parent_list()
|
||||||
|
for parent in self.parents:
|
||||||
|
for obj, model in parent._meta.get_all_related_objects_with_model():
|
||||||
|
if obj.field.creation_counter < 0 and obj.model not in parent_list:
|
||||||
|
continue
|
||||||
|
if not model:
|
||||||
|
cache[obj] = parent
|
||||||
|
else:
|
||||||
|
cache[obj] = model
|
||||||
|
for klass in get_models():
|
||||||
|
for f in klass._meta.local_fields:
|
||||||
|
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
|
||||||
|
cache[RelatedObject(f.rel.to, klass, f)] = None
|
||||||
|
self._related_objects_cache = cache
|
||||||
|
|
||||||
|
def get_all_related_many_to_many_objects(self, local_only=False):
|
||||||
|
try:
|
||||||
|
cache = self._related_many_to_many_cache
|
||||||
|
except AttributeError:
|
||||||
|
cache = self._fill_related_many_to_many_cache()
|
||||||
|
if local_only:
|
||||||
|
return [k for k, v in cache.items() if not v]
|
||||||
|
return cache.keys()
|
||||||
|
|
||||||
|
def get_all_related_m2m_objects_with_model(self):
|
||||||
|
"""
|
||||||
|
Returns a list of (related-m2m-object, model) pairs. Similar to
|
||||||
|
get_fields_with_model().
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cache = self._related_many_to_many_cache
|
||||||
|
except AttributeError:
|
||||||
|
cache = self._fill_related_many_to_many_cache()
|
||||||
|
return cache.items()
|
||||||
|
|
||||||
|
def _fill_related_many_to_many_cache(self):
|
||||||
|
cache = SortedDict()
|
||||||
|
parent_list = self.get_parent_list()
|
||||||
|
for parent in self.parents:
|
||||||
|
for obj, model in parent._meta.get_all_related_m2m_objects_with_model():
|
||||||
|
if obj.field.creation_counter < 0 and obj.model not in parent_list:
|
||||||
|
continue
|
||||||
|
if not model:
|
||||||
|
cache[obj] = parent
|
||||||
|
else:
|
||||||
|
cache[obj] = model
|
||||||
|
for klass in get_models():
|
||||||
|
for f in klass._meta.local_many_to_many:
|
||||||
|
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
|
||||||
|
cache[RelatedObject(f.rel.to, klass, f)] = None
|
||||||
|
if app_cache_ready():
|
||||||
|
self._related_many_to_many_cache = cache
|
||||||
|
return cache
|
||||||
|
|
||||||
def get_followed_related_objects(self, follow=None):
|
def get_followed_related_objects(self, follow=None):
|
||||||
if follow == None:
|
if follow == None:
|
||||||
@ -229,18 +384,35 @@ class Options(object):
|
|||||||
follow[f.name] = fol
|
follow[f.name] = fol
|
||||||
return follow
|
return follow
|
||||||
|
|
||||||
def get_all_related_many_to_many_objects(self):
|
def get_base_chain(self, model):
|
||||||
try: # Try the cache first.
|
"""
|
||||||
return self._all_related_many_to_many_objects
|
Returns a list of parent classes leading to 'model' (order from closet
|
||||||
except AttributeError:
|
to most distant ancestor). This has to handle the case were 'model' is
|
||||||
rel_objs = []
|
a granparent or even more distant relation.
|
||||||
for klass in get_models():
|
"""
|
||||||
for f in klass._meta.many_to_many:
|
if not self.parents:
|
||||||
if f.rel and not isinstance(f.rel.to, str) and self == f.rel.to._meta:
|
return
|
||||||
rel_objs.append(RelatedObject(f.rel.to, klass, f))
|
if model in self.parents:
|
||||||
if app_cache_ready():
|
return [model]
|
||||||
self._all_related_many_to_many_objects = rel_objs
|
for parent in self.parents:
|
||||||
return rel_objs
|
res = parent._meta.get_base_chain(model)
|
||||||
|
if res:
|
||||||
|
res.insert(0, parent)
|
||||||
|
return res
|
||||||
|
raise TypeError('%r is not an ancestor of this model'
|
||||||
|
% model._meta.module_name)
|
||||||
|
|
||||||
|
def get_parent_list(self):
|
||||||
|
"""
|
||||||
|
Returns a list of all the ancestor of this model as a list. Useful for
|
||||||
|
determining if something is an ancestor, regardless of lineage.
|
||||||
|
"""
|
||||||
|
# FIXME: Fix model hashing and then use a Set here.
|
||||||
|
result = []
|
||||||
|
for parent in self.parents:
|
||||||
|
result.append(parent)
|
||||||
|
result.extend(parent._meta.get_parent_list())
|
||||||
|
return result
|
||||||
|
|
||||||
def get_ordered_objects(self):
|
def get_ordered_objects(self):
|
||||||
"Returns a list of Options objects that are ordered with respect to this object."
|
"Returns a list of Options objects that are ordered with respect to this object."
|
||||||
|
@ -389,8 +389,13 @@ class Query(object):
|
|||||||
aliases.append(col.alias)
|
aliases.append(col.alias)
|
||||||
elif self.default_cols:
|
elif self.default_cols:
|
||||||
table_alias = self.tables[0]
|
table_alias = self.tables[0]
|
||||||
result = ['%s.%s' % (qn(table_alias), qn(f.column))
|
root_pk = self.model._meta.pk.column
|
||||||
for f in self.model._meta.fields]
|
seen = {None: table_alias}
|
||||||
|
for field, model in self.model._meta.get_fields_with_model():
|
||||||
|
if model not in seen:
|
||||||
|
seen[model] = self.join((table_alias, model._meta.db_table,
|
||||||
|
root_pk, model._meta.pk.column))
|
||||||
|
result.append('%s.%s' % (qn(seen[model]), qn(field.column)))
|
||||||
aliases = result[:]
|
aliases = result[:]
|
||||||
|
|
||||||
result.extend(['(%s) AS %s' % (col, alias)
|
result.extend(['(%s) AS %s' % (col, alias)
|
||||||
@ -742,7 +747,7 @@ class Query(object):
|
|||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
alias = self.join((None, opts.db_table, None, None))
|
alias = self.join((None, opts.db_table, None, None))
|
||||||
|
|
||||||
field, target, unused, join_list, = self.setup_joins(parts, opts,
|
field, target, opts, join_list, = self.setup_joins(parts, opts,
|
||||||
alias, (connector == AND))
|
alias, (connector == AND))
|
||||||
col = target.column
|
col = target.column
|
||||||
alias = join_list[-1][-1]
|
alias = join_list[-1][-1]
|
||||||
@ -850,11 +855,21 @@ class Query(object):
|
|||||||
name = opts.pk.name
|
name = opts.pk.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
field, direct, m2m = opts.get_field_by_name(name)
|
field, model, direct, m2m = opts.get_field_by_name(name)
|
||||||
except FieldDoesNotExist:
|
except FieldDoesNotExist:
|
||||||
names = opts.get_all_field_names()
|
names = opts.get_all_field_names()
|
||||||
raise TypeError("Cannot resolve keyword %r into field. "
|
raise TypeError("Cannot resolve keyword %r into field. "
|
||||||
"Choices are: %s" % (name, ", ".join(names)))
|
"Choices are: %s" % (name, ", ".join(names)))
|
||||||
|
if model:
|
||||||
|
# The field lives on a base class of the current model.
|
||||||
|
alias_list = []
|
||||||
|
for int_model in opts.get_base_chain(model):
|
||||||
|
lhs_col = opts.parents[int_model].column
|
||||||
|
opts = int_model._meta
|
||||||
|
alias = self.join((alias, opts.db_table, lhs_col,
|
||||||
|
opts.pk.column))
|
||||||
|
alias_list.append(alias)
|
||||||
|
joins.append(alias_list)
|
||||||
cached_data = opts._join_cache.get(name)
|
cached_data = opts._join_cache.get(name)
|
||||||
orig_opts = opts
|
orig_opts = opts
|
||||||
|
|
||||||
@ -899,6 +914,7 @@ class Query(object):
|
|||||||
nullable=field.null)
|
nullable=field.null)
|
||||||
joins.append([alias])
|
joins.append([alias])
|
||||||
else:
|
else:
|
||||||
|
# Non-relation fields.
|
||||||
target = field
|
target = field
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -1242,7 +1258,7 @@ class UpdateQuery(Query):
|
|||||||
def add_update_values(self, values):
|
def add_update_values(self, values):
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
for name, val in values.items():
|
for name, val in values.items():
|
||||||
field, direct, m2m = self.model._meta.get_field_by_name(name)
|
field, model, direct, m2m = self.model._meta.get_field_by_name(name)
|
||||||
if not direct or m2m:
|
if not direct or m2m:
|
||||||
# Can only update non-relation fields and foreign keys.
|
# Can only update non-relation fields and foreign keys.
|
||||||
raise TypeError('Cannot update model field %r (only non-relations and foreign keys permitted).' % field)
|
raise TypeError('Cannot update model field %r (only non-relations and foreign keys permitted).' % field)
|
||||||
|
@ -2028,6 +2028,105 @@ You can also prevent saving::
|
|||||||
|
|
||||||
.. _database API docs: ../db-api/
|
.. _database API docs: ../db-api/
|
||||||
|
|
||||||
|
Model inheritance
|
||||||
|
=================
|
||||||
|
|
||||||
|
Abstract base classes
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Abstract base classes are useful when you want to put some common information
|
||||||
|
into a number of other models. You write your base class and put
|
||||||
|
``abstract=True`` in the ``Meta`` class. This model will then not be used to
|
||||||
|
create any database table. Instead, when it is used as a base class for other
|
||||||
|
models, its fields will be added to those of the child class. It is an error
|
||||||
|
to have fields in the abstract base class with the same name as those in the
|
||||||
|
child (and Django will raise an exception).
|
||||||
|
|
||||||
|
An example::
|
||||||
|
|
||||||
|
class CommonInfo(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
age = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class Student(CommonInfo):
|
||||||
|
home_group = models.CharField(max_length=5)
|
||||||
|
|
||||||
|
The ``Student`` model will have three fields: ``name``, ``age`` and
|
||||||
|
``home_group``. The ``CommonInfo`` model cannot be used as a normal Django
|
||||||
|
model, since it is an abstract base class. It does not generate a database
|
||||||
|
table or have a manager or anything like that.
|
||||||
|
|
||||||
|
For many uses, this type of model inheritance will be exactly what you want.
|
||||||
|
It provides a way to factor out common information at the Python level, whilst
|
||||||
|
still only creating one database table per child model at the database level.
|
||||||
|
|
||||||
|
Multi-table inheritance
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
The second type of model inheritance supported by Django is when each model in
|
||||||
|
the hierarchy is a model all by itself. Each model corresponds to its own
|
||||||
|
database table and can be queried and created indvidually. The inheritance
|
||||||
|
relationship introduces links between the child model and each of its parents
|
||||||
|
(via an automatically created ``OneToOneField``). For example::
|
||||||
|
|
||||||
|
class Place(models.Model):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
address = models.CharField(max_length=80)
|
||||||
|
|
||||||
|
class Restaurant(Place):
|
||||||
|
serves_hot_dogs = models.BooleanField()
|
||||||
|
serves_pizza = models.BooleanField()
|
||||||
|
|
||||||
|
All of the fields of ``Place`` will also be available in ``Restaurant``,
|
||||||
|
although the data will reside in a different database table. So these are both
|
||||||
|
possible::
|
||||||
|
|
||||||
|
>>> Place.objects.filter(name="Bob's Cafe")
|
||||||
|
>>> Restaurant.objects.filter(name="Bob's Cafe")
|
||||||
|
|
||||||
|
If you have a ``Place`` that is also a ``Restaurant``, you can get from the
|
||||||
|
``Place`` object to the ``Restaurant`` object by using the lower-case version
|
||||||
|
of the model name::
|
||||||
|
|
||||||
|
>>> p = Place.objects.filter(name="Bob's Cafe")
|
||||||
|
# If Bob's Cafe is a Restaurant object, this will give the child class:
|
||||||
|
>>> p.restaurant
|
||||||
|
<Restaurant: ...>
|
||||||
|
|
||||||
|
However, if ``p`` in the above example was *not* a ``Restaurant`` (it had been
|
||||||
|
created directly as a ``Place`` object or was the parent of some other class),
|
||||||
|
referring to ``p.restaurant`` would give an error.
|
||||||
|
|
||||||
|
Normally you won't need to worry too much about how model inheritance works.
|
||||||
|
It will behave similarly to Python class inheritance.
|
||||||
|
|
||||||
|
Inheritance and reverse relations
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Because multi-table inheritance uses an implicit ``OneToOneField`` to link the
|
||||||
|
child and the parent, it's possible to move from the parent down to the child,
|
||||||
|
as in the above example. However, this uses up the name that is the default
|
||||||
|
``related_name`` value for ``ForeignKey`` and ``ManyToManyField`` relations.
|
||||||
|
If you are putting those type of relations on a subclass of another model, you
|
||||||
|
**must** specify the ``related_name`` attribute on each such field. If you
|
||||||
|
forget, Django will raise an error when you run ``manage.py validate`` or try
|
||||||
|
to syncdb.
|
||||||
|
|
||||||
|
For example, using the above ``Place`` class again, let's create another
|
||||||
|
subclass with a ``ManyToManyField``::
|
||||||
|
|
||||||
|
class Supplier(Place):
|
||||||
|
# Must specify related_name on all relations.
|
||||||
|
customers = models.ManyToManyField(Restaurant,
|
||||||
|
related_name='provider')
|
||||||
|
|
||||||
|
For more information about reverse relations, refer to the `Database API
|
||||||
|
reference`_ . For now, just remember to run ``manage.py validate`` when
|
||||||
|
you're writing your models and pay attention to the error messages.
|
||||||
|
|
||||||
Models across files
|
Models across files
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
@ -1,11 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
XX. Model inheritance
|
XX. Model inheritance
|
||||||
|
|
||||||
Model inheritance isn't yet supported.
|
Model inheritance exists in two varieties:
|
||||||
|
- abstract base classes which are a way of specifying common
|
||||||
|
information inherited by the subclasses. They don't exist as a separate
|
||||||
|
model.
|
||||||
|
- non-abstract base classes (the default), which are models in their own
|
||||||
|
right with their own database tables and everything. Their subclasses
|
||||||
|
have references back to them, created automatically.
|
||||||
|
|
||||||
|
Both styles are demonstrated here.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
class CommonInfo(models.Model):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
age = models.PositiveIntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u'%s %s' % (self.__class__.__name__, self.name)
|
||||||
|
|
||||||
|
class Worker(CommonInfo):
|
||||||
|
job = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
class Student(CommonInfo):
|
||||||
|
school_class = models.CharField(max_length=10)
|
||||||
|
|
||||||
class Place(models.Model):
|
class Place(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
address = models.CharField(max_length=80)
|
address = models.CharField(max_length=80)
|
||||||
@ -26,16 +50,44 @@ class ItalianRestaurant(Restaurant):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u"%s the italian restaurant" % self.name
|
return u"%s the italian restaurant" % self.name
|
||||||
|
|
||||||
# XFAIL: Recent changes to model saving mean these now fail catastrophically.
|
class Supplier(Place):
|
||||||
# They'll be re-enabled when the porting is a bit further along.
|
customers = models.ManyToManyField(Restaurant, related_name='provider')
|
||||||
not__test__ = {'API_TESTS':"""
|
|
||||||
# Make sure Restaurant has the right fields in the right order.
|
|
||||||
>>> [f.name for f in Restaurant._meta.fields]
|
|
||||||
['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza']
|
|
||||||
|
|
||||||
# Make sure ItalianRestaurant has the right fields in the right order.
|
def __unicode__(self):
|
||||||
>>> [f.name for f in ItalianRestaurant._meta.fields]
|
return u"%s the supplier" % self.name
|
||||||
['id', 'name', 'address', 'serves_hot_dogs', 'serves_pizza', 'serves_gnocchi']
|
|
||||||
|
class ParkingLot(Place):
|
||||||
|
main_site = models.ForeignKey(Place, related_name='lot')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"%s the parking lot" % self.name
|
||||||
|
|
||||||
|
__test__ = {'API_TESTS':"""
|
||||||
|
# The Student and Worker models both have 'name' and 'age' fields on them and
|
||||||
|
# inherit the __unicode__() method, just as with normal Python subclassing.
|
||||||
|
# This is useful if you want to factor out common information for programming
|
||||||
|
# purposes, but still completely independent separate models at the database
|
||||||
|
# level.
|
||||||
|
|
||||||
|
>>> w = Worker(name='Fred', age=35, job='Quarry worker')
|
||||||
|
>>> w.save()
|
||||||
|
>>> s = Student(name='Pebbles', age=5, school_class='1B')
|
||||||
|
>>> s.save()
|
||||||
|
>>> unicode(w)
|
||||||
|
u'Worker Fred'
|
||||||
|
>>> unicode(s)
|
||||||
|
u'Student Pebbles'
|
||||||
|
|
||||||
|
# However, the CommonInfo class cannot be used as a normal model (it doesn't
|
||||||
|
# exist as a model).
|
||||||
|
>>> CommonInfo.objects.all()
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AttributeError: type object 'CommonInfo' has no attribute 'objects'
|
||||||
|
|
||||||
|
# The Place/Restaurant/ItalianRestaurant models, on the other hand, all exist
|
||||||
|
# as independent models. However, the subclasses also have transparent access
|
||||||
|
# to the fields of their ancestors.
|
||||||
|
|
||||||
# Create a couple of Places.
|
# Create a couple of Places.
|
||||||
>>> p1 = Place(name='Master Shakes', address='666 W. Jersey')
|
>>> p1 = Place(name='Master Shakes', address='666 W. Jersey')
|
||||||
@ -43,7 +95,7 @@ not__test__ = {'API_TESTS':"""
|
|||||||
>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
|
>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
|
||||||
>>> p2.save()
|
>>> p2.save()
|
||||||
|
|
||||||
# Test constructor for Restaurant.
|
Test constructor for Restaurant.
|
||||||
>>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False)
|
>>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False)
|
||||||
>>> r.save()
|
>>> r.save()
|
||||||
|
|
||||||
@ -51,5 +103,88 @@ not__test__ = {'API_TESTS':"""
|
|||||||
>>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Elm', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True)
|
>>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Elm', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True)
|
||||||
>>> ir.save()
|
>>> ir.save()
|
||||||
|
|
||||||
|
# Make sure Restaurant and ItalianRestaurant have the right fields in the right
|
||||||
|
# order.
|
||||||
|
>>> [f.name for f in Restaurant._meta.fields]
|
||||||
|
['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza']
|
||||||
|
>>> [f.name for f in ItalianRestaurant._meta.fields]
|
||||||
|
['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza', 'restaurant_ptr', 'serves_gnocchi']
|
||||||
|
|
||||||
|
# Even though p.supplier for a Place 'p' (a parent of a Supplier), a Restaurant
|
||||||
|
# object cannot access that reverse relation, since it's not part of the
|
||||||
|
# Place-Supplier Hierarchy.
|
||||||
|
>>> Place.objects.filter(supplier__name='foo')
|
||||||
|
[]
|
||||||
|
>>> Restaurant.objects.filter(supplier__name='foo')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: Cannot resolve keyword 'supplier' into field. Choices are: address, id, italianrestaurant, lot, name, place_ptr, provider, serves_hot_dogs, serves_pizza
|
||||||
|
|
||||||
|
# Parent fields can be used directly in filters on the child model.
|
||||||
|
>>> Restaurant.objects.filter(name='Demon Dogs')
|
||||||
|
[<Restaurant: Demon Dogs the restaurant>]
|
||||||
|
>>> ItalianRestaurant.objects.filter(address='1234 W. Elm')
|
||||||
|
[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
|
||||||
|
|
||||||
|
# Filters against the parent model return objects of the parent's type.
|
||||||
|
>>> Place.objects.filter(name='Demon Dogs')
|
||||||
|
[<Place: Demon Dogs the place>]
|
||||||
|
|
||||||
|
# Since the parent and child are linked by an automatically created
|
||||||
|
# OneToOneField, you can get from the parent to the child by using the child's
|
||||||
|
# name.
|
||||||
|
>>> place = Place.objects.get(name='Demon Dogs')
|
||||||
|
>>> place.restaurant
|
||||||
|
<Restaurant: Demon Dogs the restaurant>
|
||||||
|
|
||||||
|
>>> Place.objects.get(name='Ristorante Miron').restaurant.italianrestaurant
|
||||||
|
<ItalianRestaurant: Ristorante Miron the italian restaurant>
|
||||||
|
>>> Restaurant.objects.get(name='Ristorante Miron').italianrestaurant
|
||||||
|
<ItalianRestaurant: Ristorante Miron the italian restaurant>
|
||||||
|
|
||||||
|
# This won't work because the Demon Dogs restaurant is not an Italian
|
||||||
|
# restaurant.
|
||||||
|
>>> place.restaurant.italianrestaurant
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
DoesNotExist: ItalianRestaurant matching query does not exist.
|
||||||
|
|
||||||
|
# Related objects work just as they normally do.
|
||||||
|
|
||||||
|
>>> s1 = Supplier(name="Joe's Chickens", address='123 Sesame St')
|
||||||
|
>>> s1.save()
|
||||||
|
>>> s1.customers = [r, ir]
|
||||||
|
>>> s2 = Supplier(name="Luigi's Pasta", address='456 Sesame St')
|
||||||
|
>>> s2.save()
|
||||||
|
>>> s2.customers = [ir]
|
||||||
|
|
||||||
|
# This won't work because the Place we select is not a Restaurant (it's a
|
||||||
|
# Supplier).
|
||||||
|
>>> p = Place.objects.get(name="Joe's Chickens")
|
||||||
|
>>> p.restaurant
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
DoesNotExist: Restaurant matching query does not exist.
|
||||||
|
|
||||||
|
# But we can descend from p to the Supplier child, as expected.
|
||||||
|
>>> p.supplier
|
||||||
|
<Supplier: Joe's Chickens the supplier>
|
||||||
|
|
||||||
|
>>> ir.provider.order_by('-name')
|
||||||
|
[<Supplier: Luigi's Pasta the supplier>, <Supplier: Joe's Chickens the supplier>]
|
||||||
|
|
||||||
|
>>> Restaurant.objects.filter(provider__name__contains="Chickens")
|
||||||
|
[<Restaurant: Demon Dogs the restaurant>, <Restaurant: Ristorante Miron the restaurant>]
|
||||||
|
>>> ItalianRestaurant.objects.filter(provider__name__contains="Chickens")
|
||||||
|
[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
|
||||||
|
|
||||||
|
>>> park1 = ParkingLot(name='Main St', address='111 Main St', main_site=s1)
|
||||||
|
>>> park1.save()
|
||||||
|
>>> park2 = ParkingLot(name='Well Lit', address='124 Sesame St', main_site=ir)
|
||||||
|
>>> park2.save()
|
||||||
|
|
||||||
|
>>> Restaurant.objects.get(lot__name='Well Lit')
|
||||||
|
<Restaurant: Ristorante Miron the restaurant>
|
||||||
|
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user