From 7b4939bb3f3d66d4a76460957673870ac7dae9dd Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 14:49:03 +0100 Subject: [PATCH 01/17] tox.ini: add spaces --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 2ebdb2f..2a62813 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,6 @@ commands= python runtests.py [testenv:docs] -changedir=docs deps=Sphinx -commands=sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html +changedir = docs +commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html From f656ec438ee13927a054b44b31ddc3577894f50d Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 14:50:37 +0100 Subject: [PATCH 02/17] docs: add showfields import change --- docs/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index fc4a27e..dbc7f82 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -202,7 +202,7 @@ Nicely Displaying Polymorphic Querysets In order to get the output as seen in all examples here, you need to use the :class:`~polymorphic.showfields.ShowFieldType` class mixin:: - from polymorphic import PolymorphicModel, ShowFieldType + from polymorphic.showfields import PolymorphicModel, ShowFieldType class ModelA(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=10) From aaf06c71a5b1dc9a35c546729128dad4187e03ef Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 14:53:32 +0100 Subject: [PATCH 03/17] Fix PEP8 whitespace issues autopep8 -r polymorphic/ example/ -i --select=E112,E113,E115,E116,E122,E123,E125,E127,E128,E201,E202,E203,E211,E225,E226,E227,E228,E231,E251,E261,E262,E271,E272,E273,E274,E301,E302,E303,E304,E309,E711,E713,W291,W293,W391 --exclude migrations,south_migrations (line conditiation fixes: E123,E125,E122,E127,E128) --- example/example/settings.py | 2 +- example/pexp/admin.py | 6 +- example/pexp/management/commands/p2cmd.py | 58 +++--- example/pexp/management/commands/pcmd.py | 20 +- example/pexp/management/commands/polybench.py | 70 ++++--- .../commands/polymorphic_create_test_data.py | 18 +- example/pexp/models.py | 34 +++- polymorphic/__init__.py | 7 +- polymorphic/admin.py | 27 +-- polymorphic/base.py | 8 +- polymorphic/models.py | 29 +-- polymorphic/query.py | 9 +- polymorphic/query_translate.py | 2 +- polymorphic/showfields.py | 5 +- .../templatetags/polymorphic_admin_tags.py | 2 +- polymorphic/tests.py | 183 +++++++++++++----- polymorphic/tools_for_tests.py | 4 +- 17 files changed, 304 insertions(+), 180 deletions(-) diff --git a/example/example/settings.py b/example/example/settings.py index 9458f25..9e33a05 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -75,7 +75,7 @@ INSTALLED_APPS = ( 'pexp', # this Django app is for testing and experimentation; not needed otherwise ) -if django.VERSION >= (1,7): +if django.VERSION >= (1, 7): TEST_RUNNER = 'django.test.runner.DiscoverRunner' # silence system checks # Logging configuration diff --git a/example/pexp/admin.py b/example/pexp/admin.py index 5b87b61..b7dd8f9 100644 --- a/example/pexp/admin.py +++ b/example/pexp/admin.py @@ -14,6 +14,7 @@ class ProjectChildAdmin(PolymorphicChildModelAdmin): }), ) + class ProjectAdmin(PolymorphicParentModelAdmin): base_model = Project list_filter = (PolymorphicChildModelFilter,) @@ -26,10 +27,10 @@ class ProjectAdmin(PolymorphicParentModelAdmin): admin.site.register(Project, ProjectAdmin) - class ModelAChildAdmin(PolymorphicChildModelAdmin): base_model = ModelA + class ModelAAdmin(PolymorphicParentModelAdmin): base_model = ModelA list_filter = (PolymorphicChildModelFilter,) @@ -45,6 +46,7 @@ admin.site.register(ModelA, ModelAAdmin) class Model2AChildAdmin(PolymorphicChildModelAdmin): base_model = Model2A + class Model2AAdmin(PolymorphicParentModelAdmin): base_model = Model2A list_filter = (PolymorphicChildModelFilter,) @@ -60,6 +62,7 @@ admin.site.register(Model2A, Model2AAdmin) class UUIDModelAChildAdmin(PolymorphicChildModelAdmin): base_model = UUIDModelA + class UUIDModelAAdmin(PolymorphicParentModelAdmin): base_model = UUIDModelA list_filter = (PolymorphicChildModelFilter,) @@ -75,6 +78,7 @@ admin.site.register(UUIDModelA, UUIDModelAAdmin) class ProxyChildAdmin(PolymorphicChildModelAdmin): base_model = ProxyBase + class ProxyAdmin(PolymorphicParentModelAdmin): base_model = ProxyBase list_filter = (PolymorphicChildModelFilter,) diff --git a/example/pexp/management/commands/p2cmd.py b/example/pexp/management/commands/p2cmd.py index 1107370..126eacd 100644 --- a/example/pexp/management/commands/p2cmd.py +++ b/example/pexp/management/commands/p2cmd.py @@ -8,46 +8,50 @@ import uuid from django.core.management.base import NoArgsCommand from django.db.models import connection from pprint import pprint -import time,sys +import time, sys from pexp.models import * + def reset_queries(): - connection.queries=[] + connection.queries = [] + def show_queries(): - print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; connection.queries=[] + print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; connection.queries = [] + def print_timing(func, message='', iterations=1): def wrapper(*arg): - results=[] + results = [] reset_queries() for i in xrange(iterations): t1 = time.time() x = func(*arg) t2 = time.time() - results.append((t2-t1)*1000.0) - res_sum=0 - for r in results: res_sum +=r + results.append((t2 - t1) * 1000.0) + res_sum = 0 + for r in results: res_sum += r median = res_sum / len(results) print '%s%-19s: %.4f ms, %i queries (%i times)' % ( - message,func.func_name, + message, func.func_name, res_sum, len(connection.queries), iterations - ) + ) sys.stdout.flush() return wrapper + class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): if False: ModelA.objects.all().delete() - a=ModelA.objects.create(field1='A1') - b=ModelB.objects.create(field1='B1', field2='B2') - c=ModelC.objects.create(field1='C1', field2='C2', field3='C3') + a = ModelA.objects.create(field1='A1') + b = ModelB.objects.create(field1='B1', field2='B2') + c = ModelC.objects.create(field1='C1', field2='C2', field3='C3') reset_queries() print ModelC.base_objects.all(); show_queries() @@ -55,29 +59,30 @@ class Command(NoArgsCommand): if False: ModelA.objects.all().delete() for i in xrange(1000): - a=ModelA.objects.create(field1=str(i%100)) - b=ModelB.objects.create(field1=str(i%100), field2=str(i%200)) - c=ModelC.objects.create(field1=str(i%100), field2=str(i%200), field3=str(i%300)) - if i%100==0: print i + a = ModelA.objects.create(field1=str(i % 100)) + b = ModelB.objects.create(field1=str(i % 100), field2=str(i % 200)) + c = ModelC.objects.create(field1=str(i % 100), field2=str(i % 200), field3=str(i % 300)) + if i % 100 == 0: print i - f=print_timing(poly_sql_query,iterations=1000) + f = print_timing(poly_sql_query, iterations=1000) f() - - f=print_timing(poly_sql_query2,iterations=1000) + + f = print_timing(poly_sql_query2, iterations=1000) f() return nModelA.objects.all().delete() - a=nModelA.objects.create(field1='A1') - b=nModelB.objects.create(field1='B1', field2='B2') - c=nModelC.objects.create(field1='C1', field2='C2', field3='C3') - qs=ModelA.objects.raw("SELECT * from pexp_modela") + a = nModelA.objects.create(field1='A1') + b = nModelB.objects.create(field1='B1', field2='B2') + c = nModelC.objects.create(field1='C1', field2='C2', field3='C3') + qs = ModelA.objects.raw("SELECT * from pexp_modela") for o in list(qs): print o from django.db import connection, transaction from random import Random -rnd=Random() +rnd = Random() + def poly_sql_query(): cursor = connection.cursor() @@ -90,10 +95,11 @@ def poly_sql_query(): ON pexp_modelb.modela_ptr_id = pexp_modelc.modelb_ptr_id WHERE pexp_modela.field1=%i ORDER BY pexp_modela.id - """ % rnd.randint(0,100) ) + """ % rnd.randint(0, 100) ) #row=cursor.fetchone() return + def poly_sql_query2(): cursor = connection.cursor() cursor.execute(""" @@ -101,6 +107,6 @@ def poly_sql_query2(): FROM pexp_modela WHERE pexp_modela.field1=%i ORDER BY pexp_modela.id - """ % rnd.randint(0,100) ) + """ % rnd.randint(0, 100) ) #row=cursor.fetchone() return diff --git a/example/pexp/management/commands/pcmd.py b/example/pexp/management/commands/pcmd.py index 92de13b..48dadb3 100644 --- a/example/pexp/management/commands/pcmd.py +++ b/example/pexp/management/commands/pcmd.py @@ -9,27 +9,29 @@ from pprint import pprint from pexp.models import * + def reset_queries(): - connection.queries=[] + connection.queries = [] + def show_queries(): - print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; connection.queries=[] + print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; connection.queries = [] + class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): Project.objects.all().delete() - a=Project.objects.create(topic="John's gathering") - b=ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") - c=ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") + a = Project.objects.create(topic="John's gathering") + b = ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") + c = ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") print Project.objects.all() print ModelA.objects.all().delete() - a=ModelA.objects.create(field1='A1') - b=ModelB.objects.create(field1='B1', field2='B2') - c=ModelC.objects.create(field1='C1', field2='C2', field3='C3') + a = ModelA.objects.create(field1='A1') + b = ModelB.objects.create(field1='B1', field2='B2') + c = ModelC.objects.create(field1='C1', field2='C2', field3='C3') print ModelA.objects.all() print - diff --git a/example/pexp/management/commands/polybench.py b/example/pexp/management/commands/polybench.py index 44d19aa..05bf0d4 100644 --- a/example/pexp/management/commands/polybench.py +++ b/example/pexp/management/commands/polybench.py @@ -9,43 +9,47 @@ from pprint import pprint import sys from pexp.models import * -num_objects=1000 +num_objects = 1000 + def reset_queries(): - connection.queries=[] + connection.queries = [] + def show_queries(): - print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; reset_queries() + print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; reset_queries() import time ################################################################################### ### benchmark wrappers + def print_timing(func, message='', iterations=1): def wrapper(*arg): - results=[] + results = [] reset_queries() for i in xrange(iterations): t1 = time.time() x = func(*arg) t2 = time.time() - results.append((t2-t1)*1000.0) - res_sum=0 - for r in results: res_sum +=r + results.append((t2 - t1) * 1000.0) + res_sum = 0 + for r in results: res_sum += r median = res_sum / len(results) print '%s%-19s: %.0f ms, %i queries' % ( - message,func.func_name, + message, func.func_name, median, - len(connection.queries)/len(results) - ) + len(connection.queries) / len(results) + ) sys.stdout.flush() return wrapper + def run_vanilla_any_poly(func, iterations=1): - f=print_timing(func,' ', iterations) + f = print_timing(func, ' ', iterations) f(nModelC) - f=print_timing(func,'poly ', iterations) + f = print_timing(func, 'poly ', iterations) f(ModelC) @@ -54,30 +58,35 @@ def run_vanilla_any_poly(func, iterations=1): def bench_create(model): for i in xrange(num_objects): - model.objects.create(field1='abc'+str(i), field2='abcd'+str(i), field3='abcde'+str(i)) + model.objects.create(field1='abc' + str(i), field2='abcd' + str(i), field3='abcde' + str(i)) #print 'count:',model.objects.count() + def bench_load1(model): for o in model.objects.all(): pass + def bench_load1_short(model): - for i in xrange(num_objects/100): + for i in xrange(num_objects / 100): for o in model.objects.all()[:100]: pass + def bench_load2(model): for o in model.objects.all(): - f1=o.field1 - f2=o.field2 - f3=o.field3 + f1 = o.field1 + f2 = o.field2 + f3 = o.field3 + def bench_load2_short(model): - for i in xrange(num_objects/100): + for i in xrange(num_objects / 100): for o in model.objects.all()[:100]: - f1=o.field1 - f2=o.field2 - f3=o.field3 + f1 = o.field1 + f2 = o.field2 + f3 = o.field3 + def bench_delete(model): model.objects.all().delete() @@ -85,19 +94,20 @@ def bench_delete(model): ################################################################################### ### Command + class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): func_list = [ - ( bench_delete, 1 ), - ( bench_create, 1 ), - ( bench_load1, 5 ), - ( bench_load1_short, 5 ), - ( bench_load2, 5 ), - ( bench_load2_short, 5 ) - ] - for f,iterations in func_list: - run_vanilla_any_poly(f,iterations=iterations) + (bench_delete, 1), + (bench_create, 1), + (bench_load1, 5), + (bench_load1_short, 5), + (bench_load2, 5), + (bench_load2_short, 5) + ] + for f, iterations in func_list: + run_vanilla_any_poly(f, iterations=iterations) print diff --git a/example/pexp/management/commands/polymorphic_create_test_data.py b/example/pexp/management/commands/polymorphic_create_test_data.py index f7d81e5..7f0ff44 100644 --- a/example/pexp/management/commands/polymorphic_create_test_data.py +++ b/example/pexp/management/commands/polymorphic_create_test_data.py @@ -9,22 +9,22 @@ from pprint import pprint from pexp.models import * + def reset_queries(): - connection.queries=[] + connection.queries = [] + def show_queries(): - print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; connection.queries=[] - + print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; connection.queries = [] + + class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): Project.objects.all().delete() - o=Project.objects.create(topic="John's gathering") - o=ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") - o=ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") + o = Project.objects.create(topic="John's gathering") + o = ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") + o = ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") print Project.objects.all() print - - - diff --git a/example/pexp/models.py b/example/pexp/models.py index 486900e..02785e0 100644 --- a/example/pexp/models.py +++ b/example/pexp/models.py @@ -6,48 +6,74 @@ from django.db import models from polymorphic.models import PolymorphicModel from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent + class Project(ShowFieldContent, PolymorphicModel): topic = models.CharField(max_length=30) + + class ArtProject(Project): artist = models.CharField(max_length=30) + + class ResearchProject(Project): supervisor = models.CharField(max_length=30) + class ModelA(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=10) + + 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) + + class nModelB(nModelA): field2 = models.CharField(max_length=10) + + class nModelC(nModelB): 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) -if django.VERSION < (1,8): +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) + 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) @@ -57,14 +83,18 @@ class ProxyBase(PolymorphicModel): class Meta: ordering = ('title',) + class ProxyA(ProxyBase): + class Meta: proxy = True def __unicode__(self): return u"".format(self.title) + class ProxyB(ProxyBase): + class Meta: proxy = True diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 412e5d1..a16661c 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -29,9 +29,9 @@ if django.VERSION[:2] < (1, 5): ct = self._get_from_cache(opts) except KeyError: ct, created = self.get_or_create( - app_label = opts.app_label, - model = opts.object_name.lower(), - defaults = {'name': smart_text(opts.verbose_name_raw)}, + app_label=opts.app_label, + model=opts.object_name.lower(), + defaults={'name': smart_text(opts.verbose_name_raw)}, ) self._add_to_cache(self.db, ct) @@ -39,4 +39,3 @@ if django.VERSION[:2] < (1, 5): ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model ContentTypeManager.get_for_model = get_for_model - diff --git a/polymorphic/admin.py b/polymorphic/admin.py index 1adba55..93d00fd 100644 --- a/polymorphic/admin.py +++ b/polymorphic/admin.py @@ -42,6 +42,7 @@ class RegistrationClosed(RuntimeError): "The admin model can't be registered anymore at this point." pass + class ChildAdminNotRegistered(RuntimeError): "The admin site for the model is not registered." pass @@ -122,13 +123,11 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): #: If your primary key consists of string values, update this regular expression. pk_regex = '(\d+|__fk__)' - def __init__(self, model, admin_site, *args, **kwargs): super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs) self._child_admin_site = self.admin_site.__class__(name=self.admin_site.name) self._is_setup = False - def _lazy_setup(self): if self._is_setup: return @@ -150,7 +149,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): self._child_admin_site._registry = complete_registry self._is_setup = True - def register_child(self, model, model_admin): """ Register a model with admin to display. @@ -167,7 +165,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): self._child_admin_site.register(model, model_admin) - def get_child_models(self): """ Return the derived model classes which this admin should handle. @@ -181,7 +178,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): return self.child_models - def get_child_type_choices(self, request, action): """ Return a list of polymorphic types for which the user has the permission to perform the given action. @@ -197,7 +193,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): choices.append((ct.id, model._meta.verbose_name)) return choices - def _get_real_admin(self, object_id): try: obj = self.model.objects.non_polymorphic() \ @@ -206,7 +201,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): raise Http404 return self._get_real_admin_by_ct(obj['polymorphic_ctype']) - def _get_real_admin_by_ct(self, ct_id): try: ct = ContentType.objects.get_for_id(ct_id) @@ -219,7 +213,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): return self._get_real_admin_by_model(model_class) - def _get_real_admin_by_model(self, model_class): # In case of a ?ct_id=### parameter, the view is already checked for permissions. # Hence, make sure this is a derived object, or risk exposing other admin interfaces. @@ -233,7 +226,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): except KeyError: raise ChildAdminNotRegistered("No child admin site was registered for a '{0}' model.".format(model_class)) - def get_queryset(self, request): # optimize the list display. qs = super(PolymorphicParentModelAdmin, self).get_queryset(request) @@ -241,7 +233,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): qs = qs.non_polymorphic() return qs - # For Django 1.5: def queryset(self, request): qs = super(PolymorphicParentModelAdmin, self).queryset(request) @@ -249,7 +240,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): qs = qs.non_polymorphic() return qs - def add_view(self, request, form_url='', extra_context=None): """Redirect the add view to the real admin.""" ct_id = int(request.GET.get('ct_id', 0)) @@ -266,13 +256,11 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): ) return real_admin.add_view(request, form_url, extra_context) - def change_view(self, request, object_id, *args, **kwargs): """Redirect the change view to the real admin.""" real_admin = self._get_real_admin(object_id) return real_admin.change_view(request, object_id, *args, **kwargs) - def delete_view(self, request, object_id, extra_context=None): """Redirect the delete view to the real admin.""" real_admin = self._get_real_admin(object_id) @@ -331,7 +319,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): return urls + custom_urls + dummy_urls - def subclass_view(self, request, path): """ Forward any request to a custom view of the real admin. @@ -350,7 +337,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): ct_id = self.model.objects.values_list('polymorphic_ctype_id', flat=True).get(pk=object_id) - real_admin = self._get_real_admin_by_ct(ct_id) resolver = RegexURLResolver('^', real_admin.urls) resolvermatch = resolver.resolve(path) # May raise Resolver404 @@ -359,7 +345,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs) - def add_type_view(self, request, form_url=''): """ Display a choice form to select which page type to add. @@ -402,7 +387,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): } return self.render_add_type_form(request, context, form_url) - def render_add_type_form(self, request, context, form_url=''): """ Render the page type choice form. @@ -426,7 +410,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): "admin/add_type_form.html" ], context, context_instance=context_instance) - @property def change_list_template(self): opts = self.model._meta @@ -446,7 +429,6 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): ] - class PolymorphicChildModelAdmin(admin.ModelAdmin): """ The *optional* base class for the admin interface of derived models. @@ -465,7 +447,6 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): base_fieldsets = None extra_fieldset_title = _("Contents") # Default title for extra fieldset - def get_form(self, request, obj=None, **kwargs): # The django admin validation requires the form to have a 'class Meta: model = ..' # attribute, or it will complain that the fields are missing. @@ -482,7 +463,6 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs) - @property def change_form_template(self): opts = self.model._meta @@ -502,7 +482,6 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): "admin/change_form.html" ] - @property def delete_confirmation_template(self): opts = self.model._meta @@ -522,21 +501,18 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): "admin/delete_confirmation.html" ] - def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): context.update({ 'base_opts': self.base_model._meta, }) return super(PolymorphicChildModelAdmin, self).render_change_form(request, context, add=add, change=change, form_url=form_url, obj=obj) - def delete_view(self, request, object_id, context=None): extra_context = { 'base_opts': self.base_model._meta, } return super(PolymorphicChildModelAdmin, self).delete_view(request, object_id, extra_context) - # ---- Extra: improving the form/fieldset default display ---- def get_fieldsets(self, request, obj=None): @@ -557,7 +533,6 @@ class PolymorphicChildModelAdmin(admin.ModelAdmin): else: return self.base_fieldsets - def get_subclass_fields(self, request, obj=None): # Find out how many fields would really be on the form, # if it weren't restricted by declared fields. diff --git a/polymorphic/base.py b/polymorphic/base.py index 10fe58c..0eb0b33 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -57,7 +57,7 @@ class PolymorphicModelBase(ModelBase): # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses: if not attrs and model_name == 'NewBase': - if django.VERSION < (1,5): + if django.VERSION < (1, 5): # Let Django fully ignore the class which is inserted in between. # Django 1.5 fixed this, see https://code.djangoproject.com/ticket/19688 attrs['__module__'] = 'django.utils.six' @@ -191,9 +191,9 @@ class PolymorphicModelBase(ModelBase): # app_label here for PolymorphicModel. meta = attrs.get('Meta', None) do_app_label_workaround = (meta - and attrs['__module__'] == 'polymorphic' - and model_name == 'PolymorphicModel' - and getattr(meta, 'app_label', None) is None) + and attrs['__module__'] == 'polymorphic' + and model_name == 'PolymorphicModel' + and getattr(meta, 'app_label', None) is None) if do_app_label_workaround: meta.app_label = 'poly_dummy_app_label' diff --git a/polymorphic/models.py b/polymorphic/models.py index cbfdef9..e993f28 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -26,6 +26,7 @@ from .query_translate import translate_polymorphic_Q_object ################################################################################### ### PolymorphicModel + class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): """ Abstract base class that provides polymorphic behaviour @@ -56,7 +57,7 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): # 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+') + 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'] @@ -110,8 +111,8 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): # 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): + 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, )) @@ -196,22 +197,22 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.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): + and model != models.Model + and model != self.__class__ + and model != PolymorphicModel): add_model(model, field_name, result) - def add_all_super_models(model, 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' + 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 + 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: @@ -220,7 +221,7 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): 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 = {} diff --git a/polymorphic/query.py b/polymorphic/query.py index 2d6306e..7038708 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -35,7 +35,7 @@ def transmogrify(cls, obj): else: # Run constructor, reassign values new = cls() - for k,v in obj.__dict__.items(): + for k, v in obj.__dict__.items(): new.__dict__[k] = v return new @@ -72,7 +72,7 @@ class PolymorphicQuerySet(QuerySet): new.polymorphic_disabled = self.polymorphic_disabled return new - if django.VERSION >= (1,7): + if django.VERSION >= (1, 7): def as_manager(cls): # Make sure the Django 1.7 way of creating managers works. from .manager import PolymorphicManager @@ -333,14 +333,15 @@ class PolymorphicQuerySet(QuerySet): def __repr__(self, *args, **kwargs): if self.model.polymorphic_query_multiline_output: result = [repr(o) for o in self.all()] - return '[ ' + ',\n '.join(result) + ' ]' + return '[ ' + ',\n '.join(result) + ' ]' else: return super(PolymorphicQuerySet, self).__repr__(*args, **kwargs) class _p_list_class(list): + def __repr__(self, *args, **kwargs): result = [repr(o) for o in self] - return '[ ' + ',\n '.join(result) + ' ]' + return '[ ' + ',\n '.join(result) + ' ]' def get_real_instances(self, base_result_objects=None): "same as _get_real_instances, but make sure that __repr__ for ShowField... creates correct output" diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index 20d823d..ecbb22d 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -248,7 +248,7 @@ def _create_model_filter_Q(modellist, not_instance_of=False): q = q | q_class_with_subclasses(subclass) return q - qlist = [q_class_with_subclasses(m) for m in modellist] + qlist = [q_class_with_subclasses(m) for m in modellist] q_ored = reduce(lambda a, b: a | b, qlist) if not_instance_of: diff --git a/polymorphic/showfields.py b/polymorphic/showfields.py index 1a279d6..6d1f6e1 100644 --- a/polymorphic/showfields.py +++ b/polymorphic/showfields.py @@ -3,6 +3,7 @@ from django.db import models from django.utils import six + class ShowFieldBase(object): """ base class for the ShowField... model mixins, does the work """ @@ -119,8 +120,8 @@ class ShowFieldBase(object): next_new_section, _, _ = parts[i + 1] if (self.polymorphic_showfield_max_line_width - and xpos + len(p) > self.polymorphic_showfield_max_line_width - and possible_line_break_pos != None): + and xpos + len(p) > self.polymorphic_showfield_max_line_width + and possible_line_break_pos != None): rest = out[possible_line_break_pos:] out = out[:possible_line_break_pos] out += '\n' + indentstr + rest diff --git a/polymorphic/templatetags/polymorphic_admin_tags.py b/polymorphic/templatetags/polymorphic_admin_tags.py index 8fc5dca..b51793a 100644 --- a/polymorphic/templatetags/polymorphic_admin_tags.py +++ b/polymorphic/templatetags/polymorphic_admin_tags.py @@ -5,6 +5,7 @@ register = Library() class BreadcrumbScope(Node): + def __init__(self, base_opts, nodelist): self.base_opts = base_opts self.nodelist = nodelist # Note, takes advantage of Node.child_nodelists @@ -25,7 +26,6 @@ class BreadcrumbScope(Node): else: raise TemplateSyntaxError("{0} tag expects 1 argument".format(token.contents[0])) - def render(self, context): # app_label is really hard to overwrite in the standard Django ModelAdmin. # To insert it in the template, the entire render_change_form() and delete_view() have to copied and adjusted. diff --git a/polymorphic/tests.py b/polymorphic/tests.py index d4abb8c..4ff99ce 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -14,7 +14,7 @@ except ImportError: from django.db.models.query import QuerySet from django.test import TestCase -from django.db.models import Q,Count +from django.db.models import Q, Count from django.db import models from django.contrib.contenttypes.models import ContentType from django.utils import six @@ -32,85 +32,133 @@ except ImportError: class PlainA(models.Model): field1 = models.CharField(max_length=10) + + class PlainB(PlainA): field2 = models.CharField(max_length=10) + + class PlainC(PlainB): field3 = models.CharField(max_length=10) + class Model2A(ShowFieldType, 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 Model2D(Model2C): field4 = models.CharField(max_length=10) + class ModelExtraA(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=10) + + class ModelExtraB(ModelExtraA): field2 = models.CharField(max_length=10) + + class ModelExtraC(ModelExtraB): field3 = models.CharField(max_length=10) + + class ModelExtraExternal(models.Model): topic = models.CharField(max_length=10) -class ModelShow1(ShowFieldType,PolymorphicModel): + +class ModelShow1(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=10) m2m = models.ManyToManyField('self') + + class ModelShow2(ShowFieldContent, PolymorphicModel): field1 = models.CharField(max_length=10) m2m = models.ManyToManyField('self') + + class ModelShow3(ShowFieldTypeAndContent, PolymorphicModel): field1 = models.CharField(max_length=10) m2m = models.ManyToManyField('self') + class ModelShow1_plain(PolymorphicModel): field1 = models.CharField(max_length=10) + + class ModelShow2_plain(ModelShow1_plain): field2 = models.CharField(max_length=10) class Base(ShowFieldType, PolymorphicModel): field_b = models.CharField(max_length=10) + + class ModelX(Base): field_x = models.CharField(max_length=10) + + class ModelY(Base): field_y = models.CharField(max_length=10) + class Enhance_Plain(models.Model): field_p = models.CharField(max_length=10) + + class Enhance_Base(ShowFieldTypeAndContent, PolymorphicModel): base_id = models.AutoField(primary_key=True) field_b = models.CharField(max_length=10) + + class Enhance_Inherit(Enhance_Base, Enhance_Plain): field_i = models.CharField(max_length=10) + class RelationBase(ShowFieldTypeAndContent, PolymorphicModel): field_base = models.CharField(max_length=10) fk = models.ForeignKey('self', null=True, related_name='relationbase_set') m2m = models.ManyToManyField('self') + + class RelationA(RelationBase): field_a = models.CharField(max_length=10) + + class RelationB(RelationBase): field_b = models.CharField(max_length=10) + + class RelationBC(RelationB): field_c = models.CharField(max_length=10) + class RelatingModel(models.Model): many2many = models.ManyToManyField(Model2A) + class One2OneRelatingModel(PolymorphicModel): one2one = models.OneToOneField(Model2A) field1 = models.CharField(max_length=10) + class One2OneRelatingModelDerived(One2OneRelatingModel): field2 = models.CharField(max_length=10) + class MyManagerQuerySet(PolymorphicQuerySet): + def my_queryset_foo(self): return self.all() # Just a method to prove the existance of the custom queryset. + class MyManager(PolymorphicManager): queryset_class = MyManagerQuerySet @@ -120,27 +168,38 @@ class MyManager(PolymorphicManager): # Django <= 1.5 compatibility get_query_set = get_queryset + class ModelWithMyManager(ShowFieldTypeAndContent, Model2A): objects = MyManager() field4 = models.CharField(max_length=10) -if django.VERSION >= (1,7): +if django.VERSION >= (1, 7): class ModelWithMyManager2(ShowFieldTypeAndContent, Model2A): objects = MyManagerQuerySet.as_manager() field4 = models.CharField(max_length=10) + class MROBase1(ShowFieldType, PolymorphicModel): objects = MyManager() - field1 = models.CharField(max_length=10) # needed as MyManager uses it + field1 = models.CharField(max_length=10) # needed as MyManager uses it + + class MROBase2(MROBase1): - pass # Django vanilla inheritance does not inherit MyManager as _default_manager here + pass # Django vanilla inheritance does not inherit MyManager as _default_manager here + + class MROBase3(models.Model): objects = PolymorphicManager() + + class MRODerived(MROBase2, MROBase3): pass + class ParentModelWithManager(PolymorphicModel): pass + + class ChildModelWithManager(PolymorphicModel): # Also test whether foreign keys receive the manager: fk = models.ForeignKey(ParentModelWithManager, related_name='childmodel_set') @@ -148,10 +207,13 @@ class ChildModelWithManager(PolymorphicModel): class PlainMyManagerQuerySet(QuerySet): + def my_queryset_foo(self): return self.all() # Just a method to prove the existance of the custom queryset. + class PlainMyManager(models.Manager): + def my_queryset_foo(self): return self.get_queryset().my_queryset_foo() @@ -161,9 +223,11 @@ class PlainMyManager(models.Manager): # Django <= 1.5 compatibility get_query_set = get_queryset + class PlainParentModelWithManager(models.Model): pass + class PlainChildModelWithManager(models.Model): fk = models.ForeignKey(PlainParentModelWithManager, related_name='childmodel_set') objects = PlainMyManager() @@ -173,82 +237,132 @@ class MgrInheritA(models.Model): mgrA = models.Manager() mgrA2 = models.Manager() field1 = models.CharField(max_length=10) + + class MgrInheritB(MgrInheritA): mgrB = models.Manager() field2 = models.CharField(max_length=10) + + class MgrInheritC(ShowFieldTypeAndContent, MgrInheritB): pass + class BlogBase(ShowFieldTypeAndContent, PolymorphicModel): name = models.CharField(max_length=10) + + class BlogA(BlogBase): info = models.CharField(max_length=10) + + class BlogB(BlogBase): pass + + class BlogEntry(ShowFieldTypeAndContent, PolymorphicModel): blog = models.ForeignKey(BlogA) text = models.CharField(max_length=10) + class BlogEntry_limit_choices_to(ShowFieldTypeAndContent, PolymorphicModel): blog = models.ForeignKey(BlogBase) text = models.CharField(max_length=10) + class ModelFieldNameTest(ShowFieldType, PolymorphicModel): modelfieldnametest = models.CharField(max_length=10) + class InitTestModel(ShowFieldType, PolymorphicModel): bar = models.CharField(max_length=100) + def __init__(self, *args, **kwargs): kwargs['bar'] = self.x() super(InitTestModel, self).__init__(*args, **kwargs) + + class InitTestModelSubclass(InitTestModel): + def x(self): return 'XYZ' # models from github issue + + class Top(PolymorphicModel): name = models.CharField(max_length=50) + class Meta: ordering = ('pk',) + + class Middle(Top): description = models.TextField() + + class Bottom(Middle): author = models.CharField(max_length=50) + class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel): - uuid_primary_key = UUIDField(primary_key = True, default=uuid.uuid1) - 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, default=uuid.uuid1) + 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) + + class UUIDPlainC(UUIDPlainB): field3 = models.CharField(max_length=10) # base -> proxy + + class ProxyBase(PolymorphicModel): some_data = models.CharField(max_length=128) + + class ProxyChild(ProxyBase): + class Meta: proxy = True + class NonProxyChild(ProxyBase): - name=models.CharField(max_length=10) + name = models.CharField(max_length=10) # base -> proxy -> real models + + class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel): name = models.CharField(max_length=10) + + class ProxyModelBase(ProxiedBase): + class Meta: proxy = True + + class ProxyModelA(ProxyModelBase): field1 = models.CharField(max_length=10) + + class ProxyModelB(ProxyModelBase): field2 = models.CharField(max_length=10) @@ -264,11 +378,16 @@ class RelatedNameClash(ShowFieldType, PolymorphicModel): ctype = models.ForeignKey(ContentType, null=True, editable=False) #class with a parent_link to superclass, and a related_name back to subclass + + class TestParentLinkAndRelatedName(ModelShow1_plain): superclass = models.OneToOneField(ModelShow1_plain, parent_link=True, related_name='related_name_subclass') + class CustomPkBase(ShowFieldTypeAndContent, PolymorphicModel): b = models.CharField(max_length=1) + + class CustomPkInherit(CustomPkBase): custom_id = models.AutoField(primary_key=True) i = models.CharField(max_length=1) @@ -323,7 +442,7 @@ class PolymorphicTests(TestCase): # test ordering for field in one subclass only # MySQL and SQLite return this order - expected1=''' + expected1 = ''' [ , , , @@ -334,7 +453,7 @@ class PolymorphicTests(TestCase): ]''' # PostgreSQL returns this order - expected2=''' + expected2 = ''' [ , , , @@ -347,7 +466,6 @@ class PolymorphicTests(TestCase): x = '\n' + repr(BlogBase.objects.order_by('-BlogA___info')) self.assertTrue(x == expected1 or x == expected2) - def test_limit_choices_to(self): """ this is not really a testcase, as limit_choices_to only affects the Django admin @@ -359,7 +477,6 @@ class PolymorphicTests(TestCase): entry1 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text='bla2') entry2 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text='bla2') - def test_primary_key_custom_field_problem(self): """ object retrieval problem occuring with some custom primary key fields (UUIDField as test case) @@ -377,7 +494,7 @@ class PolymorphicTests(TestCase): self.assertIsInstance(a.uuid_primary_key, uuid.UUID) self.assertIsInstance(a.pk, uuid.UUID) - res = re.sub(' "(.*?)..", topic',', topic', repr(qs)) + res = re.sub(' "(.*?)..", topic', ', topic', repr(qs)) res_exp = """[ , , ]""" @@ -401,7 +518,6 @@ class PolymorphicTests(TestCase): Model2C.objects.create(field1='C1', field2='C2', field3='C3') Model2D.objects.create(field1='D1', field2='D2', field3='D3', field4='D4') - def test_simple_inheritance(self): self.create_model2abcd() @@ -411,14 +527,12 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[2]), '') self.assertEqual(repr(objects[3]), '') - def test_manual_get_real_instance(self): self.create_model2abcd() o = Model2A.objects.non_polymorphic().get(field1='C1') self.assertEqual(repr(o.get_real_instance()), '') - def test_non_polymorphic(self): self.create_model2abcd() @@ -428,7 +542,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[2]), '') self.assertEqual(repr(objects[3]), '') - def test_get_real_instances(self): self.create_model2abcd() qs = Model2A.objects.all().non_polymorphic() @@ -447,7 +560,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[2]), '') self.assertEqual(repr(objects[3]), '') - def test_translate_polymorphic_q_object(self): self.create_model2abcd() @@ -456,7 +568,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[0]), '') self.assertEqual(repr(objects[1]), '') - def test_base_manager(self): def show_base_manager(model): return "{0} {1}".format( @@ -475,7 +586,6 @@ class PolymorphicTests(TestCase): self.assertEqual(show_base_manager(One2OneRelatingModel), " ") self.assertEqual(show_base_manager(One2OneRelatingModelDerived), " ") - def test_instance_default_manager(self): def show_default_manager(instance): return "{0} {1}".format( @@ -508,7 +618,6 @@ class PolymorphicTests(TestCase): object2b = Model2B.base_objects.get(field1='C1') self.assertEqual(repr(object2b.model2c), '') - def test_onetoone_field(self): self.create_model2abcd() @@ -522,7 +631,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(c.one2one), '') self.assertEqual(repr(a.one2onerelatingmodel), '') - def test_manytomany_field(self): # Model 1 o = ModelShow1.objects.create(field1='abc') @@ -537,7 +645,7 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(ModelShow2.objects.all()), '[ ]') # Model 3 - o=ModelShow3.objects.create(field1='abc') + o = ModelShow3.objects.create(field1='abc') o.m2m.add(o) o.save() self.assertEqual(repr(ModelShow3.objects.all()), '[ ]') @@ -550,7 +658,6 @@ class PolymorphicTests(TestCase): ModelShow2_plain.objects.create(field1='abc', field2='def') self.assertEqual(repr(ModelShow1_plain.objects.all()), '[, ]') - def test_extra_method(self): self.create_model2abcd() @@ -569,7 +676,7 @@ class PolymorphicTests(TestCase): ModelExtraExternal.objects.create(topic='extra1') ModelExtraExternal.objects.create(topic='extra2') ModelExtraExternal.objects.create(topic='extra3') - objects = ModelExtraA.objects.extra(tables=["polymorphic_modelextraexternal"], select={"topic":"polymorphic_modelextraexternal.topic"}, where=["polymorphic_modelextraa.id = polymorphic_modelextraexternal.id"]) + objects = ModelExtraA.objects.extra(tables=["polymorphic_modelextraexternal"], select={"topic": "polymorphic_modelextraexternal.topic"}, where=["polymorphic_modelextraa.id = polymorphic_modelextraexternal.id"]) if six.PY3: self.assertEqual(repr(objects[0]), '') self.assertEqual(repr(objects[1]), '') @@ -580,7 +687,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[2]), '') self.assertEqual(len(objects), 3) - def test_instance_of_filter(self): self.create_model2abcd() @@ -606,16 +712,14 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[0]), '') self.assertEqual(len(objects), 1) - def test_polymorphic___filter(self): self.create_model2abcd() - objects = Model2A.objects.filter(Q( Model2B___field2='B2') | Q( Model2C___field3='C3')) + objects = Model2A.objects.filter(Q(Model2B___field2='B2') | Q(Model2C___field3='C3')) self.assertEqual(len(objects), 2) self.assertEqual(repr(objects[0]), '') self.assertEqual(repr(objects[1]), '') - def test_delete(self): self.create_model2abcd() @@ -630,7 +734,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[2]), '') self.assertEqual(len(objects), 3) - def test_combine_querysets(self): ModelX.objects.create(field_x='x') ModelY.objects.create(field_y='y') @@ -640,7 +743,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(qs[1]), '') self.assertEqual(len(qs), 2) - def test_multiple_inheritance(self): # multiple inheritance, subclassing third party models (mix PolymorphicModel with models.Model) @@ -685,7 +787,6 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[1]), '') self.assertEqual(len(objects), 2) - def test_user_defined_manager(self): self.create_model2abcd() ModelWithMyManager.objects.create(field1='D1a', field4='D4a') @@ -700,7 +801,7 @@ class PolymorphicTests(TestCase): self.assertIs(type(ModelWithMyManager._default_manager), MyManager) self.assertIs(type(ModelWithMyManager.base_objects), models.Manager) - @skipIf(django.VERSION < (1,7), "This test needs Django 1.7+") + @skipIf(django.VERSION < (1, 7), "This test needs Django 1.7+") def test_user_defined_queryset_as_manager(self): self.create_model2abcd() ModelWithMyManager2.objects.create(field1='D1a', field4='D4a') @@ -715,7 +816,6 @@ class PolymorphicTests(TestCase): self.assertEqual(type(ModelWithMyManager2._default_manager).__name__, 'PolymorphicManagerFromMyManagerQuerySet') self.assertIs(type(ModelWithMyManager2.base_objects), models.Manager) - def test_manager_inheritance(self): # by choice of MRO, should be MyManager from MROBase1. self.assertIs(type(MRODerived.objects), MyManager) @@ -726,7 +826,6 @@ class PolymorphicTests(TestCase): # Django vanilla inheritance does not inherit MyManager as _default_manager here self.assertIs(type(MROBase2._default_manager), MyManager) - def test_queryset_assignment(self): # This is just a consistency check for now, testing standard Django behavior. parent = PlainParentModelWithManager.objects.create() @@ -750,7 +849,6 @@ class PolymorphicTests(TestCase): # A related set is created using the model's _default_manager, so does gain extra methods. self.assertIs(type(parent.childmodel_set.my_queryset_foo()), MyManagerQuerySet) - def test_proxy_models(self): # prepare some data for data in ('bleep bloop', 'I am a', 'computer'): @@ -771,7 +869,7 @@ class PolymorphicTests(TestCase): This unit test guards that this check is working properly. For instance, proxy child models need to be handled separately. """ - name="Item1" + name = "Item1" nonproxychild = NonProxyChild.objects.create(name=name) pb = ProxyBase.objects.get(id=1) @@ -792,7 +890,6 @@ class PolymorphicTests(TestCase): ct = ContentType.objects.get_for_model(ProxyChild, for_concrete_model=False) self.assertEqual(ProxyChild, ct.model_class()) - def test_proxy_model_inheritance(self): """ Polymorphic abilities should also work when the base model is a proxy object. @@ -852,7 +949,7 @@ class PolymorphicTests(TestCase): #check that the accessors to parent and sublass work correctly and return the right object p = ModelShow1_plain.objects.non_polymorphic().get(field1="TestParentLinkAndRelatedName") - self.assertNotEqual(p, t) #p should be Plain1 and t TestParentLinkAndRelatedName, so not equal + self.assertNotEqual(p, t) # p should be Plain1 and t TestParentLinkAndRelatedName, so not equal self.assertEqual(p, t.superclass) self.assertEqual(p.related_name_subclass, t) @@ -860,7 +957,6 @@ class PolymorphicTests(TestCase): t.delete() - class RegressionTests(TestCase): def test_for_query_result_incomplete_with_inheritance(self): @@ -881,4 +977,3 @@ class RegressionTests(TestCase): expected_queryset = [bottom] self.assertQuerysetEqual(Bottom.objects.all(), [repr(r) for r in expected_queryset]) - diff --git a/polymorphic/tools_for_tests.py b/polymorphic/tools_for_tests.py index 0f2e034..d0b45b7 100644 --- a/polymorphic/tools_for_tests.py +++ b/polymorphic/tools_for_tests.py @@ -63,7 +63,7 @@ class UUIDField(six.with_metaclass(models.SubfieldBase, models.CharField)): self.namespace, self.name = namespace, name super(UUIDField, self).__init__(verbose_name=verbose_name, - name=name, **kwargs) + name=name, **kwargs) def create_uuid(self): if not self.version or self.version == 4: @@ -139,6 +139,6 @@ class UUIDField(six.with_metaclass(models.SubfieldBase, models.CharField)): defaults = { 'form_class': forms.CharField, 'max_length': self.max_length - } + } defaults.update(kwargs) return super(UUIDField, self).formfield(**defaults) From b7431b2d0650175d1e7c662e48824a4b453f1cb7 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 15:14:24 +0100 Subject: [PATCH 04/17] Fix PEP8 block comment issues (E265) --- example/pexp/management/commands/p2cmd.py | 4 ++-- example/pexp/management/commands/polybench.py | 8 ++++---- polymorphic/base.py | 18 +++++++++--------- polymorphic/models.py | 6 +++--- polymorphic/query.py | 4 ++-- polymorphic/query_translate.py | 2 +- polymorphic/tests.py | 12 ++++++------ polymorphic/tools_for_tests.py | 6 +++--- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/example/pexp/management/commands/p2cmd.py b/example/pexp/management/commands/p2cmd.py index 126eacd..1c094e1 100644 --- a/example/pexp/management/commands/p2cmd.py +++ b/example/pexp/management/commands/p2cmd.py @@ -96,7 +96,7 @@ def poly_sql_query(): WHERE pexp_modela.field1=%i ORDER BY pexp_modela.id """ % rnd.randint(0, 100) ) - #row=cursor.fetchone() + # row=cursor.fetchone() return @@ -108,5 +108,5 @@ def poly_sql_query2(): WHERE pexp_modela.field1=%i ORDER BY pexp_modela.id """ % rnd.randint(0, 100) ) - #row=cursor.fetchone() + # row=cursor.fetchone() return diff --git a/example/pexp/management/commands/polybench.py b/example/pexp/management/commands/polybench.py index 05bf0d4..4656045 100644 --- a/example/pexp/management/commands/polybench.py +++ b/example/pexp/management/commands/polybench.py @@ -22,7 +22,7 @@ def show_queries(): import time ################################################################################### -### benchmark wrappers +# benchmark wrappers def print_timing(func, message='', iterations=1): @@ -54,12 +54,12 @@ def run_vanilla_any_poly(func, iterations=1): ################################################################################### -### benchmarks +# benchmarks def bench_create(model): for i in xrange(num_objects): model.objects.create(field1='abc' + str(i), field2='abcd' + str(i), field3='abcde' + str(i)) - #print 'count:',model.objects.count() + # print 'count:',model.objects.count() def bench_load1(model): @@ -92,7 +92,7 @@ def bench_delete(model): model.objects.all().delete() ################################################################################### -### Command +# Command class Command(NoArgsCommand): diff --git a/polymorphic/base.py b/polymorphic/base.py index 0eb0b33..d9b2ff7 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -26,7 +26,7 @@ except ImportError: ################################################################################### -### PolymorphicModel meta class +# PolymorphicModel meta class class PolymorphicModelBase(ModelBase): """ @@ -53,7 +53,7 @@ class PolymorphicModelBase(ModelBase): """ def __new__(self, model_name, bases, attrs): - #print; print '###', model_name, '- bases:', bases + # print; print '###', model_name, '- bases:', bases # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses: if not attrs and model_name == 'NewBase': @@ -75,7 +75,7 @@ class PolymorphicModelBase(ModelBase): # add the managers to the new model for source_name, mgr_name, manager in inherited_managers: - #print '** add inherited manager from model %s, manager %s, %s' % (source_name, mgr_name, manager.__class__.__name__) + # print '** add inherited manager from model %s, manager %s, %s' % (source_name, mgr_name, manager.__class__.__name__) new_manager = manager._copy_to_model(new_class) if mgr_name == '_default_manager': new_class._default_manager = new_manager @@ -86,7 +86,7 @@ class PolymorphicModelBase(ModelBase): # this value is used by the related objects, restoring access to custom queryset methods on related objects. user_manager = self.get_first_user_defined_manager(new_class) if user_manager: - #print '## add default manager', type(def_mgr) + # print '## add default manager', type(def_mgr) new_class._default_manager = user_manager._copy_to_model(new_class) new_class._default_manager._inherited = False # the default mgr was defined by the user, not inherited @@ -111,7 +111,7 @@ class PolymorphicModelBase(ModelBase): use correct mro, only use managers with _inherited==False (they are of no use), skip managers that are overwritten by the user with same-named class attributes (in attrs) """ - #print "** ", self.__name__ + # print "** ", self.__name__ add_managers = [] add_managers_keys = set() for base in self.__mro__[1:]: @@ -147,7 +147,7 @@ class PolymorphicModelBase(ModelBase): continue # manager with that name already added, skip if manager._inherited: continue # inherited managers (on the bases) have no significance, they are just copies - #print '## {0} {1}'.format(self.__name__, key) + # print '## {0} {1}'.format(self.__name__, key) if isinstance(manager, PolymorphicManager): # validate any inherited polymorphic managers self.validate_model_manager(manager, self.__name__, key) @@ -175,7 +175,7 @@ class PolymorphicModelBase(ModelBase): # if there are user defined managers, use first one as _default_manager if mgr_list: _, manager_name, manager = sorted(mgr_list)[0] - #sys.stderr.write( '\n# first user defined manager for model "{model}":\n# "{mgrname}": {mgr}\n# manager model: {mgrmodel}\n\n' + # sys.stderr.write( '\n# first user defined manager for model "{model}":\n# "{mgrname}": {mgr}\n# manager model: {mgrmodel}\n\n' # .format( model=self.__name__, mgrname=manager_name, mgr=manager, mgrmodel=manager.model ) ) return manager return None @@ -241,8 +241,8 @@ class PolymorphicModelBase(ModelBase): frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name if 'django/core/management/commands/dumpdata.py' in frm[1]: return self.base_objects - #caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4 - #if caller_mod_name == 'django.core.management.commands.dumpdata': + # caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4 + # if caller_mod_name == 'django.core.management.commands.dumpdata': return super(PolymorphicModelBase, self).__getattribute__(name) # TODO: investigate Django how this can be avoided diff --git a/polymorphic/models.py b/polymorphic/models.py index e993f28..85d8d28 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -24,7 +24,7 @@ from .manager import PolymorphicManager from .query_translate import translate_polymorphic_Q_object ################################################################################### -### PolymorphicModel +# PolymorphicModel class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): @@ -216,10 +216,10 @@ class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)): 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 + # 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 + # 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) diff --git a/polymorphic/query.py b/polymorphic/query.py index 7038708..1d229ab 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -41,7 +41,7 @@ def transmogrify(cls, obj): ################################################################################### -### PolymorphicQuerySet +# PolymorphicQuerySet def _query_annotations(query): try: @@ -160,7 +160,7 @@ class PolymorphicQuerySet(QuerySet): # 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. - #def extra(self, *args, **kwargs): + # def extra(self, *args, **kwargs): def _get_real_instances(self, base_result_objects): """ diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index ecbb22d..92115df 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -21,7 +21,7 @@ from functools import reduce ################################################################################### -### PolymorphicQuerySet support functions +# PolymorphicQuerySet support functions # These functions implement the additional filter- and Q-object functionality. # They form a kind of small framework for easily adding more diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 4ff99ce..9fb0119 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -368,7 +368,7 @@ class ProxyModelB(ProxyModelBase): # test bad field name -#class TestBadFieldModel(ShowFieldType, PolymorphicModel): +# class TestBadFieldModel(ShowFieldType, PolymorphicModel): # instance_of = models.CharField(max_length=10) # validation error: "polymorphic.relatednameclash: Accessor for field 'polymorphic_ctype' clashes @@ -377,7 +377,7 @@ class ProxyModelB(ProxyModelBase): class RelatedNameClash(ShowFieldType, PolymorphicModel): ctype = models.ForeignKey(ContentType, null=True, editable=False) -#class with a parent_link to superclass, and a related_name back to subclass +# class with a parent_link to superclass, and a related_name back to subclass class TestParentLinkAndRelatedName(ModelShow1_plain): @@ -929,7 +929,7 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(qs[1]), '') def test_fix_getattribute(self): - ### fixed issue in PolymorphicModel.__getattribute__: field name same as model name + # fixed issue in PolymorphicModel.__getattribute__: field name same as model name o = ModelFieldNameTest.objects.create(modelfieldnametest='1') self.assertEqual(repr(o), '') @@ -943,17 +943,17 @@ class PolymorphicTests(TestCase): t.save() p = ModelShow1_plain.objects.get(field1="TestParentLinkAndRelatedName") - #check that p is equal to the + # check that p is equal to the self.assertIsInstance(p, TestParentLinkAndRelatedName) self.assertEqual(p, t) - #check that the accessors to parent and sublass work correctly and return the right object + # check that the accessors to parent and sublass work correctly and return the right object p = ModelShow1_plain.objects.non_polymorphic().get(field1="TestParentLinkAndRelatedName") self.assertNotEqual(p, t) # p should be Plain1 and t TestParentLinkAndRelatedName, so not equal self.assertEqual(p, t.superclass) self.assertEqual(p.related_name_subclass, t) - #test that we can delete the object + # test that we can delete the object t.delete() diff --git a/polymorphic/tools_for_tests.py b/polymorphic/tools_for_tests.py index d0b45b7..9a60b60 100644 --- a/polymorphic/tools_for_tests.py +++ b/polymorphic/tools_for_tests.py @@ -103,11 +103,11 @@ class UUIDField(six.with_metaclass(models.SubfieldBase, models.CharField)): # instance), everything works. # - #if not value: + # if not value: # return None - #if isinstance(value, uuid.UUID): + # if isinstance(value, uuid.UUID): # return smart_text(value) - #else: + # else: # return value def pre_save(self, model_instance, add): From 4e5ac03412532a4da0f3838917c9a8e2f2519377 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 15:26:22 +0100 Subject: [PATCH 05/17] Fix running example management commands in Django 1.9 --- example/pexp/management/commands/p2cmd.py | 8 ++++++-- example/pexp/management/commands/pcmd.py | 2 +- example/pexp/management/commands/polybench.py | 8 ++++++-- .../management/commands/polymorphic_create_test_data.py | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/example/pexp/management/commands/p2cmd.py b/example/pexp/management/commands/p2cmd.py index 1c094e1..5b55f22 100644 --- a/example/pexp/management/commands/p2cmd.py +++ b/example/pexp/management/commands/p2cmd.py @@ -4,9 +4,10 @@ This module is a scratchpad for general development, testing & debugging Well, even more so than pcmd.py. You best ignore p2cmd.py. """ import uuid +import django from django.core.management.base import NoArgsCommand -from django.db.models import connection +from django.db import connection from pprint import pprint import time, sys @@ -14,7 +15,10 @@ from pexp.models import * def reset_queries(): - connection.queries = [] + if django.VERSION < (1, 9): + connection.queries = [] + else: + connection.queries_log.clear() def show_queries(): diff --git a/example/pexp/management/commands/pcmd.py b/example/pexp/management/commands/pcmd.py index 48dadb3..b496ba7 100644 --- a/example/pexp/management/commands/pcmd.py +++ b/example/pexp/management/commands/pcmd.py @@ -4,7 +4,7 @@ This module is a scratchpad for general development, testing & debugging. """ from django.core.management.base import NoArgsCommand -from django.db.models import connection +from django.db import connection from pprint import pprint from pexp.models import * diff --git a/example/pexp/management/commands/polybench.py b/example/pexp/management/commands/polybench.py index 4656045..f056b26 100644 --- a/example/pexp/management/commands/polybench.py +++ b/example/pexp/management/commands/polybench.py @@ -3,8 +3,9 @@ This module is a scratchpad for general development, testing & debugging """ +import django from django.core.management.base import NoArgsCommand -from django.db.models import connection +from django.db import connection from pprint import pprint import sys from pexp.models import * @@ -13,7 +14,10 @@ num_objects = 1000 def reset_queries(): - connection.queries = [] + if django.VERSION < (1, 9): + connection.queries = [] + else: + connection.queries_log.clear() def show_queries(): diff --git a/example/pexp/management/commands/polymorphic_create_test_data.py b/example/pexp/management/commands/polymorphic_create_test_data.py index 7f0ff44..bade771 100644 --- a/example/pexp/management/commands/polymorphic_create_test_data.py +++ b/example/pexp/management/commands/polymorphic_create_test_data.py @@ -4,7 +4,7 @@ This module is a scratchpad for general development, testing & debugging """ from django.core.management.base import NoArgsCommand -from django.db.models import connection +from django.db import connection from pprint import pprint from pexp.models import * From 07242d210fd196893433d9a26d215c2c0e372d14 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 16:19:27 +0100 Subject: [PATCH 06/17] Fix "Model" __ "_field" query support Broken by 24e6b212046de58b39ab77b5c9eb1ae73e31ae2b for Django 1.9 upgrade --- polymorphic/query_translate.py | 11 ++--------- polymorphic/tests.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index 92115df..98da463 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -8,14 +8,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.db.models import Q, FieldDoesNotExist -try: - from django.db.models.related import RelatedObject -except ImportError: - # django.db.models.related.RelatedObject was replaced - # by django.db.models.fields.related.ForeignObjectRel in - # Django 1.8 - from django.db.models.fields.related import ForeignObjectRel - RelatedObject = ForeignObjectRel +from django.db.models.fields.related import RelatedField # Django 1.8 from functools import reduce @@ -163,7 +156,7 @@ def translate_polymorphic_field_path(queryset_model, field_path): try: # rel = (field_object, model, direct, m2m) field = queryset_model._meta.get_field(classname) - if isinstance(field, RelatedObject): + if isinstance(field, RelatedField): # 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. return field_path # No exception raised, field does exist. diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 9fb0119..b2da0eb 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -153,6 +153,15 @@ class One2OneRelatingModelDerived(One2OneRelatingModel): field2 = models.CharField(max_length=10) +class ModelUnderRelParent(PolymorphicModel): + field1 = models.CharField(max_length=10) + _private = models.CharField(max_length=10) + + +class ModelUnderRelChild(PolymorphicModel): + parent = models.ForeignKey(ModelUnderRelParent) + + class MyManagerQuerySet(PolymorphicQuerySet): def my_queryset_foo(self): @@ -720,6 +729,14 @@ class PolymorphicTests(TestCase): self.assertEqual(repr(objects[0]), '') self.assertEqual(repr(objects[1]), '') + def test_polymorphic___filter_field(self): + p = ModelUnderRelParent.objects.create(_private=True, field1='AA') + ModelUnderRelChild.objects.create(parent=p) + + # The "___" filter should also parse to "parent" -> "_private" as fallback. + objects = ModelUnderRelChild.objects.filter(parent___private=True) + self.assertEqual(len(objects), 1) + def test_delete(self): self.create_model2abcd() From c2b7e75125acc5bceaf63be80549cad5497feb25 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 16:19:52 +0100 Subject: [PATCH 07/17] Improve runtests.py to run specific tests --- runtests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/runtests.py b/runtests.py index 545a014..f536b43 100755 --- a/runtests.py +++ b/runtests.py @@ -50,9 +50,15 @@ if not settings.configured: SITE_ID = 3, ) +DEFAULT_TEST_APPS = [ + 'polymorphic', +] + def runtests(): - argv = sys.argv[:1] + ['test', 'polymorphic', '--traceback'] + sys.argv[1:] + other_args = list(filter(lambda arg: arg.startswith('-'), sys.argv[1:])) + test_apps = list(filter(lambda arg: not arg.startswith('-'), sys.argv[1:])) or DEFAULT_TEST_APPS + argv = sys.argv[:1] + ['test', '--traceback'] + other_args + test_apps execute_from_command_line(argv) if __name__ == '__main__': From 50f21dfa43ade10d7029761d659cd4f32ee806dc Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 16:59:39 +0100 Subject: [PATCH 08/17] Rename manager.py => managers.py for consistency --- polymorphic/base.py | 2 +- polymorphic/manager.py | 57 ++--------------------------------------- polymorphic/managers.py | 55 +++++++++++++++++++++++++++++++++++++++ polymorphic/models.py | 2 +- polymorphic/query.py | 2 +- polymorphic/tests.py | 12 ++++----- 6 files changed, 66 insertions(+), 64 deletions(-) create mode 100644 polymorphic/managers.py diff --git a/polymorphic/base.py b/polymorphic/base.py index d9b2ff7..8c6b154 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -12,7 +12,7 @@ from django.db import models from django.db.models.base import ModelBase from django.db.models.manager import ManagerDescriptor -from .manager import PolymorphicManager +from .managers import PolymorphicManager from .query import PolymorphicQuerySet # PolymorphicQuerySet Q objects (and filter()) support these additional key words. diff --git a/polymorphic/manager.py b/polymorphic/manager.py index 06f52c8..3e3e35d 100644 --- a/polymorphic/manager.py +++ b/polymorphic/manager.py @@ -1,55 +1,2 @@ -# -*- coding: utf-8 -*- -""" PolymorphicManager - Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/ -""" -from __future__ import unicode_literals -import warnings -import django -from django.db import models -from polymorphic.query import PolymorphicQuerySet - - -class PolymorphicManager(models.Manager): - """ - Manager for PolymorphicModel - - Usually not explicitly needed, except if a custom manager or - a custom queryset class is to be used. - """ - # Tell Django that related fields also need to use this manager: - use_for_related_fields = True - queryset_class = PolymorphicQuerySet - - def __init__(self, queryset_class=None, *args, **kwrags): - # Up till polymorphic 0.4, the queryset class could be specified as parameter to __init__. - # However, this doesn't work for related managers which instantiate a new version of this class. - # Hence, for custom managers the new default is using the 'queryset_class' attribute at class level instead. - if queryset_class: - warnings.warn("Using PolymorphicManager(queryset_class=..) is deprecated; override the queryset_class attribute instead", DeprecationWarning) - # For backwards compatibility, still allow the parameter: - self.queryset_class = queryset_class - - super(PolymorphicManager, self).__init__(*args, **kwrags) - - def get_queryset(self): - return self.queryset_class(self.model, using=self._db) - - # For Django 1.5 - if django.VERSION < (1, 7): - get_query_set = get_queryset - - # Proxy all unknown method calls to the queryset, so that its members are - # directly accessible as PolymorphicModel.objects.* - # The advantage of this method is that not yet known member functions of derived querysets will be proxied as well. - # We exclude any special functions (__) from this automatic proxying. - # - # NOTE: Fetching the queryset is done by calling self.all() here on purpose. - # By using .all(), the proper get_query_set()/get_queryset() will be used for each Django version. - # Django 1.4/1.5 need to use get_query_set(), because the RelatedManager overrides that. - def __getattr__(self, name): - if name.startswith('__'): - return super(PolymorphicManager, self).__getattr__(self, name) - return getattr(self.all(), name) - - def __unicode__(self): - return '%s (PolymorphicManager) using %s' % (self.__class__.__name__, self.queryset_class.__name__) +# For compatibility with pre 0.8 versions +from .managers import PolymorphicQuerySet, PolymorphicManager diff --git a/polymorphic/managers.py b/polymorphic/managers.py new file mode 100644 index 0000000..06f52c8 --- /dev/null +++ b/polymorphic/managers.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +""" PolymorphicManager + Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/ +""" +from __future__ import unicode_literals +import warnings +import django +from django.db import models +from polymorphic.query import PolymorphicQuerySet + + +class PolymorphicManager(models.Manager): + """ + Manager for PolymorphicModel + + Usually not explicitly needed, except if a custom manager or + a custom queryset class is to be used. + """ + # Tell Django that related fields also need to use this manager: + use_for_related_fields = True + queryset_class = PolymorphicQuerySet + + def __init__(self, queryset_class=None, *args, **kwrags): + # Up till polymorphic 0.4, the queryset class could be specified as parameter to __init__. + # However, this doesn't work for related managers which instantiate a new version of this class. + # Hence, for custom managers the new default is using the 'queryset_class' attribute at class level instead. + if queryset_class: + warnings.warn("Using PolymorphicManager(queryset_class=..) is deprecated; override the queryset_class attribute instead", DeprecationWarning) + # For backwards compatibility, still allow the parameter: + self.queryset_class = queryset_class + + super(PolymorphicManager, self).__init__(*args, **kwrags) + + def get_queryset(self): + return self.queryset_class(self.model, using=self._db) + + # For Django 1.5 + if django.VERSION < (1, 7): + get_query_set = get_queryset + + # Proxy all unknown method calls to the queryset, so that its members are + # directly accessible as PolymorphicModel.objects.* + # The advantage of this method is that not yet known member functions of derived querysets will be proxied as well. + # We exclude any special functions (__) from this automatic proxying. + # + # NOTE: Fetching the queryset is done by calling self.all() here on purpose. + # By using .all(), the proper get_query_set()/get_queryset() will be used for each Django version. + # Django 1.4/1.5 need to use get_query_set(), because the RelatedManager overrides that. + def __getattr__(self, name): + if name.startswith('__'): + return super(PolymorphicManager, self).__getattr__(self, name) + return getattr(self.all(), name) + + def __unicode__(self): + return '%s (PolymorphicManager) using %s' % (self.__class__.__name__, self.queryset_class.__name__) diff --git a/polymorphic/models.py b/polymorphic/models.py index 85d8d28..eebcc02 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -20,7 +20,7 @@ from django.contrib.contenttypes.models import ContentType from django.utils import six from .base import PolymorphicModelBase -from .manager import PolymorphicManager +from .managers import PolymorphicManager from .query_translate import translate_polymorphic_Q_object ################################################################################### diff --git a/polymorphic/query.py b/polymorphic/query.py index 1d229ab..068ef47 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -75,7 +75,7 @@ class PolymorphicQuerySet(QuerySet): if django.VERSION >= (1, 7): def as_manager(cls): # Make sure the Django 1.7 way of creating managers works. - from .manager import PolymorphicManager + from .managers import PolymorphicManager manager = PolymorphicManager.from_queryset(cls)() manager._built_with_as_manager = True return manager diff --git a/polymorphic/tests.py b/polymorphic/tests.py index b2da0eb..0bd1e17 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -20,7 +20,7 @@ from django.contrib.contenttypes.models import ContentType from django.utils import six from polymorphic.models import PolymorphicModel -from polymorphic.manager import PolymorphicManager +from polymorphic.managers import PolymorphicManager from polymorphic.query import PolymorphicQuerySet from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent try: @@ -588,11 +588,11 @@ class PolymorphicTests(TestCase): self.assertEqual(show_base_manager(PlainB), " ") self.assertEqual(show_base_manager(PlainC), " ") - self.assertEqual(show_base_manager(Model2A), " ") + self.assertEqual(show_base_manager(Model2A), " ") self.assertEqual(show_base_manager(Model2B), " ") self.assertEqual(show_base_manager(Model2C), " ") - self.assertEqual(show_base_manager(One2OneRelatingModel), " ") + self.assertEqual(show_base_manager(One2OneRelatingModel), " ") self.assertEqual(show_base_manager(One2OneRelatingModelDerived), " ") def test_instance_default_manager(self): @@ -614,9 +614,9 @@ class PolymorphicTests(TestCase): self.assertEqual(show_default_manager(plain_b), " ") self.assertEqual(show_default_manager(plain_c), " ") - self.assertEqual(show_default_manager(model_2a), " ") - self.assertEqual(show_default_manager(model_2b), " ") - self.assertEqual(show_default_manager(model_2c), " ") + self.assertEqual(show_default_manager(model_2a), " ") + self.assertEqual(show_default_manager(model_2b), " ") + self.assertEqual(show_default_manager(model_2c), " ") def test_foreignkey_field(self): self.create_model2abcd() From 5191b6c130cdf0d4c759a90d456b609cd41cb476 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:00:47 +0100 Subject: [PATCH 09/17] Also remove showfields imports from __init__.py If we're to cause pain, let it be once. --- polymorphic/__init__.py | 5 +---- polymorphic/tests.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index a16661c..22373d9 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -6,13 +6,10 @@ 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 -import django -from .showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent -from .showfields import ShowFields, ShowFieldTypes, ShowFieldsAndTypes # import old names for compatibility # Monkey-patch Django < 1.5 to allow ContentTypes for proxy models. +import django if django.VERSION[:2] < (1, 5): from django.contrib.contenttypes.models import ContentTypeManager from django.utils.encoding import smart_text diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 0bd1e17..f11f9cd 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -22,7 +22,7 @@ from django.utils import six from polymorphic.models import PolymorphicModel from polymorphic.managers import PolymorphicManager from polymorphic.query import PolymorphicQuerySet -from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent +from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent try: from django.db.models import UUIDField except ImportError: From 9e2c11beb8f4b5dc7642274dc347b47ddc57f5d1 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:02:00 +0100 Subject: [PATCH 10/17] Replace __version__.py with __init__.py Also fixes reporting in sentry. --- polymorphic/__init__.py | 2 ++ polymorphic/__version__.py | 5 ----- setup.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 polymorphic/__version__.py diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 22373d9..0b4632f 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -6,6 +6,8 @@ Copyright: This code and affiliated files are (C) by Bert Constantin and individual contributors. Please see LICENSE and AUTHORS for more information. """ +# See PEP 440 (https://www.python.org/dev/peps/pep-0440/) +__version__ = "0.8" # Monkey-patch Django < 1.5 to allow ContentTypes for proxy models. diff --git a/polymorphic/__version__.py b/polymorphic/__version__.py deleted file mode 100644 index 32f061a..0000000 --- a/polymorphic/__version__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -""" -See PEP 440 (https://www.python.org/dev/peps/pep-0440/) -""" -__version__ = "0.7.2" diff --git a/setup.py b/setup.py index 7c6fe60..3371cdc 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def find_version(*parts): setup( name = 'django_polymorphic', - version = find_version('polymorphic', '__version__.py'), + version = find_version('polymorphic', '__init__.py'), license = 'BSD', description = 'Seamless Polymorphic Inheritance for Django Models', From 009069a32b3ff627a2b364c557b80accb9ab14c0 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:10:58 +0100 Subject: [PATCH 11/17] Avoid auto-proxying all methods to the queryset. This is unwanted behavior, and django provides much better methods for it instead. (PolymorphicQuerySet.as_manager() / PolymorphicManager.from_queryset() --- polymorphic/managers.py | 26 +++++++++++++------------- polymorphic/query.py | 2 +- polymorphic/tests.py | 3 +++ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/polymorphic/managers.py b/polymorphic/managers.py index 06f52c8..f20cd83 100644 --- a/polymorphic/managers.py +++ b/polymorphic/managers.py @@ -38,18 +38,18 @@ class PolymorphicManager(models.Manager): if django.VERSION < (1, 7): get_query_set = get_queryset - # Proxy all unknown method calls to the queryset, so that its members are - # directly accessible as PolymorphicModel.objects.* - # The advantage of this method is that not yet known member functions of derived querysets will be proxied as well. - # We exclude any special functions (__) from this automatic proxying. - # - # NOTE: Fetching the queryset is done by calling self.all() here on purpose. - # By using .all(), the proper get_query_set()/get_queryset() will be used for each Django version. - # Django 1.4/1.5 need to use get_query_set(), because the RelatedManager overrides that. - def __getattr__(self, name): - if name.startswith('__'): - return super(PolymorphicManager, self).__getattr__(self, name) - return getattr(self.all(), name) - def __unicode__(self): return '%s (PolymorphicManager) using %s' % (self.__class__.__name__, self.queryset_class.__name__) + + # Proxied methods + def non_polymorphic(self): + return self.all().non_polymorphic() + + def instance_of(self, *args): + return self.all().instance_of(*args) + + def not_instance_of(self, *args): + return self.all().not_instance_of(*args) + + def get_real_instances(self, base_result_objects=None): + return self.all().get_real_instances(base_result_objects=base_result_objects) diff --git a/polymorphic/query.py b/polymorphic/query.py index 068ef47..61071dc 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -82,7 +82,7 @@ class PolymorphicQuerySet(QuerySet): as_manager.queryset_only = True as_manager = classmethod(as_manager) - def non_polymorphic(self, *args, **kwargs): + def non_polymorphic(self): """switch off polymorphic behaviour for this query. When the queryset is evaluated, only objects of the type of the base class used for this query are returned.""" diff --git a/polymorphic/tests.py b/polymorphic/tests.py index f11f9cd..b67f584 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -174,6 +174,9 @@ class MyManager(PolymorphicManager): def get_queryset(self): return super(MyManager, self).get_queryset().order_by('-field1') + def my_queryset_foo(self): + return self.all().my_queryset_foo() + # Django <= 1.5 compatibility get_query_set = get_queryset From 7d4f9dd9ae9a7a334c695f40233104354a9b42d6 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:18:40 +0100 Subject: [PATCH 12/17] docs: update changelog on how to to port to 0.8 --- docs/changelog.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index cb657f3..6e1afb0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,34 @@ Changelog ========== +Version 0.8 (2015-12-28) +------------------------ + +* Added Django 1.9 compatibility. + +**NOTE:** The import paths have changed. Instead of ``from polymorphic import X``, +you'll have to import from the proper package. For example: + +.. code-block:: python + + polymorphic.models import PolymorphicModel + polymorphic.managers import PolymorphicManager, PolymorphicQuerySet + polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent + +* Removed ``__version__.py`` in favor of a standard ``__version__`` in ``polymorphic/__init__.py``. +* Renamed ``polymorphic.manager`` => ``polymorphic.managers`` for consistentcy. +* Removed automatic proxying of method calls to the queryset class. + Use the standard Django methods instead: + +.. code-block:: python + + # In model code: + objects = PolymorphicQuerySet.as_manager() + + # For manager code: + MyCustomManager = PolymorphicManager.from_queryset(MyCustomQuerySet) + + Version 0.7.2 (2015-10-01) -------------------------- From 156968b12b78e795cbf8e9fccdc653f9316eb202 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:19:37 +0100 Subject: [PATCH 13/17] Bump version to 0.8 --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index da84e0f..4abefde 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,9 +54,9 @@ copyright = u'2013, Bert Constantin, Chris Glass, Diederik van der Boor' # built documents. # # The short X.Y version. -version = '0.7.2' +version = '0.8' # The full version, including alpha/beta/rc tags. -release = '0.7.2' +release = '0.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From f90e87afcca9fee6561aafa9f0f4d311aa76a999 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:34:25 +0100 Subject: [PATCH 14/17] docs: highlight BACKWARDS INCOMPATIBILITY --- docs/changelog.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6e1afb0..be0fa8e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,8 +5,9 @@ Version 0.8 (2015-12-28) ------------------------ * Added Django 1.9 compatibility. - -**NOTE:** The import paths have changed. Instead of ``from polymorphic import X``, +* Renamed ``polymorphic.manager`` => ``polymorphic.managers`` for consistentcy. +* **BACKWARDS INCOMPATIBILITY:** The import paths have changed to support Django 1.9. + Instead of ``from polymorphic import X``, you'll have to import from the proper package. For example: .. code-block:: python @@ -15,9 +16,8 @@ you'll have to import from the proper package. For example: polymorphic.managers import PolymorphicManager, PolymorphicQuerySet polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent -* Removed ``__version__.py`` in favor of a standard ``__version__`` in ``polymorphic/__init__.py``. -* Renamed ``polymorphic.manager`` => ``polymorphic.managers`` for consistentcy. -* Removed automatic proxying of method calls to the queryset class. +* **BACKWARDS INCOMPATIBILITY:** Removed ``__version__.py`` in favor of a standard ``__version__`` in ``polymorphic/__init__.py``. +* **BACKWARDS INCOMPATIBILITY:** Removed automatic proxying of method calls to the queryset class. Use the standard Django methods instead: .. code-block:: python @@ -29,6 +29,7 @@ you'll have to import from the proper package. For example: MyCustomManager = PolymorphicManager.from_queryset(MyCustomQuerySet) + Version 0.7.2 (2015-10-01) -------------------------- From 137139f2bbfacfc9c90399d4c1b1b8413ea7cd5d Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 28 Dec 2015 17:39:04 +0100 Subject: [PATCH 15/17] Fix other PEP8 issues in assorted files --- docs/conf.py | 25 ++++++++++--------- example/pexp/management/commands/p2cmd.py | 20 ++++++++++----- example/pexp/management/commands/pcmd.py | 6 ++++- example/pexp/management/commands/polybench.py | 9 +++++-- .../commands/polymorphic_create_test_data.py | 6 ++++- runtests.py | 10 ++++---- setup.py | 24 +++++++++--------- 7 files changed, 61 insertions(+), 39 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4abefde..ced3b37 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -176,21 +177,21 @@ htmlhelp_basename = 'django-polymorphicdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-polymorphic.tex', u'django-polymorphic Documentation', - u'Bert Constantin, Chris Glass, Diederik van der Boor', 'manual'), + ('index', 'django-polymorphic.tex', u'django-polymorphic Documentation', + u'Bert Constantin, Chris Glass, Diederik van der Boor', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -233,9 +234,9 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-polymorphic', u'django-polymorphic Documentation', - u'Bert Constantin, Chris Glass, Diederik van der Boor', 'django-polymorphic', 'One line description of project.', - 'Miscellaneous'), + ('index', 'django-polymorphic', u'django-polymorphic Documentation', + u'Bert Constantin, Chris Glass, Diederik van der Boor', 'django-polymorphic', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/example/pexp/management/commands/p2cmd.py b/example/pexp/management/commands/p2cmd.py index 5b55f22..f6c31bd 100644 --- a/example/pexp/management/commands/p2cmd.py +++ b/example/pexp/management/commands/p2cmd.py @@ -9,7 +9,8 @@ import django from django.core.management.base import NoArgsCommand from django.db import connection from pprint import pprint -import time, sys +import time +import sys from pexp.models import * @@ -22,7 +23,11 @@ def reset_queries(): def show_queries(): - print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; connection.queries = [] + print + print 'QUERIES:', len(connection.queries) + pprint(connection.queries) + print + connection.queries = [] def print_timing(func, message='', iterations=1): @@ -35,7 +40,8 @@ def print_timing(func, message='', iterations=1): t2 = time.time() results.append((t2 - t1) * 1000.0) res_sum = 0 - for r in results: res_sum += r + for r in results: + res_sum += r median = res_sum / len(results) print '%s%-19s: %.4f ms, %i queries (%i times)' % ( message, func.func_name, @@ -57,7 +63,7 @@ class Command(NoArgsCommand): b = ModelB.objects.create(field1='B1', field2='B2') c = ModelC.objects.create(field1='C1', field2='C2', field3='C3') reset_queries() - print ModelC.base_objects.all(); + print ModelC.base_objects.all() show_queries() if False: @@ -66,7 +72,8 @@ class Command(NoArgsCommand): a = ModelA.objects.create(field1=str(i % 100)) b = ModelB.objects.create(field1=str(i % 100), field2=str(i % 200)) c = ModelC.objects.create(field1=str(i % 100), field2=str(i % 200), field3=str(i % 300)) - if i % 100 == 0: print i + if i % 100 == 0: + print i f = print_timing(poly_sql_query, iterations=1000) f() @@ -81,7 +88,8 @@ class Command(NoArgsCommand): b = nModelB.objects.create(field1='B1', field2='B2') c = nModelC.objects.create(field1='C1', field2='C2', field3='C3') qs = ModelA.objects.raw("SELECT * from pexp_modela") - for o in list(qs): print o + for o in list(qs): + print o from django.db import connection, transaction from random import Random diff --git a/example/pexp/management/commands/pcmd.py b/example/pexp/management/commands/pcmd.py index b496ba7..e28608e 100644 --- a/example/pexp/management/commands/pcmd.py +++ b/example/pexp/management/commands/pcmd.py @@ -15,7 +15,11 @@ def reset_queries(): def show_queries(): - print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; connection.queries = [] + print + print 'QUERIES:', len(connection.queries) + pprint(connection.queries) + print + connection.queries = [] class Command(NoArgsCommand): diff --git a/example/pexp/management/commands/polybench.py b/example/pexp/management/commands/polybench.py index f056b26..7622178 100644 --- a/example/pexp/management/commands/polybench.py +++ b/example/pexp/management/commands/polybench.py @@ -21,7 +21,11 @@ def reset_queries(): def show_queries(): - print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; reset_queries() + print + print 'QUERIES:', len(connection.queries) + pprint(connection.queries) + print + reset_queries() import time @@ -39,7 +43,8 @@ def print_timing(func, message='', iterations=1): t2 = time.time() results.append((t2 - t1) * 1000.0) res_sum = 0 - for r in results: res_sum += r + for r in results: + res_sum += r median = res_sum / len(results) print '%s%-19s: %.0f ms, %i queries' % ( message, func.func_name, diff --git a/example/pexp/management/commands/polymorphic_create_test_data.py b/example/pexp/management/commands/polymorphic_create_test_data.py index bade771..89217db 100644 --- a/example/pexp/management/commands/polymorphic_create_test_data.py +++ b/example/pexp/management/commands/polymorphic_create_test_data.py @@ -15,7 +15,11 @@ def reset_queries(): def show_queries(): - print; print 'QUERIES:', len(connection.queries); pprint(connection.queries); print; connection.queries = [] + print + print 'QUERIES:', len(connection.queries) + pprint(connection.queries) + print + connection.queries = [] class Command(NoArgsCommand): diff --git a/runtests.py b/runtests.py index f536b43..f9517d2 100755 --- a/runtests.py +++ b/runtests.py @@ -19,15 +19,15 @@ sys.stderr.write('Using Django version {0} from {1}\n'.format( if not settings.configured: settings.configure( - DEBUG = True, - TEMPLATE_DEBUG = True, - DATABASES = { + DEBUG=True, + TEMPLATE_DEBUG=True, + DATABASES={ 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' } }, - TEMPLATE_LOADERS = ( + TEMPLATE_LOADERS=( 'django.template.loaders.app_directories.Loader', ), TEMPLATE_CONTEXT_PROCESSORS=( @@ -37,7 +37,7 @@ if not settings.configured: 'django.core.context_processors.request', ] ), - TEST_RUNNER = 'django.test.runner.DiscoverRunner' if django.VERSION >= (1,7) else 'django.test.simple.DjangoTestSuiteRunner', + TEST_RUNNER = 'django.test.runner.DiscoverRunner' if django.VERSION >= (1, 7) else 'django.test.simple.DjangoTestSuiteRunner', INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/setup.py b/setup.py index 3371cdc..64a126a 100755 --- a/setup.py +++ b/setup.py @@ -21,22 +21,22 @@ def find_version(*parts): setup( - name = 'django_polymorphic', - version = find_version('polymorphic', '__init__.py'), - license = 'BSD', + name='django_polymorphic', + version=find_version('polymorphic', '__init__.py'), + license='BSD', - description = 'Seamless Polymorphic Inheritance for Django Models', - long_description = read('README.rst'), - url = 'https://github.com/chrisglass/django_polymorphic', + description='Seamless Polymorphic Inheritance for Django Models', + long_description=read('README.rst'), + url='https://github.com/chrisglass/django_polymorphic', - author = 'Bert Constantin', - author_email = 'bert.constantin@gmx.de', + author='Bert Constantin', + author_email='bert.constantin@gmx.de', - maintainer = 'Christopher Glass', - maintainer_email = 'tribaal@gmail.com', + maintainer='Christopher Glass', + maintainer_email='tribaal@gmail.com', - packages = find_packages(), - package_data = { + packages=find_packages(), + package_data={ 'polymorphic': [ 'templates/admin/polymorphic/*.html', ], From b0657ef9c7aa5020969993f86033835d5a9040bc Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 29 Dec 2015 14:42:24 +0100 Subject: [PATCH 16/17] Fix reverse relation support for ___ filter operator --- docs/changelog.rst | 7 +++++++ polymorphic/query_translate.py | 26 ++++++++++++++++++++++---- polymorphic/tests.py | 13 +++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index be0fa8e..5de4115 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog ========== +Version 0.8.1 (2015-12-29) +-------------------------- + +* Fixed support for reverse relations for ``relname___field`` when the field starts with an ``_`` character. + Otherwise, the query will be interpreted as subclass lookup (``ClassName___field``). + + Version 0.8 (2015-12-28) ------------------------ diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index 98da463..3424eaf 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -4,11 +4,25 @@ """ from __future__ import absolute_import +import django from django.db import models from django.contrib.contenttypes.models import ContentType from django.db.models import Q, FieldDoesNotExist -from django.db.models.fields.related import RelatedField # Django 1.8 +from django.db.models.fields.related import RelatedField +if django.VERSION < (1, 6): + # There was no common base class in Django 1.5, mention all variants here. + from django.db.models.fields.related import RelatedObject, ManyToOneRel, ManyToManyRel + REL_FIELD_CLASSES = (RelatedField, RelatedObject, ManyToOneRel, ManyToManyRel) # Leaving GenericRel out. +elif django.VERSION < (1, 8): + # As of Django 1.6 there is a ForeignObjectRel. + from django.db.models.fields.related import ForeignObjectRel, RelatedObject + REL_FIELD_CLASSES = (RelatedField, ForeignObjectRel, RelatedObject) +else: + # As of Django 1.8 the base class serves everything. RelatedObject is gone. + from django.db.models.fields.related import ForeignObjectRel + REL_FIELD_CLASSES = (RelatedField, ForeignObjectRel) + from functools import reduce @@ -154,9 +168,13 @@ def translate_polymorphic_field_path(queryset_model, field_path): # Test whether it's actually a regular relation__ _fieldname (the field starting with an _) # so no tripple ClassName___field was intended. try: - # rel = (field_object, model, direct, m2m) - field = queryset_model._meta.get_field(classname) - if isinstance(field, RelatedField): + if django.VERSION >= (1, 8): + # This also retreives M2M relations now (including reverse foreign key relations) + field = queryset_model._meta.get_field(classname) + else: + field = queryset_model._meta.get_field_by_name(classname)[0] + + if isinstance(field, REL_FIELD_CLASSES): # 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. return field_path # No exception raised, field does exist. diff --git a/polymorphic/tests.py b/polymorphic/tests.py index b67f584..e1278b7 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -159,7 +159,8 @@ class ModelUnderRelParent(PolymorphicModel): class ModelUnderRelChild(PolymorphicModel): - parent = models.ForeignKey(ModelUnderRelParent) + parent = models.ForeignKey(ModelUnderRelParent, related_name='children') + _private2 = models.CharField(max_length=10) class MyManagerQuerySet(PolymorphicQuerySet): @@ -734,12 +735,20 @@ class PolymorphicTests(TestCase): def test_polymorphic___filter_field(self): p = ModelUnderRelParent.objects.create(_private=True, field1='AA') - ModelUnderRelChild.objects.create(parent=p) + ModelUnderRelChild.objects.create(parent=p, _private2=True) # The "___" filter should also parse to "parent" -> "_private" as fallback. objects = ModelUnderRelChild.objects.filter(parent___private=True) self.assertEqual(len(objects), 1) + def test_polymorphic___filter_reverse_field(self): + p = ModelUnderRelParent.objects.create(_private=True, field1='BB') + ModelUnderRelChild.objects.create(parent=p, _private2=True) + + # Also test for reverse relations + objects = ModelUnderRelParent.objects.filter(children___private2=True) + self.assertEqual(len(objects), 1) + def test_delete(self): self.create_model2abcd() From e9f549758f017046b980f90e8b576e15cd1b7909 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Tue, 29 Dec 2015 14:55:25 +0100 Subject: [PATCH 17/17] Bump version to 0.8.1 --- docs/conf.py | 4 ++-- polymorphic/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ced3b37..c0e277a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ copyright = u'2013, Bert Constantin, Chris Glass, Diederik van der Boor' # built documents. # # The short X.Y version. -version = '0.8' +version = '0.8.1' # The full version, including alpha/beta/rc tags. -release = '0.8' +release = '0.8.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 0b4632f..f9b34fe 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -7,7 +7,7 @@ This code and affiliated files are (C) by Bert Constantin and individual contrib Please see LICENSE and AUTHORS for more information. """ # See PEP 440 (https://www.python.org/dev/peps/pep-0440/) -__version__ = "0.8" +__version__ = "0.8.1" # Monkey-patch Django < 1.5 to allow ContentTypes for proxy models.