diff --git a/.travis.yml b/.travis.yml index 5553c32..8f3961c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,25 +5,49 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" env: - DJANGO=">=1.4,<1.5" - DJANGO=">=1.5,<1.6" - DJANGO=">=1.6,<1.7" - DJANGO=">=1.7,<1.8" - DJANGO=">=1.8,<1.9" + - DJANGO=">=1.9a1,<1.10" matrix: exclude: + - python: "3.5" + env: DJANGO=">=1.4,<1.5" + - python: "3.5" + env: DJANGO=">=1.5,<1.6" + - python: "3.5" + env: DJANGO=">=1.6,<1.7" + - python: "3.5" + env: DJANGO=">=1.7,<1.8" + - python: "3.5" + env: DJANGO=">=1.8,<1.9" + - python: "3.4" env: DJANGO=">=1.4,<1.5" + - python: "3.4" + env: DJANGO=">=1.9a1,<1.10" + - python: "3.3" env: DJANGO=">=1.4,<1.5" + - python: "3.3" + env: DJANGO=">=1.9a1,<1.10" + - python: "3.2" env: DJANGO=">=1.4,<1.5" + - python: "3.2" + env: DJANGO=">=1.9a1,<1.10" + - python: "2.6" env: DJANGO=">=1.7,<1.8" - python: "2.6" env: DJANGO=">=1.8,<1.9" + - python: "2.6" + env: DJANGO=">=1.9a1,<1.10" install: - pip install django$DJANGO coverage==3.6 diff --git a/AUTHORS.rst b/AUTHORS.rst index 8611135..5e562ce 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -19,6 +19,7 @@ Contributors * Evan Borgstrom * Gavin Wahl * Germán M. Bravo +* Hugo Osvaldo Barrera * Jacob Rief * Jedediah Smith (proxy models support) * John Furr diff --git a/docs/advanced.rst b/docs/advanced.rst index 3b7a2a3..fc4a27e 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -6,7 +6,7 @@ Advanced features In the examples below, these models are being used:: from django.db import models - from polymorphic import PolymorphicModel + from polymorphic.models import PolymorphicModel class ModelA(PolymorphicModel): field1 = models.CharField(max_length=10) diff --git a/docs/managers.rst b/docs/managers.rst index 15db12d..9ed04f7 100644 --- a/docs/managers.rst +++ b/docs/managers.rst @@ -10,9 +10,10 @@ manager class, just derive your manager from ``PolymorphicManager`` instead of ``models.Manager``. As with vanilla Django, in your model class, you should explicitly add the default manager first, and then your custom manager:: - from polymorphic import PolymorphicModel, PolymorphicManager + from polymorphic.models import PolymorphicModel + from polymorphic.manager import PolymorphicManager - class TimeOrderedManager(PolymorphicManager): + class TimeOrderedManager(PolymorphicManager): def get_queryset(self): qs = super(TimeOrderedManager,self).get_queryset() return qs.order_by('-start_date') # order the queryset @@ -41,9 +42,10 @@ base models, as long as these are polymorphic. This means that all managers defined in polymorphic base models continue to work as expected in models inheriting from this base model:: - from polymorphic import PolymorphicModel, PolymorphicManager + from polymorphic.models import PolymorphicModel + from polymorphic.manager import PolymorphicManager - class TimeOrderedManager(PolymorphicManager): + class TimeOrderedManager(PolymorphicManager): def get_queryset(self): qs = super(TimeOrderedManager,self).get_queryset() return qs.order_by('-start_date') # order the queryset @@ -77,7 +79,9 @@ which is the queryset class the manager should use. Just as with vanilla Django, you may define your own custom queryset classes. Just use PolymorphicQuerySet instead of Django's QuerySet as the base class:: - from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet + from polymorphic.models import PolymorphicModel + from polymorphic.manager import PolymorphicManager + from polymorphic.query import PolymorphicQuerySet class MyQuerySet(PolymorphicQuerySet): def my_queryset_method(...): diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 68a2f25..85e7f61 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -19,7 +19,7 @@ Making Your Models Polymorphic Use ``PolymorphicModel`` instead of Django's ``models.Model``, like so:: - from polymorphic import PolymorphicModel + from polymorphic.models import PolymorphicModel class Project(PolymorphicModel): topic = models.CharField(max_length=30) diff --git a/example/example/urls.py b/example/example/urls.py index d070426..5a7d838 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,11 +1,11 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin from django.core.urlresolvers import reverse_lazy from django.views.generic import RedirectView admin.autodiscover() -urlpatterns = patterns('', +urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^$', RedirectView.as_view(url=reverse_lazy('admin:index'), permanent=False)), -) +] diff --git a/example/pexp/admin.py b/example/pexp/admin.py index de9f3c3..5b87b61 100644 --- a/example/pexp/admin.py +++ b/example/pexp/admin.py @@ -42,36 +42,34 @@ class ModelAAdmin(PolymorphicParentModelAdmin): admin.site.register(ModelA, ModelAAdmin) -if 'Model2A' in globals(): - class Model2AChildAdmin(PolymorphicChildModelAdmin): - base_model = Model2A +class Model2AChildAdmin(PolymorphicChildModelAdmin): + base_model = Model2A - class Model2AAdmin(PolymorphicParentModelAdmin): - base_model = Model2A - list_filter = (PolymorphicChildModelFilter,) - child_models = ( - (Model2A, Model2AChildAdmin), - (Model2B, Model2AChildAdmin), - (Model2C, Model2AChildAdmin), - ) +class Model2AAdmin(PolymorphicParentModelAdmin): + base_model = Model2A + list_filter = (PolymorphicChildModelFilter,) + child_models = ( + (Model2A, Model2AChildAdmin), + (Model2B, Model2AChildAdmin), + (Model2C, Model2AChildAdmin), + ) - admin.site.register(Model2A, Model2AAdmin) +admin.site.register(Model2A, Model2AAdmin) -if 'UUIDModelA' in globals(): - class UUIDModelAChildAdmin(PolymorphicChildModelAdmin): - base_model = UUIDModelA +class UUIDModelAChildAdmin(PolymorphicChildModelAdmin): + base_model = UUIDModelA - class UUIDModelAAdmin(PolymorphicParentModelAdmin): - base_model = UUIDModelA - list_filter = (PolymorphicChildModelFilter,) - child_models = ( - (UUIDModelA, UUIDModelAChildAdmin), - (UUIDModelB, UUIDModelAChildAdmin), - (UUIDModelC, UUIDModelAChildAdmin), - ) +class UUIDModelAAdmin(PolymorphicParentModelAdmin): + base_model = UUIDModelA + list_filter = (PolymorphicChildModelFilter,) + child_models = ( + (UUIDModelA, UUIDModelAChildAdmin), + (UUIDModelB, UUIDModelAChildAdmin), + (UUIDModelC, UUIDModelAChildAdmin), + ) - admin.site.register(UUIDModelA, UUIDModelAAdmin) +admin.site.register(UUIDModelA, UUIDModelAAdmin) class ProxyChildAdmin(PolymorphicChildModelAdmin): diff --git a/example/pexp/migrations/0001_initial.py b/example/pexp/migrations/0001_initial.py new file mode 100644 index 0000000..55730d1 --- /dev/null +++ b/example/pexp/migrations/0001_initial.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9b1 on 2015-10-23 22:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import polymorphic.showfields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Model2A', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('field1', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ModelA', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('field1', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), + ), + migrations.CreateModel( + name='nModelA', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('field1', models.CharField(max_length=10)), + ], + ), + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('topic', models.CharField(max_length=30)), + ], + options={ + 'abstract': False, + }, + bases=(polymorphic.showfields.ShowFieldContent, models.Model), + ), + migrations.CreateModel( + name='ProxyBase', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.proxybase_set+', to='contenttypes.ContentType')), + ], + options={ + 'ordering': ('title',), + }, + ), + migrations.CreateModel( + name='UUIDModelA', + fields=[ + ('uuid_primary_key', models.UUIDField(primary_key=True, serialize=False)), + ('field1', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=(polymorphic.showfields.ShowFieldTypeAndContent, models.Model), + ), + migrations.CreateModel( + name='ArtProject', + fields=[ + ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Project')), + ('artist', models.CharField(max_length=30)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.project',), + ), + migrations.CreateModel( + name='Model2B', + fields=[ + ('model2a_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Model2A')), + ('field2', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.model2a',), + ), + migrations.CreateModel( + name='ModelB', + fields=[ + ('modela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.ModelA')), + ('field2', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.modela',), + ), + migrations.CreateModel( + name='nModelB', + fields=[ + ('nmodela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.nModelA')), + ('field2', models.CharField(max_length=10)), + ], + bases=('pexp.nmodela',), + ), + migrations.CreateModel( + name='ResearchProject', + fields=[ + ('project_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Project')), + ('supervisor', models.CharField(max_length=30)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.project',), + ), + migrations.CreateModel( + name='UUIDModelB', + fields=[ + ('uuidmodela_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.UUIDModelA')), + ('field2', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.uuidmodela',), + ), + migrations.AddField( + model_name='uuidmodela', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.uuidmodela_set+', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='project', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.project_set+', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='modela', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.modela_set+', to='contenttypes.ContentType'), + ), + migrations.AddField( + model_name='model2a', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_pexp.model2a_set+', to='contenttypes.ContentType'), + ), + migrations.CreateModel( + name='ProxyA', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('pexp.proxybase',), + ), + migrations.CreateModel( + name='ProxyB', + fields=[ + ], + options={ + 'proxy': True, + }, + bases=('pexp.proxybase',), + ), + migrations.CreateModel( + name='Model2C', + fields=[ + ('model2b_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.Model2B')), + ('field3', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.model2b',), + ), + migrations.CreateModel( + name='ModelC', + fields=[ + ('modelb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.ModelB')), + ('field3', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.modelb',), + ), + migrations.CreateModel( + name='nModelC', + fields=[ + ('nmodelb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.nModelB')), + ('field3', models.CharField(max_length=10)), + ], + bases=('pexp.nmodelb',), + ), + migrations.CreateModel( + name='UUIDModelC', + fields=[ + ('uuidmodelb_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='pexp.UUIDModelB')), + ('field3', models.CharField(max_length=10)), + ], + options={ + 'abstract': False, + }, + bases=('pexp.uuidmodelb',), + ), + ] diff --git a/example/pexp/migrations/0002_modelc_field4.py b/example/pexp/migrations/0002_modelc_field4.py new file mode 100644 index 0000000..c2eb6bb --- /dev/null +++ b/example/pexp/migrations/0002_modelc_field4.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9b1 on 2015-10-24 01:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pexp', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='modelc', + name='field4', + field=models.ManyToManyField(related_name='related_c', to='pexp.ModelB'), + ), + ] diff --git a/example/pexp/migrations/__init__.py b/example/pexp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/pexp/models.py b/example/pexp/models.py index b0ed8c5..486900e 100644 --- a/example/pexp/models.py +++ b/example/pexp/models.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- +import django from django.db import models -from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet +from polymorphic.models import PolymorphicModel from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent class Project(ShowFieldContent, PolymorphicModel): @@ -18,6 +19,7 @@ class ModelB(ModelA): field2 = models.CharField(max_length=10) class ModelC(ModelB): field3 = models.CharField(max_length=10) + field4 = models.ManyToManyField(ModelB, related_name='related_c') class nModelA(models.Model): field1 = models.CharField(max_length=10) @@ -26,27 +28,25 @@ class nModelB(nModelA): class nModelC(nModelB): field3 = models.CharField(max_length=10) -# for Django 1.2+, test models with same names in different apps -# (the other models with identical names are in polymorphic/tests.py) -from django import VERSION as django_VERSION -if not (django_VERSION[0]<=1 and django_VERSION[1]<=1): - class Model2A(PolymorphicModel): - field1 = models.CharField(max_length=10) - class Model2B(Model2A): - field2 = models.CharField(max_length=10) - class Model2C(Model2B): - field3 = models.CharField(max_length=10) +class Model2A(PolymorphicModel): + field1 = models.CharField(max_length=10) +class Model2B(Model2A): + field2 = models.CharField(max_length=10) +class Model2C(Model2B): + field3 = models.CharField(max_length=10) -try: from polymorphic.test_tools import UUIDField -except: pass -if 'UUIDField' in globals(): - class UUIDModelA(ShowFieldTypeAndContent, PolymorphicModel): - uuid_primary_key = UUIDField(primary_key = True) - field1 = models.CharField(max_length=10) - class UUIDModelB(UUIDModelA): - field2 = models.CharField(max_length=10) - class UUIDModelC(UUIDModelB): - field3 = models.CharField(max_length=10) +if django.VERSION < (1,8): + from polymorphic.tools_for_tests import UUIDField +else: + from django.db.models import UUIDField + +class UUIDModelA(ShowFieldTypeAndContent, PolymorphicModel): + uuid_primary_key = UUIDField(primary_key = True) + field1 = models.CharField(max_length=10) +class UUIDModelB(UUIDModelA): + field2 = models.CharField(max_length=10) +class UUIDModelC(UUIDModelB): + field3 = models.CharField(max_length=10) class ProxyBase(PolymorphicModel): title = models.CharField(max_length=200) diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index c598b84..412e5d1 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -8,10 +8,6 @@ Please see LICENSE and AUTHORS for more information. """ from __future__ import absolute_import import django -from .polymorphic_model import PolymorphicModel -from .manager import PolymorphicManager -from .query import PolymorphicQuerySet -from .query_translate import translate_polymorphic_Q_object from .showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent from .showfields import ShowFields, ShowFieldTypes, ShowFieldsAndTypes # import old names for compatibility diff --git a/polymorphic/admin.py b/polymorphic/admin.py index 216f74f..1adba55 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -3,7 +3,7 @@ ModelAdmin code to display polymorphic models. """ import sys from django import forms -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib import admin from django.contrib.admin.helpers import AdminForm, AdminErrorList from django.contrib.admin.widgets import AdminRadioSelect @@ -18,6 +18,7 @@ from django.utils.encoding import force_text from django.utils.http import urlencode from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ +import django try: @@ -268,7 +269,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): def change_view(self, request, object_id, *args, **kwargs): """Redirect the change view to the real admin.""" - # between Django 1.3 and 1.4 this method signature differs. Hence the *args, **kwargs real_admin = self._get_real_admin(object_id) return real_admin.change_view(request, object_id, *args, **kwargs) @@ -296,17 +296,28 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): urls = super(PolymorphicParentModelAdmin, self).get_urls() info = _get_opt(self.model) - # Patch the change URL so it's not a big catch-all; allowing all custom URLs to be added to the end. - # The url needs to be recreated, patching url.regex is not an option Django 1.4's LocaleRegexProvider changed it. - new_change_url = url(r'^{0}/$'.format(self.pk_regex), self.admin_site.admin_view(self.change_view), name='{0}_{1}_change'.format(*info)) - for i, oldurl in enumerate(urls): - if oldurl.name == new_change_url.name: - urls[i] = new_change_url + # Patch the change view URL so it's not a big catch-all; allowing all + # custom URLs to be added to the end. This is done by adding '/$' to the + # end of the regex. The url needs to be recreated, patching url.regex + # is not an option Django 1.4's LocaleRegexProvider changed it. + if django.VERSION < (1, 9): + # On Django 1.9, the change view URL has been changed from + # //// to ////change/, which is + # why we can skip this workaround for Django >= 1.9. + new_change_url = url( + r'^{0}/$'.format(self.pk_regex), + self.admin_site.admin_view(self.change_view), + name='{0}_{1}_change'.format(*info) + ) + + for i, oldurl in enumerate(urls): + if oldurl.name == new_change_url.name: + urls[i] = new_change_url # Define the catch-all for custom views - custom_urls = patterns('', + custom_urls = [ url(r'^(?P.+)$', self.admin_site.admin_view(self.subclass_view)) - ) + ] # At this point. all admin code needs to be known. self._lazy_setup() @@ -383,7 +394,8 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): context = { 'title': _('Add %s') % force_text(opts.verbose_name), 'adminform': adminForm, - 'is_popup': "_popup" in request.REQUEST, + 'is_popup': ("_popup" in request.POST or + "_popup" in request.GET), 'media': mark_safe(media), 'errors': AdminErrorList(form, ()), 'app_label': opts.app_label, @@ -465,7 +477,7 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): kwargs.setdefault('form', self.base_form or self.form) # prevent infinite recursion in django 1.6+ - if not self.declared_fieldsets: + if not getattr(self, 'declared_fieldsets', None): kwargs.setdefault('fields', None) return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) @@ -529,7 +541,8 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): def get_fieldsets(self, request, obj=None): # If subclass declares fieldsets, this is respected - if self.declared_fieldsets or not self.base_fieldsets: + if (hasattr(self, 'declared_fieldset') and self.declared_fieldsets) \ + or not self.base_fieldsets: return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj) # Have a reasonable default fieldsets, diff --git a/polymorphic/models.py b/polymorphic/models.py index 69652f9..cbfdef9 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -1,10 +1,229 @@ # -*- coding: utf-8 -*- """ -IMPORTANT: +Seamless Polymorphic Inheritance for Django Models +================================================== -The models.py module is not used anymore. -Please use the following import method in your apps: +Please see README.rst and DOCS.rst for further information. - from polymorphic import PolymorphicModel, ... +Or on the Web: +http://chrisglass.github.com/django_polymorphic/ +http://github.com/chrisglass/django_polymorphic +Copyright: +This code and affiliated files are (C) by Bert Constantin and individual contributors. +Please see LICENSE and AUTHORS for more information. """ +from __future__ import absolute_import + +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.utils import six + +from .base import PolymorphicModelBase +from .manager import PolymorphicManager +from .query_translate import translate_polymorphic_Q_object + +################################################################################### +### PolymorphicModel + +class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): + """ + Abstract base class that provides polymorphic behaviour + for any model directly or indirectly derived from it. + + For usage instructions & examples please see documentation. + + PolymorphicModel declares one field for internal use (polymorphic_ctype) + and provides a polymorphic manager as the default manager + (and as 'objects'). + + PolymorphicModel overrides the save() and __init__ methods. + + If your derived class overrides any of these methods as well, then you need + to take care that you correctly call the method of the superclass, like: + + super(YourClass,self).save(*args,**kwargs) + """ + + # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing) + polymorphic_model_marker = True + + # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery + polymorphic_query_multiline_output = False + + class Meta: + abstract = True + + # avoid ContentType related field accessor clash (an error emitted by model validation) + polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False, + related_name='polymorphic_%(app_label)s.%(class)s_set+') + + # some applications want to know the name of the fields that are added to its models + polymorphic_internal_model_fields = ['polymorphic_ctype'] + + # Note that Django 1.5 removes these managers because the model is abstract. + # They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers() + objects = PolymorphicManager() + base_objects = models.Manager() + + @classmethod + def translate_polymorphic_Q_object(self_class, q): + return translate_polymorphic_Q_object(self_class, q) + + def pre_save_polymorphic(self): + """Normally not needed. + This function may be called manually in special use-cases. When the object + is saved for the first time, we store its real class in polymorphic_ctype. + When the object later is retrieved by PolymorphicQuerySet, it uses this + field to figure out the real class of this object + (used by PolymorphicQuerySet._get_real_instances) + """ + if not self.polymorphic_ctype_id: + self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False) + pre_save_polymorphic.alters_data = True + + def save(self, *args, **kwargs): + """Overridden model save function which supports the polymorphism + functionality (through pre_save_polymorphic).""" + self.pre_save_polymorphic() + return super(PolymorphicModel, self).save(*args, **kwargs) + save.alters_data = True + + def get_real_instance_class(self): + """ + Normally not needed. + If a non-polymorphic manager (like base_objects) has been used to + retrieve objects, then the real class/type of these objects may be + determined using this method. + """ + # the following line would be the easiest way to do this, but it produces sql queries + # return self.polymorphic_ctype.model_class() + # so we use the following version, which uses the ContentType manager cache. + # Note that model_class() can return None for stale content types; + # when the content type record still exists but no longer refers to an existing model. + try: + model = ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class() + except AttributeError: + # Django <1.6 workaround + return None + + # Protect against bad imports (dumpdata without --natural) or other + # issues missing with the ContentType models. + if model is not None \ + and not issubclass(model, self.__class__) \ + and not issubclass(model, self.__class__._meta.proxy_for_model): + raise RuntimeError("ContentType {0} for {1} #{2} does not point to a subclass!".format( + self.polymorphic_ctype_id, model, self.pk, + )) + return model + + def get_real_concrete_instance_class_id(self): + model_class = self.get_real_instance_class() + if model_class is None: + return None + return ContentType.objects.get_for_model(model_class, for_concrete_model=True).pk + + def get_real_concrete_instance_class(self): + model_class = self.get_real_instance_class() + if model_class is None: + return None + return ContentType.objects.get_for_model(model_class, for_concrete_model=True).model_class() + + def get_real_instance(self): + """Normally not needed. + If a non-polymorphic manager (like base_objects) has been used to + retrieve objects, then the complete object with it's real class/type + and all fields may be retrieved with this method. + Each method call executes one db query (if necessary).""" + real_model = self.get_real_instance_class() + if real_model == self.__class__: + return self + return real_model.objects.get(pk=self.pk) + + def __init__(self, * args, ** kwargs): + """Replace Django's inheritance accessor member functions for our model + (self.__class__) with our own versions. + We monkey patch them until a patch can be added to Django + (which would probably be very small and make all of this obsolete). + + If we have inheritance of the form ModelA -> ModelB ->ModelC then + Django creates accessors like this: + - ModelA: modelb + - ModelB: modela_ptr, modelb, modelc + - ModelC: modela_ptr, modelb, modelb_ptr, modelc + + These accessors allow Django (and everyone else) to travel up and down + the inheritance tree for the db object at hand. + + The original Django accessors use our polymorphic manager. + But they should not. So we replace them with our own accessors that use + our appropriate base_objects manager. + """ + super(PolymorphicModel, self).__init__(*args, ** kwargs) + + if self.__class__.polymorphic_super_sub_accessors_replaced: + return + self.__class__.polymorphic_super_sub_accessors_replaced = True + + def create_accessor_function_for_model(model, accessor_name): + def accessor_function(self): + attr = model.base_objects.get(pk=self.pk) + return attr + return accessor_function + + subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models() + + try: + from django.db.models.fields.related import ReverseOneToOneDescriptor, ForwardManyToOneDescriptor + except ImportError: + # django < 1.9 + from django.db.models.fields.related import ( + SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor, + ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor, + ) + for name, model in subclasses_and_superclasses_accessors.items(): + orig_accessor = getattr(self.__class__, name, None) + if type(orig_accessor) in [ReverseOneToOneDescriptor, ForwardManyToOneDescriptor]: + #print >>sys.stderr, '---------- replacing', name, orig_accessor, '->', model + setattr(self.__class__, name, property(create_accessor_function_for_model(model, name))) + + def _get_inheritance_relation_fields_and_models(self): + """helper function for __init__: + determine names of all Django inheritance accessor member functions for type(self)""" + + def add_model(model, field_name, result): + result[field_name] = model + + def add_model_if_regular(model, field_name, result): + if (issubclass(model, models.Model) + and model != models.Model + and model != self.__class__ + and model != PolymorphicModel): + add_model(model, field_name, result) + + def add_all_super_models(model, result): + for super_cls, field_to_super in model._meta.parents.items(): + if field_to_super is not None: #if not a link to a proxy model + field_name = field_to_super.name #the field on model can have a different name to super_cls._meta.module_name, if the field is created manually using 'parent_link' + add_model_if_regular(super_cls, field_name, result) + add_all_super_models(super_cls, result) + + def add_all_sub_models(super_cls, result): + for sub_cls in super_cls.__subclasses__(): #go through all subclasses of model + if super_cls in sub_cls._meta.parents: #super_cls may not be in sub_cls._meta.parents if super_cls is a proxy model + field_to_super = sub_cls._meta.parents[super_cls] #get the field that links sub_cls to super_cls + if field_to_super is not None: # if filed_to_super is not a link to a proxy model + super_to_sub_related_field = field_to_super.rel + if super_to_sub_related_field.related_name is None: + #if related name is None the related field is the name of the subclass + to_subclass_fieldname = sub_cls.__name__.lower() + else: + #otherwise use the given related name + to_subclass_fieldname = super_to_sub_related_field.related_name + + add_model_if_regular(sub_cls, to_subclass_fieldname, result) + + result = {} + add_all_super_models(self.__class__, result) + add_all_sub_models(self.__class__, result) + return result diff --git a/polymorphic/polymorphic_model.py b/polymorphic/polymorphic_model.py deleted file mode 100644 index cbfdef9..0000000 --- a/polymorphic/polymorphic_model.py +++ /dev/null @@ -1,229 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Seamless Polymorphic Inheritance for Django Models -================================================== - -Please see README.rst and DOCS.rst for further information. - -Or on the Web: -http://chrisglass.github.com/django_polymorphic/ -http://github.com/chrisglass/django_polymorphic - -Copyright: -This code and affiliated files are (C) by Bert Constantin and individual contributors. -Please see LICENSE and AUTHORS for more information. -""" -from __future__ import absolute_import - -from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.utils import six - -from .base import PolymorphicModelBase -from .manager import PolymorphicManager -from .query_translate import translate_polymorphic_Q_object - -################################################################################### -### PolymorphicModel - -class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): - """ - Abstract base class that provides polymorphic behaviour - for any model directly or indirectly derived from it. - - For usage instructions & examples please see documentation. - - PolymorphicModel declares one field for internal use (polymorphic_ctype) - and provides a polymorphic manager as the default manager - (and as 'objects'). - - PolymorphicModel overrides the save() and __init__ methods. - - If your derived class overrides any of these methods as well, then you need - to take care that you correctly call the method of the superclass, like: - - super(YourClass,self).save(*args,**kwargs) - """ - - # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing) - polymorphic_model_marker = True - - # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery - polymorphic_query_multiline_output = False - - class Meta: - abstract = True - - # avoid ContentType related field accessor clash (an error emitted by model validation) - polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False, - related_name='polymorphic_%(app_label)s.%(class)s_set+') - - # some applications want to know the name of the fields that are added to its models - polymorphic_internal_model_fields = ['polymorphic_ctype'] - - # Note that Django 1.5 removes these managers because the model is abstract. - # They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers() - objects = PolymorphicManager() - base_objects = models.Manager() - - @classmethod - def translate_polymorphic_Q_object(self_class, q): - return translate_polymorphic_Q_object(self_class, q) - - def pre_save_polymorphic(self): - """Normally not needed. - This function may be called manually in special use-cases. When the object - is saved for the first time, we store its real class in polymorphic_ctype. - When the object later is retrieved by PolymorphicQuerySet, it uses this - field to figure out the real class of this object - (used by PolymorphicQuerySet._get_real_instances) - """ - if not self.polymorphic_ctype_id: - self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False) - pre_save_polymorphic.alters_data = True - - def save(self, *args, **kwargs): - """Overridden model save function which supports the polymorphism - functionality (through pre_save_polymorphic).""" - self.pre_save_polymorphic() - return super(PolymorphicModel, self).save(*args, **kwargs) - save.alters_data = True - - def get_real_instance_class(self): - """ - Normally not needed. - If a non-polymorphic manager (like base_objects) has been used to - retrieve objects, then the real class/type of these objects may be - determined using this method. - """ - # the following line would be the easiest way to do this, but it produces sql queries - # return self.polymorphic_ctype.model_class() - # so we use the following version, which uses the ContentType manager cache. - # Note that model_class() can return None for stale content types; - # when the content type record still exists but no longer refers to an existing model. - try: - model = ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class() - except AttributeError: - # Django <1.6 workaround - return None - - # Protect against bad imports (dumpdata without --natural) or other - # issues missing with the ContentType models. - if model is not None \ - and not issubclass(model, self.__class__) \ - and not issubclass(model, self.__class__._meta.proxy_for_model): - raise RuntimeError("ContentType {0} for {1} #{2} does not point to a subclass!".format( - self.polymorphic_ctype_id, model, self.pk, - )) - return model - - def get_real_concrete_instance_class_id(self): - model_class = self.get_real_instance_class() - if model_class is None: - return None - return ContentType.objects.get_for_model(model_class, for_concrete_model=True).pk - - def get_real_concrete_instance_class(self): - model_class = self.get_real_instance_class() - if model_class is None: - return None - return ContentType.objects.get_for_model(model_class, for_concrete_model=True).model_class() - - def get_real_instance(self): - """Normally not needed. - If a non-polymorphic manager (like base_objects) has been used to - retrieve objects, then the complete object with it's real class/type - and all fields may be retrieved with this method. - Each method call executes one db query (if necessary).""" - real_model = self.get_real_instance_class() - if real_model == self.__class__: - return self - return real_model.objects.get(pk=self.pk) - - def __init__(self, * args, ** kwargs): - """Replace Django's inheritance accessor member functions for our model - (self.__class__) with our own versions. - We monkey patch them until a patch can be added to Django - (which would probably be very small and make all of this obsolete). - - If we have inheritance of the form ModelA -> ModelB ->ModelC then - Django creates accessors like this: - - ModelA: modelb - - ModelB: modela_ptr, modelb, modelc - - ModelC: modela_ptr, modelb, modelb_ptr, modelc - - These accessors allow Django (and everyone else) to travel up and down - the inheritance tree for the db object at hand. - - The original Django accessors use our polymorphic manager. - But they should not. So we replace them with our own accessors that use - our appropriate base_objects manager. - """ - super(PolymorphicModel, self).__init__(*args, ** kwargs) - - if self.__class__.polymorphic_super_sub_accessors_replaced: - return - self.__class__.polymorphic_super_sub_accessors_replaced = True - - def create_accessor_function_for_model(model, accessor_name): - def accessor_function(self): - attr = model.base_objects.get(pk=self.pk) - return attr - return accessor_function - - subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models() - - try: - from django.db.models.fields.related import ReverseOneToOneDescriptor, ForwardManyToOneDescriptor - except ImportError: - # django < 1.9 - from django.db.models.fields.related import ( - SingleRelatedObjectDescriptor as ReverseOneToOneDescriptor, - ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor, - ) - for name, model in subclasses_and_superclasses_accessors.items(): - orig_accessor = getattr(self.__class__, name, None) - if type(orig_accessor) in [ReverseOneToOneDescriptor, ForwardManyToOneDescriptor]: - #print >>sys.stderr, '---------- replacing', name, orig_accessor, '->', model - setattr(self.__class__, name, property(create_accessor_function_for_model(model, name))) - - def _get_inheritance_relation_fields_and_models(self): - """helper function for __init__: - determine names of all Django inheritance accessor member functions for type(self)""" - - def add_model(model, field_name, result): - result[field_name] = model - - def add_model_if_regular(model, field_name, result): - if (issubclass(model, models.Model) - and model != models.Model - and model != self.__class__ - and model != PolymorphicModel): - add_model(model, field_name, result) - - def add_all_super_models(model, result): - for super_cls, field_to_super in model._meta.parents.items(): - if field_to_super is not None: #if not a link to a proxy model - field_name = field_to_super.name #the field on model can have a different name to super_cls._meta.module_name, if the field is created manually using 'parent_link' - add_model_if_regular(super_cls, field_name, result) - add_all_super_models(super_cls, result) - - def add_all_sub_models(super_cls, result): - for sub_cls in super_cls.__subclasses__(): #go through all subclasses of model - if super_cls in sub_cls._meta.parents: #super_cls may not be in sub_cls._meta.parents if super_cls is a proxy model - field_to_super = sub_cls._meta.parents[super_cls] #get the field that links sub_cls to super_cls - if field_to_super is not None: # if filed_to_super is not a link to a proxy model - super_to_sub_related_field = field_to_super.rel - if super_to_sub_related_field.related_name is None: - #if related name is None the related field is the name of the subclass - to_subclass_fieldname = sub_cls.__name__.lower() - else: - #otherwise use the given related name - to_subclass_fieldname = super_to_sub_related_field.related_name - - add_model_if_regular(sub_cls, to_subclass_fieldname, result) - - result = {} - add_all_super_models(self.__class__, result) - add_all_sub_models(self.__class__, result) - return result diff --git a/polymorphic/query.py b/polymorphic/query.py index 3ecaea5..2d6306e 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -43,6 +43,14 @@ def transmogrify(cls, obj): ################################################################################### ### PolymorphicQuerySet +def _query_annotations(query): + try: + return query.annotations + except AttributeError: + # Django < 1.8 + return query.aggregates + + class PolymorphicQuerySet(QuerySet): """ QuerySet for PolymorphicModel @@ -137,7 +145,18 @@ class PolymorphicQuerySet(QuerySet): qs = self.non_polymorphic() return super(PolymorphicQuerySet, qs).aggregate(*args, **kwargs) - # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.^ + if django.VERSION >= (1, 9): + # On Django < 1.9, 'qs.values(...)' returned a new special ValuesQuerySet + # object, which our polymorphic modifications didn't apply to. + # Starting with Django 1.9, the copy returned by 'qs.values(...)' has the + # same class as 'qs', so our polymorphic modifications would apply. + # We want to leave values queries untouched, so we set 'polymorphic_disabled'. + def _values(self, *args, **kwargs): + clone = super(PolymorphicQuerySet, self)._values(*args, **kwargs) + clone.polymorphic_disabled = True + return clone + + # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results. # The resulting objects are required to have a unique primary key within the result set # (otherwise an error is thrown). # The "polymorphic" keyword argument is not supported anymore. @@ -197,7 +216,7 @@ class PolymorphicQuerySet(QuerySet): for base_object in base_result_objects: ordered_id_list.append(base_object.pk) - # check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...) + # check if id of the result object occurres more than once - this can happen e.g. with base_objects.extra(tables=...) if not base_object.pk in base_result_objects_by_id: base_result_objects_by_id[base_object.pk] = base_object @@ -239,8 +258,8 @@ class PolymorphicQuerySet(QuerySet): if real_class != real_concrete_class: real_object = transmogrify(real_class, real_object) - if self.query.aggregates: - for anno_field_name in six.iterkeys(self.query.aggregates): + if _query_annotations(self.query): + for anno_field_name in six.iterkeys(_query_annotations(self.query)): attr = getattr(base_result_objects_by_id[o_pk], anno_field_name) setattr(real_object, anno_field_name, attr) @@ -255,8 +274,8 @@ class PolymorphicQuerySet(QuerySet): resultlist = [results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results] # set polymorphic_annotate_names in all objects (currently just used for debugging/printing) - if self.query.aggregates: - annotate_names = list(six.iterkeys(self.query.aggregates)) # get annotate field list + if _query_annotations(self.query): + annotate_names = list(six.iterkeys(_query_annotations(self.query))) # get annotate field list for real_object in resultlist: real_object.polymorphic_annotate_names = annotate_names @@ -289,7 +308,7 @@ class PolymorphicQuerySet(QuerySet): if self.polymorphic_disabled: for o in base_iter: yield o - raise StopIteration + return while True: base_result_objects = [] @@ -309,7 +328,7 @@ class PolymorphicQuerySet(QuerySet): yield o if reached_end: - raise StopIteration + return def __repr__(self, *args, **kwargs): if self.model.polymorphic_query_multiline_output: diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index 4008e61..20d823d 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -162,7 +162,7 @@ def translate_polymorphic_field_path(queryset_model, field_path): # so no tripple ClassName___field was intended. try: # rel = (field_object, model, direct, m2m) - field = queryset_model._meta.get_field_by_name(classname)[0] + field = queryset_model._meta.get_field(classname) if isinstance(field, RelatedObject): # Can also test whether the field exists in the related object to avoid ambiguity between # class names and field names, but that never happens when your class names are in CamelCase. @@ -234,7 +234,7 @@ def _create_model_filter_Q(modellist, not_instance_of=False): if not modellist: return None - from .polymorphic_model import PolymorphicModel + from .models import PolymorphicModel if type(modellist) != list and type(modellist) != tuple: if issubclass(modellist, PolymorphicModel): diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 14def4d..d4abb8c 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -6,7 +6,11 @@ from __future__ import print_function import uuid import re import django -from django.utils.unittest import skipIf +try: + from unittest import skipIf +except ImportError: + # python<2.7 + from django.utils.unittest import skipIf from django.db.models.query import QuerySet from django.test import TestCase @@ -15,9 +19,15 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.utils import six -from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet +from polymorphic.models import PolymorphicModel +from polymorphic.manager import PolymorphicManager +from polymorphic.query import PolymorphicQuerySet from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent -from polymorphic.tools_for_tests import UUIDField +try: + from django.db.models import UUIDField +except ImportError: + # django<1.8 + from polymorphic.tools_for_tests import UUIDField class PlainA(models.Model): @@ -206,15 +216,15 @@ class Bottom(Middle): author = models.CharField(max_length=50) class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel): - uuid_primary_key = UUIDField(primary_key = True) - topic = models.CharField(max_length = 30) + uuid_primary_key = UUIDField(primary_key = True, default=uuid.uuid1) + topic = models.CharField(max_length = 30) class UUIDArtProject(UUIDProject): - artist = models.CharField(max_length = 30) + artist = models.CharField(max_length = 30) class UUIDResearchProject(UUIDProject): - supervisor = models.CharField(max_length = 30) + supervisor = models.CharField(max_length = 30) class UUIDPlainA(models.Model): - uuid_primary_key = UUIDField(primary_key = True) + uuid_primary_key = UUIDField(primary_key = True, default=uuid.uuid1) field1 = models.CharField(max_length=10) class UUIDPlainB(UUIDPlainA): field2 = models.CharField(max_length=10) diff --git a/polymorphic/tools_for_tests.py b/polymorphic/tools_for_tests.py index b7c017d..0f2e034 100644 --- a/polymorphic/tools_for_tests.py +++ b/polymorphic/tools_for_tests.py @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- - -#################################################################### - +# Compatibility module for Django < 1.8 import uuid from django import forms @@ -9,6 +6,7 @@ from django.db import models from django.utils.encoding import smart_text from django.utils import six + class UUIDVersionError(Exception): pass diff --git a/runtests.py b/runtests.py index 6120a17..545a014 100755 --- a/runtests.py +++ b/runtests.py @@ -30,8 +30,12 @@ if not settings.configured: TEMPLATE_LOADERS = ( 'django.template.loaders.app_directories.Loader', ), - TEMPLATE_CONTEXT_PROCESSORS = default_settings.TEMPLATE_CONTEXT_PROCESSORS + ( - 'django.core.context_processors.request', + TEMPLATE_CONTEXT_PROCESSORS=( + # list() is only needed for older versions of django where this is + # a tuple: + list(default_settings.TEMPLATE_CONTEXT_PROCESSORS) + [ + 'django.core.context_processors.request', + ] ), TEST_RUNNER = 'django.test.runner.DiscoverRunner' if django.VERSION >= (1,7) else 'django.test.simple.DjangoTestSuiteRunner', INSTALLED_APPS = ( diff --git a/tox.ini b/tox.ini index 5a551f2..19774de 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist= py32-django{15,16,17,18}, py33-django{15,16,17,18}, py34-django{15,16,17,18}, + py35-django18 # py33-django-dev, docs,