From b7be3d63e36ee79db51dcabf0f210349e4d6f715 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Fri, 22 Feb 2008 04:58:28 +0000 Subject: [PATCH] queryset-refactor: Added the ability to manually specify a child-parent link. git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7142 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/db/models/base.py | 48 +++++++++++--------- django/db/models/fields/related.py | 14 ++++-- django/db/models/options.py | 22 ++++----- docs/model-api.txt | 37 +++++++++++++++ tests/modeltests/model_inheritance/models.py | 2 + 5 files changed, 85 insertions(+), 38 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index 9a4d6664ab..dc2b4ca0fe 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -59,21 +59,6 @@ class ModelBase(type): if not hasattr(meta, 'get_latest_by'): new_class._meta.get_latest_by = base_meta.get_latest_by - # Do the appropriate setup for any model parents. - abstract_parents = [] - for base in parents: - if not hasattr(base, '_meta'): - # Things without _meta aren't functional models, so they're - # uninteresting parents. - continue - 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. @@ -93,13 +78,32 @@ class ModelBase(type): for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) - for parent in abstract_parents: - names = [f.name for f in new_class._meta.local_fields + new_class._meta.many_to_many] - for field in parent._meta.local_fields: - if field.name in names: - raise TypeError('Local field %r in class %r clashes with field of similar name from abstract base class %r' - % (field.name, name, parent.__name__)) - new_class.add_to_class(field.name, field) + # Do the appropriate setup for any model parents. + o2o_map = dict([(f.rel.to, f) for f in new_class._meta.local_fields + if isinstance(f, OneToOneField)]) + for base in parents: + if not hasattr(base, '_meta'): + # Things without _meta aren't functional models, so they're + # uninteresting parents. + continue + if not base._meta.abstract: + if base in o2o_map: + field = o2o_map[base] + field.primary_key = True + new_class._meta.setup_pk(field) + else: + attr_name = '%s_ptr' % base._meta.module_name + field = OneToOneField(base, name=attr_name, + auto_created=True, parent_link=True) + new_class.add_to_class(attr_name, field) + new_class._meta.parents[base] = field + else: + names = [f.name for f in new_class._meta.local_fields + new_class._meta.many_to_many] + for field in base._meta.local_fields: + if field.name in names: + raise TypeError('Local field %r in class %r clashes with field of similar name from abstract base class %r' + % (field.name, name, base.__name__)) + new_class.add_to_class(field.name, field) if abstract: # Abstract base models can't be instantiated and don't appear in diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 39fe6d794c..4ebf48beab 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -461,8 +461,9 @@ class ReverseManyRelatedObjectsDescriptor(object): class ManyToOneRel(object): def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, - max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False): + max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, + related_name=None, limit_choices_to=None, lookup_overrides=None, + raw_id_admin=False, parent_link=False): try: to._meta except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT @@ -477,6 +478,7 @@ class ManyToOneRel(object): self.lookup_overrides = lookup_overrides or {} self.raw_id_admin = raw_id_admin self.multiple = True + self.parent_link = parent_link def get_related_field(self): """ @@ -489,14 +491,15 @@ class OneToOneRel(ManyToOneRel): def __init__(self, to, field_name, num_in_admin=0, min_num_in_admin=None, max_num_in_admin=None, num_extra_on_change=None, edit_inline=False, related_name=None, limit_choices_to=None, lookup_overrides=None, - raw_id_admin=False): + raw_id_admin=False, parent_link=False): # NOTE: *_num_in_admin and num_extra_on_change are intentionally # ignored here. We accept them as parameters only to match the calling # signature of ManyToOneRel.__init__(). super(OneToOneRel, self).__init__(to, field_name, num_in_admin, edit_inline=edit_inline, related_name=related_name, limit_choices_to=limit_choices_to, - lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin) + lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin, + parent_link=parent_link) self.multiple = False class ManyToManyRel(object): @@ -541,7 +544,8 @@ class ForeignKey(RelatedField, Field): related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), lookup_overrides=kwargs.pop('lookup_overrides', None), - raw_id_admin=kwargs.pop('raw_id_admin', False)) + raw_id_admin=kwargs.pop('raw_id_admin', False), + parent_link=kwargs.pop('parent_link', False)) Field.__init__(self, **kwargs) self.db_index = True diff --git a/django/db/models/options.py b/django/db/models/options.py index a5e853de33..7fa7fe0174 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -113,22 +113,22 @@ class Options(object): # self.many_to_many. if field.rel and isinstance(field.rel, ManyToManyRel): self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field) + if hasattr(self, '_m2m_cache'): + del self._m2m_cache else: self.local_fields.insert(bisect(self.local_fields, field), field) - if not self.pk and field.primary_key: - self.pk = field - field.serialize = False + self.setup_pk(field) + if hasattr(self, '_field_cache'): + del self._field_cache - # 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 setup_pk(self, field): + if not self.pk and field.primary_key: + self.pk = field + field.serialize = False + def __repr__(self): return '' % self.object_name @@ -315,7 +315,7 @@ class Options(object): 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: + if (obj.field.creation_counter < 0 or obj.field.rel.parent_link) and obj.model not in parent_list: continue if not model: cache[obj] = parent diff --git a/docs/model-api.txt b/docs/model-api.txt index 2687c00d14..1926495728 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -1000,6 +1000,21 @@ As with ``ForeignKey``, a relationship to self can be defined by using the string ``"self"`` instead of the model name; references to as-yet undefined models can be made by using a string containing the model name. +Finally, ``OneToOneField`` takes the following extra option: + + ======================= ============================================================ + Argument Description + ======================= ============================================================ + ``parent_link`` When ``True`` and used in a model inherited from + another model, indicates that this field should + be used as the link from the child back to the + parent. See `Model inheritance`_ for more + details. + + **New in Django development version** + + ======================= ============================================================ + **New in Django development version:** ``OneToOneField`` classes used to automatically become the primary key on a model. This is no longer true, although you can manually pass in the ``primary_key`` attribute if you like. @@ -1036,6 +1051,14 @@ Model metadata is "anything that's not a field", such as ordering options, etc. Here's a list of all possible ``Meta`` options. No options are required. Adding ``class Meta`` to a model is completely optional. +``abstract`` +------------ + +**New in Django development version** + +When set to ``True``, denotes this model as an abstract base class. See +`Abstract base classes`_ for more details. Defaults to ``False``. + ``db_table`` ------------ @@ -2192,6 +2215,20 @@ 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. +Specifying the parent link field +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As mentioned, Django will automatically create a ``OneToOneField`` linking +your child class back any non-abstract parent models. If you want to control +the name of the attribute linking back to the parent, you can create your own +link field and pass it ``parent_link=True``. For example, to explicitly +specify the field that will link ``Supplier`` to ``Place`` in the above +example, you could write:: + + class Supplier(Place): + parent = models.OneToOneField(Place, parent_link=True) + ... + Multiple inheritance -------------------- diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index 1187c5fe64..df0fb77ccb 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -67,6 +67,8 @@ class Supplier(Place): return u"%s the supplier" % self.name class ParkingLot(Place): + # An explicit link to the parent (we can control the attribute name). + parent = models.OneToOneField(Place, primary_key=True, parent_link=True) main_site = models.ForeignKey(Place, related_name='lot') def __unicode__(self):