├── __init__.py ├── pexp ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── polymorphic_create_test_data.py │ │ ├── pcmd.py │ │ ├── polybench.py │ │ └── p2cmd.py ├── views.py ├── tests.py ├── dumpdata_test_correct_output.txt └── models.py ├── polymorphic ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── polymorphic_dumpdata.py ├── models.py ├── __init__.py ├── manager.py ├── compatibility_tools.py ├── tools_for_tests.py ├── showfields.py ├── polymorphic_model.py ├── query_translate.py ├── base.py ├── query.py └── tests.py ├── dbreset ├── AUTHORS ├── .gitignore ├── setup.py ├── diffmanagement ├── rst-to-html.py ├── test_dumpdata ├── manage.py ├── LICENSE ├── test_all_versions ├── settings.py ├── rst.css ├── README.rst ├── CHANGES.rst ├── README.html ├── CHANGES.html └── DOCS.rst /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pexp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pexp/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pexp/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polymorphic/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pexp/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /polymorphic/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dbreset: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm /var/tmp/django-polymorphic-test-db.sqlite3 4 | ./manage.py syncdb 5 | 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | django_polymorphic was created by Bert Constantin in 2009/2010. 2 | 3 | Andrew Ingram contributed setup.py 4 | Charles Leifer contributed Python 2.4 compatibility 5 | -------------------------------------------------------------------------------- /polymorphic/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | IMPORTANT: 4 | 5 | The models.py module is not used anymore. 6 | Please use the following import method in your apps: 7 | 8 | from polymorphic import PolymorphicModel, ... 9 | 10 | """ 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hg 2 | *.svn 3 | *.pyc 4 | *~ 5 | .project 6 | .pydevproject 7 | .settings 8 | nbproject 9 | 10 | tmp 11 | libraries-local 12 | 13 | pushgit 14 | pushhg 15 | pushreg 16 | pbackup 17 | mcmd.py 18 | dbconfig_local.py 19 | diffmanagement 20 | scrapbook.py 21 | 22 | pip-log.txt 23 | build 24 | ppreadme.py 25 | ppdocs.py 26 | common.css 27 | screen.css 28 | 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name = 'django_polymorphic', 5 | version = '0.2', 6 | description = 'Seamless Polymorphic Inheritance for Django Models', 7 | author = 'Bert Constantin', 8 | author_email = 'bert.constantin@gmx.de', 9 | url = 'http://bserve.webhop.org/wiki/django_polymorphic', 10 | packages = [ 'polymorphic' ], 11 | ) 12 | -------------------------------------------------------------------------------- /polymorphic/management/commands/polymorphic_dumpdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | polymorphic_dumpdata has been disabled since it's no longer needed 3 | (this is now handled by polymorphic.base.PolymorphicModelBase). 4 | """ 5 | 6 | assert False, """ 7 | ERROR: The management command polymorphic_dumpdata is no longer supported or needed. 8 | Please use the standard Django dumpdata management command instead! 9 | """ 10 | -------------------------------------------------------------------------------- /diffmanagement: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | colordiff -u -w libraries-local/django-versions/django1.1/core/management/commands/dumpdata.py polymorphic/management/commands/polymorphic_dumpdata_11.py 4 | 5 | colordiff -u -w libraries-local/django-versions/django1.2/core/management/commands/dumpdata.py polymorphic/management/commands/polymorphic_dumpdata_12.py 6 | 7 | colordiff -u -w libraries-local/django-versions/django1.3/core/management/commands/dumpdata.py polymorphic/management/commands/polymorphic_dumpdata_13.py 8 | 9 | -------------------------------------------------------------------------------- /pexp/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /rst-to-html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys,os 4 | 5 | dopart = None 6 | if len(sys.argv)>1: dopart = sys.argv[1] 7 | noshow = 'noshow' in sys.argv 8 | 9 | css='--stylesheet-path=rst.css' 10 | 11 | def conv(name): 12 | print 'convert',name 13 | if noshow: 14 | os.system('rst2html.py '+css+' %s.rst >%s.html' % (name, name) ) 15 | else: 16 | os.system('rst2html.py '+css+' %s.rst >%s.html ; firefox %s.html' % (name, name, name) ) 17 | 18 | if not dopart or dopart=='1': conv('DOCS') 19 | if not dopart or dopart=='2': conv('README') 20 | if not dopart or dopart=='3': conv('CHANGES') 21 | 22 | sys.exit() 23 | 24 | 25 | -------------------------------------------------------------------------------- /test_dumpdata: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -f /var/tmp/django-polymorphic-test-db.sqlite3 4 | rm -f /ram/django-polymorphic-test-db.sqlite3 5 | 6 | TMPFILE=/tmp/django-polymorphic-test.dump 7 | 8 | PYCMD="python$1" 9 | 10 | echo 11 | echo "#####################################################################" 12 | echo "### Testing dumpdata" 13 | echo 14 | 15 | $PYCMD ./manage.py syncdb 16 | $PYCMD ./manage.py polymorphic_create_test_data 17 | 18 | $PYCMD ./manage.py dumpdata --indent=4 pexp >$TMPFILE 19 | 20 | if ! diff -w $TMPFILE pexp/dumpdata_test_correct_output.txt ; then 21 | echo "#####################################################################" 22 | echo "ERROR: test_dumpdata failed!" 23 | exit 10 24 | fi 25 | echo "#####################################################################" 26 | echo 'SUCCESS!' 27 | 28 | -------------------------------------------------------------------------------- /polymorphic/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Seamless Polymorphic Inheritance for Django Models 4 | 5 | Copyright: 6 | This code and affiliated files are (C) by Bert Constantin and individual contributors. 7 | Please see LICENSE and AUTHORS for more information. 8 | """ 9 | 10 | from polymorphic_model import PolymorphicModel 11 | from manager import PolymorphicManager 12 | from query import PolymorphicQuerySet 13 | from query_translate import translate_polymorphic_Q_object 14 | from showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent 15 | from showfields import ShowFields, ShowFieldTypes, ShowFieldsAndTypes # import old names for compatibility 16 | 17 | 18 | VERSION = (1, 0 , 0, 'beta') 19 | 20 | def get_version(): 21 | version = '%s.%s' % VERSION[0:2] 22 | if VERSION[2]: 23 | version += '.%s' % VERSION[2] 24 | if VERSION[3]: 25 | version += ' %s' % VERSION[3] 26 | return version -------------------------------------------------------------------------------- /pexp/dumpdata_test_correct_output.txt: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "pexp.project", 5 | "fields": { 6 | "topic": "John's gathering", 7 | "polymorphic_ctype": 2 8 | } 9 | }, 10 | { 11 | "pk": 2, 12 | "model": "pexp.project", 13 | "fields": { 14 | "topic": "Sculpting with Tim", 15 | "polymorphic_ctype": 3 16 | } 17 | }, 18 | { 19 | "pk": 3, 20 | "model": "pexp.project", 21 | "fields": { 22 | "topic": "Swallow Aerodynamics", 23 | "polymorphic_ctype": 4 24 | } 25 | }, 26 | { 27 | "pk": 2, 28 | "model": "pexp.artproject", 29 | "fields": { 30 | "artist": "T. Turner" 31 | } 32 | }, 33 | { 34 | "pk": 3, 35 | "model": "pexp.researchproject", 36 | "fields": { 37 | "supervisor": "Dr. Winter" 38 | } 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /pexp/management/commands/polymorphic_create_test_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module is a scratchpad for general development, testing & debugging 4 | """ 5 | 6 | from django.core.management.base import NoArgsCommand 7 | from django.db.models import connection 8 | from pprint import pprint 9 | import settings 10 | 11 | from pexp.models import * 12 | 13 | def reset_queries(): 14 | connection.queries=[] 15 | 16 | def show_queries(): 17 | print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; connection.queries=[] 18 | 19 | class Command(NoArgsCommand): 20 | help = "" 21 | 22 | def handle_noargs(self, **options): 23 | print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH 24 | print 25 | 26 | Project.objects.all().delete() 27 | o=Project.objects.create(topic="John's gathering") 28 | o=ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") 29 | o=ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") 30 | print Project.objects.all() 31 | print 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Prepend project subdirectory 'libraries-local' to sys.path. 4 | # This allows us to use/test any version of Django 5 | # (e.g. Django 1.2 subversion) or any other packages/libraries. 6 | import sys, os 7 | project_path = os.path.dirname(os.path.abspath(__file__)) 8 | libs_local_path = os.path.join(project_path, 'libraries-local') 9 | if libs_local_path not in sys.path: sys.path.insert(1, libs_local_path) 10 | 11 | sys.stderr.write( 'using Python version: %s\n' % sys.version[:5]) 12 | 13 | import django 14 | sys.stderr.write( 'using Django version: %s, from %s\n' % ( 15 | django.get_version(), 16 | os.path.dirname(os.path.abspath(django.__file__))) ) 17 | 18 | # vanilla Django manage.py from here on: 19 | 20 | from django.core.management import execute_manager 21 | try: 22 | import settings # Assumed to be in the same directory. 23 | except ImportError: 24 | import sys 25 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 26 | sys.exit(1) 27 | 28 | if __name__ == "__main__": 29 | execute_manager(settings) 30 | -------------------------------------------------------------------------------- /pexp/management/commands/pcmd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module is a scratchpad for general development, testing & debugging. 4 | """ 5 | 6 | from django.core.management.base import NoArgsCommand 7 | from django.db.models import connection 8 | from pprint import pprint 9 | import settings 10 | 11 | from pexp.models import * 12 | 13 | def reset_queries(): 14 | connection.queries=[] 15 | 16 | def show_queries(): 17 | print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; connection.queries=[] 18 | 19 | class Command(NoArgsCommand): 20 | help = "" 21 | 22 | def handle_noargs(self, **options): 23 | print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH 24 | print 25 | 26 | Project.objects.all().delete() 27 | a=Project.objects.create(topic="John's gathering") 28 | b=ArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") 29 | c=ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") 30 | print Project.objects.all() 31 | print 32 | 33 | ModelA.objects.all().delete() 34 | a=ModelA.objects.create(field1='A1') 35 | b=ModelB.objects.create(field1='B1', field2='B2') 36 | c=ModelC.objects.create(field1='C1', field2='C2', field3='C3') 37 | print ModelA.objects.all() 38 | print 39 | 40 | -------------------------------------------------------------------------------- /polymorphic/manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ PolymorphicManager 3 | Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic 4 | """ 5 | 6 | from django.db import models 7 | from query import PolymorphicQuerySet 8 | 9 | class PolymorphicManager(models.Manager): 10 | """ 11 | Manager for PolymorphicModel 12 | 13 | Usually not explicitly needed, except if a custom manager or 14 | a custom queryset class is to be used. 15 | """ 16 | use_for_related_fields = True 17 | 18 | def __init__(self, queryset_class=None, *args, **kwrags): 19 | if not queryset_class: self.queryset_class = PolymorphicQuerySet 20 | else: self.queryset_class = queryset_class 21 | super(PolymorphicManager, self).__init__(*args, **kwrags) 22 | 23 | def get_query_set(self): 24 | return self.queryset_class(self.model) 25 | 26 | # Proxy all unknown method calls to the queryset, so that its members are 27 | # directly accessible as PolymorphicModel.objects.* 28 | # The advantage of this method is that not yet known member functions of derived querysets will be proxied as well. 29 | # We exclude any special functions (__) from this automatic proxying. 30 | def __getattr__(self, name): 31 | if name.startswith('__'): return super(PolymorphicManager, self).__getattr__(self, name) 32 | return getattr(self.get_query_set(), name) 33 | 34 | def __unicode__(self): 35 | return self.__class__.__name__ + ' (PolymorphicManager) using ' + self.queryset_class.__name__ 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 or later by the individual contributors. 2 | Please see the AUTHORS file. 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /pexp/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | 5 | from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet 6 | from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent 7 | 8 | class Project(ShowFieldContent, PolymorphicModel): 9 | topic = models.CharField(max_length=30) 10 | class ArtProject(Project): 11 | artist = models.CharField(max_length=30) 12 | class ResearchProject(Project): 13 | supervisor = models.CharField(max_length=30) 14 | 15 | class ModelA(ShowFieldTypeAndContent, PolymorphicModel): 16 | field1 = models.CharField(max_length=10) 17 | class ModelB(ModelA): 18 | field2 = models.CharField(max_length=10) 19 | class ModelC(ModelB): 20 | field3 = models.CharField(max_length=10) 21 | 22 | class nModelA(models.Model): 23 | field1 = models.CharField(max_length=10) 24 | class nModelB(nModelA): 25 | field2 = models.CharField(max_length=10) 26 | class nModelC(nModelB): 27 | field3 = models.CharField(max_length=10) 28 | 29 | # for Django 1.2+, test models with same names in different apps 30 | # (the other models with identical names are in polymorphic/tests.py) 31 | from django import VERSION as django_VERSION 32 | if not (django_VERSION[0]<=1 and django_VERSION[1]<=1): 33 | class Model2A(PolymorphicModel): 34 | field1 = models.CharField(max_length=10) 35 | class Model2B(Model2A): 36 | field2 = models.CharField(max_length=10) 37 | class Model2C(Model2B): 38 | field3 = models.CharField(max_length=10) 39 | 40 | try: from polymorphic.test_tools import UUIDField 41 | except: pass 42 | if 'UUIDField' in globals(): 43 | class UUIDModelA(ShowFieldTypeAndContent, PolymorphicModel): 44 | uuid_primary_key = UUIDField(primary_key = True) 45 | field1 = models.CharField(max_length=10) 46 | class UUIDModelB(UUIDModelA): 47 | field2 = models.CharField(max_length=10) 48 | class UUIDModelC(UUIDModelB): 49 | field3 = models.CharField(max_length=10) 50 | -------------------------------------------------------------------------------- /test_all_versions: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this test script runs "./manage.py test" for 4 | # all supported python versions (2.4, 2.5, 2.6) 5 | # and all supported Django versions (1.1, 1.2, 1.3) 6 | 7 | # it needs symbolic links named "django1.1" and "django1.2" etc. in: 8 | # libraries-local/django-versions 9 | # which point to the respective django versions 10 | 11 | cd libraries-local 12 | rm -f django-orig 13 | if test -e django ; then mv django django-orig ; fi 14 | cd .. 15 | 16 | function restore_django { 17 | echo "### restoring original libraries-local/django" 18 | cd libraries-local 19 | if test -e django-orig ; then mv django-orig django ; fi 20 | cd . 21 | } 22 | 23 | function test_python_version { 24 | echo ; echo ; echo 25 | echo "#########################################################################" 26 | echo "### Testing Python $1, Django $2" 27 | echo "#########################################################################" 28 | echo 29 | 30 | if which python$1 ; then 31 | if ! python$1 manage.py test polymorphic; then 32 | echo ERROR 33 | restore_django 34 | exit 10 35 | fi 36 | if ! ./test_dumpdata $1 ; then 37 | echo ERROR 38 | restore_django 39 | exit 10 40 | fi 41 | else 42 | echo 43 | echo "### python $1 is not installed!" 44 | echo 45 | fi 46 | } 47 | 48 | function test_all_python_versions { 49 | test_python_version 2.4 $1 50 | test_python_version 2.5 $1 51 | test_python_version 2.6 $1 52 | } 53 | 54 | function test_django_version { 55 | if ! test -e libraries-local/django-versions/django$1 ; then 56 | echo 57 | echo "### django $1 is not installed!" 58 | echo 59 | return 60 | fi 61 | cd libraries-local 62 | rm -f django 63 | ln -s django-versions/django$1 django 64 | cd .. 65 | test_all_python_versions $1 66 | } 67 | 68 | test_django_version 1.1 69 | test_django_version 1.2 70 | test_django_version 1.3 71 | 72 | restore_django 73 | 74 | -------------------------------------------------------------------------------- /polymorphic/compatibility_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Compatibility layer for Python 2.4 4 | ================================== 5 | 6 | Currently implements: 7 | 8 | + collections.defaultdict 9 | + compat_partition (compatibility replacement for str.partition) 10 | 11 | """ 12 | try: 13 | assert False 14 | from collections import defaultdict 15 | except: 16 | class defaultdict(dict): 17 | def __init__(self, default_factory=None, *a, **kw): 18 | if (default_factory is not None and 19 | not hasattr(default_factory, '__call__')): 20 | raise TypeError('first argument must be callable') 21 | dict.__init__(self, *a, **kw) 22 | self.default_factory = default_factory 23 | def __getitem__(self, key): 24 | try: 25 | return dict.__getitem__(self, key) 26 | except KeyError: 27 | return self.__missing__(key) 28 | def __missing__(self, key): 29 | if self.default_factory is None: 30 | raise KeyError(key) 31 | self[key] = value = self.default_factory() 32 | return value 33 | def __reduce__(self): 34 | if self.default_factory is None: 35 | args = tuple() 36 | else: 37 | args = self.default_factory, 38 | return type(self), args, None, None, self.items() 39 | def copy(self): 40 | return self.__copy__() 41 | def __copy__(self): 42 | return type(self)(self.default_factory, self) 43 | def __deepcopy__(self, memo): 44 | import copy 45 | return type(self)(self.default_factory, 46 | copy.deepcopy(self.items())) 47 | def __repr__(self): 48 | return 'defaultdict(%s, %s)' % (self.default_factory, dict.__repr__(self)) 49 | 50 | 51 | if getattr(str,'partition',None): 52 | def compat_partition(s,sep): return s.partition(sep) 53 | else: 54 | """ from: 55 | http://mail.python.org/pipermail/python-dev/2005-September/055962.html 56 | """ 57 | def compat_partition(s, sep, at_sep=1): 58 | """ Returns a three element tuple, (head, sep, tail) where: 59 | 60 | head + sep + tail == s 61 | sep == '' or sep is t 62 | bool(sep) == (t in s) # sep indicates if the string was found 63 | """ 64 | if not isinstance(sep, basestring) or not sep: 65 | raise ValueError('partititon argument must be a non-empty string') 66 | if at_sep == 0: 67 | result = ('', '', s) 68 | else: 69 | if at_sep > 0: 70 | parts = s.split(sep, at_sep) 71 | if len(parts) <= at_sep: 72 | result = (s, '', '') 73 | else: 74 | result = (sep.join(parts[:at_sep]), sep, parts[at_sep]) 75 | else: 76 | parts = s.rsplit(sep, at_sep) 77 | if len(parts) <= at_sep: 78 | result = ('', '', s) 79 | else: 80 | result = (parts[0], sep, sep.join(parts[1:])) 81 | assert len(result) == 3 82 | assert ''.join(result) == s 83 | assert result[1] == '' or result[1] is sep 84 | return result 85 | 86 | -------------------------------------------------------------------------------- /pexp/management/commands/polybench.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module is a scratchpad for general development, testing & debugging 4 | """ 5 | 6 | from django.core.management.base import NoArgsCommand 7 | from django.db.models import connection 8 | from pprint import pprint 9 | import settings 10 | import sys 11 | from pexp.models import * 12 | 13 | num_objects=1000 14 | 15 | def reset_queries(): 16 | connection.queries=[] 17 | 18 | def show_queries(): 19 | print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; reset_queries() 20 | 21 | import time 22 | 23 | ################################################################################### 24 | ### benchmark wrappers 25 | 26 | def print_timing(func, message='', iterations=1): 27 | def wrapper(*arg): 28 | results=[] 29 | reset_queries() 30 | for i in xrange(iterations): 31 | t1 = time.time() 32 | x = func(*arg) 33 | t2 = time.time() 34 | results.append((t2-t1)*1000.0) 35 | res_sum=0 36 | for r in results: res_sum +=r 37 | median = res_sum / len(results) 38 | print '%s%-19s: %.0f ms, %i queries' % ( 39 | message,func.func_name, 40 | median, 41 | len(connection.queries)/len(results) 42 | ) 43 | sys.stdout.flush() 44 | return wrapper 45 | 46 | def run_vanilla_any_poly(func, iterations=1): 47 | f=print_timing(func,' ', iterations) 48 | f(nModelC) 49 | f=print_timing(func,'poly ', iterations) 50 | f(ModelC) 51 | 52 | 53 | ################################################################################### 54 | ### benchmarks 55 | 56 | def bench_create(model): 57 | for i in xrange(num_objects): 58 | model.objects.create(field1='abc'+str(i), field2='abcd'+str(i), field3='abcde'+str(i)) 59 | #print 'count:',model.objects.count() 60 | 61 | def bench_load1(model): 62 | for o in model.objects.all(): 63 | pass 64 | 65 | def bench_load1_short(model): 66 | for i in xrange(num_objects/100): 67 | for o in model.objects.all()[:100]: 68 | pass 69 | 70 | def bench_load2(model): 71 | for o in model.objects.all(): 72 | f1=o.field1 73 | f2=o.field2 74 | f3=o.field3 75 | 76 | def bench_load2_short(model): 77 | for i in xrange(num_objects/100): 78 | for o in model.objects.all()[:100]: 79 | f1=o.field1 80 | f2=o.field2 81 | f3=o.field3 82 | 83 | def bench_delete(model): 84 | model.objects.all().delete() 85 | 86 | ################################################################################### 87 | ### Command 88 | 89 | class Command(NoArgsCommand): 90 | help = "" 91 | 92 | def handle_noargs(self, **options): 93 | print 'polybench - sqlite test db is stored in:',settings.SQLITE_DB_PATH 94 | 95 | func_list = [ 96 | ( bench_delete, 1 ), 97 | ( bench_create, 1 ), 98 | ( bench_load1, 5 ), 99 | ( bench_load1_short, 5 ), 100 | ( bench_load2, 5 ), 101 | ( bench_load2_short, 5 ) 102 | ] 103 | for f,iterations in func_list: 104 | run_vanilla_any_poly(f,iterations=iterations) 105 | 106 | print 107 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for polymorphic_demo project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@domain.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | import django 13 | import os 14 | 15 | if os.path.ismount('/ram'): 16 | SQLITE_DB_PATH = '/ram/django-polymorphic-test-db.sqlite3' 17 | else: 18 | SQLITE_DB_PATH = '/var/tmp/django-polymorphic-test-db.sqlite3' 19 | 20 | if django.VERSION[:2][0]>=1 and django.VERSION[:2][1]>=3: 21 | DATABASES = { 22 | 'default': { 23 | 'ENGINE': 'django.db.backends.sqlite3', 24 | 'NAME': SQLITE_DB_PATH 25 | } 26 | } 27 | else: 28 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 29 | DATABASE_NAME = SQLITE_DB_PATH # Or path to database file if using sqlite3. 30 | DATABASE_USER = '' # Not used with sqlite3. 31 | DATABASE_PASSWORD = '' # Not used with sqlite3. 32 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. 33 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 34 | 35 | # Local time zone for this installation. Choices can be found here: 36 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 37 | # although not all choices may be available on all operating systems. 38 | # If running in a Windows environment this must be set to the same as your 39 | # system time zone. 40 | TIME_ZONE = 'America/Chicago' 41 | 42 | # Language code for this installation. All choices can be found here: 43 | # http://www.i18nguy.com/unicode/language-identifiers.html 44 | LANGUAGE_CODE = 'en-us' 45 | 46 | SITE_ID = 1 47 | 48 | # If you set this to False, Django will make some optimizations so as not 49 | # to load the internationalization machinery. 50 | USE_I18N = True 51 | 52 | # Absolute path to the directory that holds media. 53 | # Example: "/home/media/media.lawrence.com/" 54 | MEDIA_ROOT = '' 55 | 56 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 57 | # trailing slash if there is a path component (optional in other cases). 58 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 59 | MEDIA_URL = '' 60 | 61 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 62 | # trailing slash. 63 | # Examples: "http://foo.com/media/", "/media/". 64 | ADMIN_MEDIA_PREFIX = '/media/' 65 | 66 | # Make this unique, and don't share it with anybody. 67 | SECRET_KEY = 'nk=c&k+c&#+)8557)%&0auysdd3g^sfq6@rw8_x1k8)-p@y)!(' 68 | 69 | # List of callables that know how to import templates from various sources. 70 | TEMPLATE_LOADERS = ( 71 | 'django.template.loaders.filesystem.load_template_source', 72 | 'django.template.loaders.app_directories.load_template_source', 73 | # 'django.template.loaders.eggs.load_template_source', 74 | ) 75 | 76 | MIDDLEWARE_CLASSES = ( 77 | 'django.middleware.common.CommonMiddleware', 78 | 'django.contrib.sessions.middleware.SessionMiddleware', 79 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 80 | ) 81 | 82 | ROOT_URLCONF = '' 83 | 84 | TEMPLATE_DIRS = ( 85 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 86 | # Always use forward slashes, even on Windows. 87 | # Don't forget to use absolute paths, not relative paths. 88 | ) 89 | 90 | INSTALLED_APPS = ( 91 | #'django.contrib.auth', 92 | 'django.contrib.contenttypes', 93 | #'django.contrib.sessions', 94 | #'django.contrib.sites', 95 | 'polymorphic', # only needed if you want to use polymorphic_dumpdata 96 | 'pexp', # this Django app is for testing and experimentation; not needed otherwise 97 | ) 98 | -------------------------------------------------------------------------------- /pexp/management/commands/p2cmd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module is a scratchpad for general development, testing & debugging 4 | Well, even more so than pcmd.py. You best ignore p2cmd.py. 5 | """ 6 | import uuid 7 | 8 | from django.core.management.base import NoArgsCommand 9 | from django.db.models import connection 10 | from pprint import pprint 11 | import settings 12 | import time,sys 13 | 14 | from pexp.models import * 15 | 16 | def reset_queries(): 17 | connection.queries=[] 18 | 19 | def show_queries(): 20 | print; print 'QUERIES:',len(connection.queries); pprint(connection.queries); print; connection.queries=[] 21 | 22 | def print_timing(func, message='', iterations=1): 23 | def wrapper(*arg): 24 | results=[] 25 | reset_queries() 26 | for i in xrange(iterations): 27 | t1 = time.time() 28 | x = func(*arg) 29 | t2 = time.time() 30 | results.append((t2-t1)*1000.0) 31 | res_sum=0 32 | for r in results: res_sum +=r 33 | median = res_sum / len(results) 34 | print '%s%-19s: %.4f ms, %i queries (%i times)' % ( 35 | message,func.func_name, 36 | res_sum, 37 | len(connection.queries), 38 | iterations 39 | ) 40 | sys.stdout.flush() 41 | return wrapper 42 | 43 | class Command(NoArgsCommand): 44 | help = "" 45 | 46 | def handle_noargs(self, **options): 47 | print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH 48 | print 49 | 50 | if False: 51 | ModelA.objects.all().delete() 52 | a=ModelA.objects.create(field1='A1') 53 | b=ModelB.objects.create(field1='B1', field2='B2') 54 | c=ModelC.objects.create(field1='C1', field2='C2', field3='C3') 55 | reset_queries() 56 | print ModelC.base_objects.all(); 57 | show_queries() 58 | 59 | if False: 60 | ModelA.objects.all().delete() 61 | for i in xrange(1000): 62 | a=ModelA.objects.create(field1=str(i%100)) 63 | b=ModelB.objects.create(field1=str(i%100), field2=str(i%200)) 64 | c=ModelC.objects.create(field1=str(i%100), field2=str(i%200), field3=str(i%300)) 65 | if i%100==0: print i 66 | 67 | f=print_timing(poly_sql_query,iterations=1000) 68 | f() 69 | 70 | f=print_timing(poly_sql_query2,iterations=1000) 71 | f() 72 | 73 | return 74 | 75 | nModelA.objects.all().delete() 76 | a=nModelA.objects.create(field1='A1') 77 | b=nModelB.objects.create(field1='B1', field2='B2') 78 | c=nModelC.objects.create(field1='C1', field2='C2', field3='C3') 79 | qs=ModelA.objects.raw("SELECT * from pexp_modela") 80 | for o in list(qs): print o 81 | 82 | from django.db import connection, transaction 83 | from random import Random 84 | rnd=Random() 85 | 86 | def poly_sql_query(): 87 | cursor = connection.cursor() 88 | cursor.execute(""" 89 | SELECT id, pexp_modela.field1, pexp_modelb.field2, pexp_modelc.field3 90 | FROM pexp_modela 91 | LEFT OUTER JOIN pexp_modelb 92 | ON pexp_modela.id = pexp_modelb.modela_ptr_id 93 | LEFT OUTER JOIN pexp_modelc 94 | ON pexp_modelb.modela_ptr_id = pexp_modelc.modelb_ptr_id 95 | WHERE pexp_modela.field1=%i 96 | ORDER BY pexp_modela.id 97 | """ % rnd.randint(0,100) ) 98 | #row=cursor.fetchone() 99 | return 100 | 101 | def poly_sql_query2(): 102 | cursor = connection.cursor() 103 | cursor.execute(""" 104 | SELECT id, pexp_modela.field1 105 | FROM pexp_modela 106 | WHERE pexp_modela.field1=%i 107 | ORDER BY pexp_modela.id 108 | """ % rnd.randint(0,100) ) 109 | #row=cursor.fetchone() 110 | return 111 | -------------------------------------------------------------------------------- /rst.css: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, 2 | #table-of-contents 3 | { 4 | color: #47c; 5 | } 6 | h1 { padding-top: 15px; } 7 | h2 { padding-top: 10px; } 8 | h3 { padding-top: 7px; } 9 | 10 | a:hover { border-bottom: 1px solid #0066cc; } 11 | a {color: #0066cc; text-decoration: none;} 12 | 13 | li { 14 | padding-top: 5px; 15 | padding-bottom: 5px; 16 | } 17 | 18 | tt { 19 | color: #080; 20 | } 21 | 22 | blockquote tt { 23 | color: #000 24 | } 25 | 26 | .first { 27 | margin-top: 0 } 28 | 29 | .last { 30 | margin-bottom: 0 } 31 | 32 | /* 33 | a.toc-backref { 34 | text-decoration: none ; 35 | color: black } 36 | */ 37 | 38 | dd { 39 | margin-bottom: 0.5em } 40 | 41 | div.abstract { 42 | margin: 2em 5em } 43 | 44 | div.abstract p.topic-title { 45 | font-weight: bold ; 46 | text-align: center } 47 | 48 | div.attention, div.caution, div.danger, div.error, div.hint, 49 | div.important, div.note, div.tip, div.warning { 50 | margin: 2em ; 51 | border: medium outset ; 52 | padding: 1em } 53 | 54 | div.attention p.admonition-title, div.caution p.admonition-title, 55 | div.danger p.admonition-title, div.error p.admonition-title, 56 | div.warning p.admonition-title { 57 | color: red ; 58 | font-weight: bold ; 59 | font-family: sans-serif } 60 | 61 | div.hint p.admonition-title, div.important p.admonition-title, 62 | div.note p.admonition-title, div.tip p.admonition-title { 63 | font-weight: bold ; 64 | font-family: sans-serif } 65 | 66 | div.dedication { 67 | margin: 2em 5em ; 68 | text-align: center ; 69 | font-style: italic } 70 | 71 | div.dedication p.topic-title { 72 | font-weight: bold ; 73 | font-style: normal } 74 | 75 | div.figure { 76 | margin-left: 2em } 77 | 78 | div.footer, div.header { 79 | font-size: smaller } 80 | 81 | div.system-messages { 82 | margin: 5em } 83 | 84 | div.system-messages h1 { 85 | color: red } 86 | 87 | div.system-message { 88 | border: medium outset ; 89 | padding: 1em } 90 | 91 | div.system-message p.system-message-title { 92 | color: red ; 93 | font-weight: bold } 94 | 95 | div.topic { 96 | margin: 2em } 97 | 98 | h1.title { 99 | text-align: center } 100 | 101 | h2.subtitle { 102 | text-align: center } 103 | 104 | hr { 105 | width: 75% } 106 | 107 | ol.simple, ul.simple { 108 | margin-bottom: 1em } 109 | 110 | ol.arabic { 111 | list-style: decimal } 112 | 113 | ol.loweralpha { 114 | list-style: lower-alpha } 115 | 116 | ol.upperalpha { 117 | list-style: upper-alpha } 118 | 119 | ol.lowerroman { 120 | list-style: lower-roman } 121 | 122 | ol.upperroman { 123 | list-style: upper-roman } 124 | 125 | p.caption { 126 | font-style: italic } 127 | 128 | p.credits { 129 | font-style: italic ; 130 | font-size: smaller } 131 | 132 | p.label { 133 | white-space: nowrap } 134 | 135 | p.topic-title { 136 | font-weight: bold } 137 | 138 | pre.address { 139 | margin-bottom: 0 ; 140 | margin-top: 0 ; 141 | font-family: serif ; 142 | font-size: 100% } 143 | 144 | pre.line-block { 145 | font-family: serif ; 146 | font-size: 100% } 147 | 148 | pre.literal-block, pre.doctest-block { 149 | margin-left: 2em ; 150 | margin-right: 2em ; 151 | background-color: #eeeeee } 152 | 153 | span.classifier { 154 | font-family: sans-serif ; 155 | font-style: oblique } 156 | 157 | span.classifier-delimiter { 158 | font-family: sans-serif ; 159 | font-weight: bold } 160 | 161 | span.interpreted { 162 | font-family: sans-serif } 163 | 164 | span.option-argument { 165 | font-style: italic } 166 | 167 | span.pre { 168 | white-space: pre } 169 | 170 | span.problematic { 171 | color: red } 172 | 173 | table { 174 | margin-top: 0.5em ; 175 | margin-bottom: 0.5em } 176 | 177 | table.citation { 178 | border-left: solid thin gray ; 179 | padding-left: 0.5ex } 180 | 181 | table.docinfo { 182 | margin: 2em 4em } 183 | 184 | table.footnote { 185 | border-left: solid thin black ; 186 | padding-left: 0.5ex } 187 | 188 | td, th { 189 | padding-left: 0.5em ; 190 | padding-right: 0.5em ; 191 | vertical-align: top } 192 | 193 | th.docinfo-name, th.field-name { 194 | font-weight: bold ; 195 | text-align: left ; 196 | white-space: nowrap } 197 | 198 | h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { 199 | font-size: 100% } 200 | 201 | tt, pre.literal-block, pre.doctest-block { 202 | font-size: 115%; 203 | line-height: 150% } 204 | 205 | ul.auto-toc { 206 | list-style-type: none } 207 | -------------------------------------------------------------------------------- /polymorphic/tools_for_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | #################################################################### 4 | 5 | import uuid 6 | 7 | from django.forms.util import ValidationError 8 | from django import forms 9 | from django.db import models 10 | from django.utils.encoding import smart_unicode 11 | from django.utils.translation import ugettext_lazy 12 | 13 | class UUIDVersionError(Exception): 14 | pass 15 | 16 | class UUIDField(models.CharField): 17 | """Encode and stores a Python uuid.UUID in a manner that is appropriate 18 | for the given datatabase that we are using. 19 | 20 | For sqlite3 or MySQL we save it as a 36-character string value 21 | For PostgreSQL we save it as a uuid field 22 | 23 | This class supports type 1, 2, 4, and 5 UUID's. 24 | """ 25 | __metaclass__ = models.SubfieldBase 26 | 27 | _CREATE_COLUMN_TYPES = { 28 | 'postgresql_psycopg2': 'uuid', 29 | 'postgresql': 'uuid' 30 | } 31 | 32 | def __init__(self, verbose_name=None, name=None, auto=True, version=1, node=None, clock_seq=None, namespace=None, **kwargs): 33 | """Contruct a UUIDField. 34 | 35 | @param verbose_name: Optional verbose name to use in place of what 36 | Django would assign. 37 | @param name: Override Django's name assignment 38 | @param auto: If True, create a UUID value if one is not specified. 39 | @param version: By default we create a version 1 UUID. 40 | @param node: Used for version 1 UUID's. If not supplied, then the uuid.getnode() function is called to obtain it. This can be slow. 41 | @param clock_seq: Used for version 1 UUID's. If not supplied a random 14-bit sequence number is chosen 42 | @param namespace: Required for version 3 and version 5 UUID's. 43 | @param name: Required for version4 and version 5 UUID's. 44 | 45 | See Also: 46 | - Python Library Reference, section 18.16 for more information. 47 | - RFC 4122, "A Universally Unique IDentifier (UUID) URN Namespace" 48 | 49 | If you want to use one of these as a primary key for a Django 50 | model, do this:: 51 | id = UUIDField(primary_key=True) 52 | This will currently I{not} work with Jython because PostgreSQL support 53 | in Jython is not working for uuid column types. 54 | """ 55 | self.max_length = 36 56 | kwargs['max_length'] = self.max_length 57 | if auto: 58 | kwargs['blank'] = True 59 | kwargs.setdefault('editable', False) 60 | 61 | self.auto = auto 62 | self.version = version 63 | if version==1: 64 | self.node, self.clock_seq = node, clock_seq 65 | elif version==3 or version==5: 66 | self.namespace, self.name = namespace, name 67 | 68 | super(UUIDField, self).__init__(verbose_name=verbose_name, 69 | name=name, **kwargs) 70 | 71 | def create_uuid(self): 72 | if not self.version or self.version==4: 73 | return uuid.uuid4() 74 | elif self.version==1: 75 | return uuid.uuid1(self.node, self.clock_seq) 76 | elif self.version==2: 77 | raise UUIDVersionError("UUID version 2 is not supported.") 78 | elif self.version==3: 79 | return uuid.uuid3(self.namespace, self.name) 80 | elif self.version==5: 81 | return uuid.uuid5(self.namespace, self.name) 82 | else: 83 | raise UUIDVersionError("UUID version %s is not valid." % self.version) 84 | 85 | def db_type(self, connection): 86 | from django.conf import settings 87 | return UUIDField._CREATE_COLUMN_TYPES.get(settings.DATABASE_ENGINE, "char(%s)" % self.max_length) 88 | 89 | def to_python(self, value): 90 | """Return a uuid.UUID instance from the value returned by the database.""" 91 | # 92 | # This is the proper way... But this doesn't work correctly when 93 | # working with an inherited model 94 | # 95 | if not value: 96 | return None 97 | if isinstance(value, uuid.UUID): 98 | return value 99 | # attempt to parse a UUID 100 | return uuid.UUID(smart_unicode(value)) 101 | 102 | # 103 | # If I do the following (returning a String instead of a UUID 104 | # instance), everything works. 105 | # 106 | 107 | #if not value: 108 | # return None 109 | #if isinstance(value, uuid.UUID): 110 | # return smart_unicode(value) 111 | #else: 112 | # return value 113 | 114 | def pre_save(self, model_instance, add): 115 | if self.auto and add: 116 | value = self.create_uuid() 117 | setattr(model_instance, self.attname, value) 118 | else: 119 | value = super(UUIDField, self).pre_save(model_instance,add) 120 | if self.auto and not value: 121 | value = self.create_uuid() 122 | setattr(model_instance, self.attname, value) 123 | return value 124 | 125 | def get_db_prep_value(self, value, connection, prepared): 126 | """Casts uuid.UUID values into the format expected by the back end for use in queries""" 127 | if isinstance(value, uuid.UUID): 128 | return smart_unicode(value) 129 | return value 130 | 131 | def value_to_string(self, obj): 132 | val = self._get_val_from_obj(obj) 133 | if val is None: 134 | data = '' 135 | else: 136 | data = smart_unicode(val) 137 | return data 138 | 139 | def formfield(self, **kwargs): 140 | defaults = { 141 | 'form_class': forms.CharField, 142 | 'max_length': self.max_length 143 | } 144 | defaults.update(kwargs) 145 | return super(UUIDField, self).formfield(**defaults) 146 | -------------------------------------------------------------------------------- /polymorphic/showfields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | from pprint import pformat 5 | 6 | class ShowFieldBase(object): 7 | """ base class for the ShowField... model mixins, does the work """ 8 | 9 | polymorphic_query_multiline_output = True # cause nicer multiline PolymorphicQuery output 10 | 11 | polymorphic_showfield_type = False 12 | polymorphic_showfield_content = False 13 | 14 | # these may be overridden by the user 15 | polymorphic_showfield_max_line_width = None 16 | polymorphic_showfield_max_field_width = 20 17 | polymorphic_showfield_old_format = False 18 | 19 | def __repr__(self): 20 | return self.__unicode__() 21 | 22 | def _showfields_get_content(self, field_name, field_type=type(None)): 23 | "helper for __unicode__" 24 | content = getattr(self, field_name) 25 | if self.polymorphic_showfield_old_format: out = ': ' 26 | else: out = ' ' 27 | if issubclass(field_type, models.ForeignKey): 28 | if content is None: out += 'None' 29 | else: out += content.__class__.__name__ 30 | elif issubclass(field_type, models.ManyToManyField): 31 | out += '%d' % content.count() 32 | elif type(content) in (int,long): 33 | out += unicode(content) 34 | elif content is None: 35 | out += 'None' 36 | else: 37 | txt=unicode(content) 38 | if len(txt)>self.polymorphic_showfield_max_field_width: 39 | txt=txt[:self.polymorphic_showfield_max_field_width-2]+'..' 40 | out += '"' + txt + '"' 41 | return out 42 | 43 | def _showfields_add_regular_fields(self, parts): 44 | "helper for __unicode__" 45 | done_fields = set() 46 | for field in self._meta.fields + self._meta.many_to_many: 47 | if field.name in self.polymorphic_internal_model_fields or '_ptr' in field.name: continue 48 | if field.name in done_fields: continue # work around django diamond inheritance problem 49 | done_fields.add(field.name) 50 | 51 | out = field.name 52 | 53 | # if this is the standard primary key named "id", print it as we did with older versions of django_polymorphic 54 | if field.primary_key and field.name=='id' and type(field)==models.AutoField: 55 | out += ' '+ unicode(getattr(self, field.name)) 56 | 57 | # otherwise, display it just like all other fields (with correct type, shortened content etc.) 58 | else: 59 | if self.polymorphic_showfield_type: 60 | out += ' (' + type(field).__name__ 61 | if field.primary_key: out += '/pk' 62 | out += ')' 63 | 64 | if self.polymorphic_showfield_content: 65 | out += self._showfields_get_content(field.name,type(field)) 66 | 67 | parts.append((False, out,',')) 68 | 69 | def _showfields_add_dynamic_fields(self, field_list, title, parts): 70 | "helper for __unicode__" 71 | parts.append( ( True, '- '+title, ':' ) ) 72 | for field_name in field_list: 73 | out = field_name 74 | content = getattr(self, field_name) 75 | if self.polymorphic_showfield_type: 76 | out += ' (' + type(content).__name__ + ')' 77 | if self.polymorphic_showfield_content: 78 | out += self._showfields_get_content(field_name) 79 | 80 | parts.append( ( False, out, ',' ) ) 81 | 82 | def __unicode__(self): 83 | # create list ("parts") containing one tuple for each title/field: 84 | # ( bool: new section , item-text , separator to use after item ) 85 | 86 | # start with model name 87 | parts = [ (True, self.__class__.__name__, ':') ] 88 | 89 | # add all regular fields 90 | self._showfields_add_regular_fields(parts) 91 | 92 | # add annotate fields 93 | if hasattr(self,'polymorphic_annotate_names'): 94 | self._showfields_add_dynamic_fields(self.polymorphic_annotate_names, 'Ann', parts) 95 | 96 | # add extra() select fields 97 | if hasattr(self,'polymorphic_extra_select_names'): 98 | self._showfields_add_dynamic_fields(self.polymorphic_extra_select_names, 'Extra', parts) 99 | 100 | # format result 101 | 102 | indent = len(self.__class__.__name__)+5 103 | indentstr = ''.rjust(indent) 104 | out=u''; xpos=0; possible_line_break_pos = None 105 | 106 | for i in xrange(len(parts)): 107 | new_section, p, separator = parts[i] 108 | final = (i==len(parts)-1) 109 | if not final: 110 | next_new_section, _, _ = parts[i+1] 111 | 112 | if ( self.polymorphic_showfield_max_line_width 113 | and xpos+len(p) > self.polymorphic_showfield_max_line_width 114 | and possible_line_break_pos!=None ): 115 | rest = out[possible_line_break_pos:] 116 | out = out[:possible_line_break_pos] 117 | out+= '\n'+indentstr+rest 118 | xpos=indent+len(rest) 119 | 120 | out += p; xpos += len(p) 121 | 122 | if not final: 123 | if not next_new_section: 124 | out += separator; xpos += len(separator) 125 | out += ' '; xpos += 1 126 | 127 | if not new_section: 128 | possible_line_break_pos=len(out) 129 | 130 | return u'<'+out+'>' 131 | 132 | 133 | class ShowFieldType(ShowFieldBase): 134 | """ model mixin that shows the object's class and it's field types """ 135 | polymorphic_showfield_type = True 136 | 137 | class ShowFieldContent(ShowFieldBase): 138 | """ model mixin that shows the object's class, it's fields and field contents """ 139 | polymorphic_showfield_content = True 140 | 141 | class ShowFieldTypeAndContent(ShowFieldBase): 142 | """ model mixin, like ShowFieldContent, but also show field types """ 143 | polymorphic_showfield_type = True 144 | polymorphic_showfield_content = True 145 | 146 | 147 | # compatibility with old class names 148 | ShowFieldTypes = ShowFieldType 149 | ShowFields = ShowFieldContent 150 | ShowFieldsAndTypes = ShowFieldTypeAndContent 151 | -------------------------------------------------------------------------------- /polymorphic/polymorphic_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Seamless Polymorphic Inheritance for Django Models 4 | ================================================== 5 | 6 | Please see README.rst and DOCS.rst for further information. 7 | 8 | Or on the Web: 9 | http://bserve.webhop.org/wiki/django_polymorphic 10 | http://github.com/bconstantin/django_polymorphic 11 | http://bitbucket.org/bconstantin/django_polymorphic 12 | 13 | Copyright: 14 | This code and affiliated files are (C) by Bert Constantin and individual contributors. 15 | Please see LICENSE and AUTHORS for more information. 16 | """ 17 | 18 | from pprint import pprint 19 | import sys 20 | from compatibility_tools import defaultdict 21 | 22 | from django.db import models 23 | from django.contrib.contenttypes.models import ContentType 24 | from django import VERSION as django_VERSION 25 | 26 | from base import PolymorphicModelBase 27 | from manager import PolymorphicManager 28 | from query import PolymorphicQuerySet 29 | from showfields import ShowFieldType 30 | from query_translate import translate_polymorphic_Q_object 31 | 32 | 33 | ################################################################################### 34 | ### PolymorphicModel 35 | 36 | class PolymorphicModel(models.Model): 37 | """ 38 | Abstract base class that provides polymorphic behaviour 39 | for any model directly or indirectly derived from it. 40 | 41 | For usage instructions & examples please see documentation. 42 | 43 | PolymorphicModel declares one field for internal use (polymorphic_ctype) 44 | and provides a polymorphic manager as the default manager 45 | (and as 'objects'). 46 | 47 | PolymorphicModel overrides the save() and __init__ methods. 48 | 49 | If your derived class overrides any of these methods as well, then you need 50 | to take care that you correctly call the method of the superclass, like: 51 | 52 | super(YourClass,self).save(*args,**kwargs) 53 | """ 54 | __metaclass__ = PolymorphicModelBase 55 | 56 | # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing) 57 | polymorphic_model_marker = True 58 | 59 | # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery 60 | polymorphic_query_multiline_output = False 61 | 62 | class Meta: 63 | abstract = True 64 | 65 | # avoid ContentType related field accessor clash (an error emitted by model validation) 66 | # we really should use both app_label and model name, but this is only possible since Django 1.2 67 | if django_VERSION[0] <= 1 and django_VERSION[1] <= 1: 68 | p_related_name_template = 'polymorphic_%(class)s_set' 69 | else: 70 | p_related_name_template = 'polymorphic_%(app_label)s.%(class)s_set' 71 | polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False, 72 | related_name=p_related_name_template) 73 | 74 | # some applications want to know the name of the fields that are added to its models 75 | polymorphic_internal_model_fields = [ 'polymorphic_ctype' ] 76 | 77 | objects = PolymorphicManager() 78 | base_objects = models.Manager() 79 | 80 | @classmethod 81 | def translate_polymorphic_Q_object(self_class,q): 82 | return translate_polymorphic_Q_object(self_class,q) 83 | 84 | def pre_save_polymorphic(self): 85 | """Normally not needed. 86 | This function may be called manually in special use-cases. When the object 87 | is saved for the first time, we store its real class in polymorphic_ctype. 88 | When the object later is retrieved by PolymorphicQuerySet, it uses this 89 | field to figure out the real class of this object 90 | (used by PolymorphicQuerySet._get_real_instances) 91 | """ 92 | if not self.polymorphic_ctype: 93 | self.polymorphic_ctype = ContentType.objects.get_for_model(self) 94 | 95 | def save(self, *args, **kwargs): 96 | """Overridden model save function which supports the polymorphism 97 | functionality (through pre_save_polymorphic).""" 98 | self.pre_save_polymorphic() 99 | return super(PolymorphicModel, self).save(*args, **kwargs) 100 | 101 | def get_real_instance_class(self): 102 | """Normally not needed. 103 | If a non-polymorphic manager (like base_objects) has been used to 104 | retrieve objects, then the real class/type of these objects may be 105 | determined using this method.""" 106 | # the following line would be the easiest way to do this, but it produces sql queries 107 | #return self.polymorphic_ctype.model_class() 108 | # so we use the following version, which uses the CopntentType manager cache 109 | return ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class() 110 | 111 | def get_real_instance(self): 112 | """Normally not needed. 113 | If a non-polymorphic manager (like base_objects) has been used to 114 | retrieve objects, then the complete object with it's real class/type 115 | and all fields may be retrieved with this method. 116 | Each method call executes one db query (if necessary).""" 117 | real_model = self.get_real_instance_class() 118 | if real_model == self.__class__: return self 119 | return real_model.objects.get(pk=self.pk) 120 | 121 | 122 | def __init__(self, * args, ** kwargs): 123 | """Replace Django's inheritance accessor member functions for our model 124 | (self.__class__) with our own versions. 125 | We monkey patch them until a patch can be added to Django 126 | (which would probably be very small and make all of this obsolete). 127 | 128 | If we have inheritance of the form ModelA -> ModelB ->ModelC then 129 | Django creates accessors like this: 130 | - ModelA: modelb 131 | - ModelB: modela_ptr, modelb, modelc 132 | - ModelC: modela_ptr, modelb, modelb_ptr, modelc 133 | 134 | These accessors allow Django (and everyone else) to travel up and down 135 | the inheritance tree for the db object at hand. 136 | 137 | The original Django accessors use our polymorphic manager. 138 | But they should not. So we replace them with our own accessors that use 139 | our appropriate base_objects manager. 140 | """ 141 | super(PolymorphicModel, self).__init__(*args, ** kwargs) 142 | 143 | if self.__class__.polymorphic_super_sub_accessors_replaced: return 144 | self.__class__.polymorphic_super_sub_accessors_replaced = True 145 | 146 | def create_accessor_function_for_model(model, accessor_name): 147 | def accessor_function(self): 148 | attr = model.base_objects.get(pk=self.pk) 149 | return attr 150 | return accessor_function 151 | 152 | subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models() 153 | 154 | from django.db.models.fields.related import SingleRelatedObjectDescriptor, ReverseSingleRelatedObjectDescriptor 155 | for name,model in subclasses_and_superclasses_accessors.iteritems(): 156 | orig_accessor = getattr(self.__class__, name, None) 157 | if type(orig_accessor) in [SingleRelatedObjectDescriptor,ReverseSingleRelatedObjectDescriptor]: 158 | #print >>sys.stderr, '---------- replacing',name, orig_accessor 159 | setattr(self.__class__, name, property(create_accessor_function_for_model(model, name)) ) 160 | 161 | def _get_inheritance_relation_fields_and_models(self): 162 | """helper function for __init__: 163 | determine names of all Django inheritance accessor member functions for type(self)""" 164 | 165 | def add_model(model, as_ptr, result): 166 | name = model.__name__.lower() 167 | if as_ptr: name+='_ptr' 168 | result[name] = model 169 | 170 | def add_model_if_regular(model, as_ptr, result): 171 | if ( issubclass(model, models.Model) and model != models.Model 172 | and model != self.__class__ 173 | and model != PolymorphicModel ): 174 | add_model(model,as_ptr,result) 175 | 176 | def add_all_super_models(model, result): 177 | add_model_if_regular(model, True, result) 178 | for b in model.__bases__: 179 | add_all_super_models(b, result) 180 | 181 | def add_all_sub_models(model, result): 182 | for b in model.__subclasses__(): 183 | add_model_if_regular(b, False, result) 184 | 185 | result = {} 186 | add_all_super_models(self.__class__,result) 187 | add_all_sub_models(self.__class__,result) 188 | return result 189 | 190 | -------------------------------------------------------------------------------- /polymorphic/query_translate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ PolymorphicQuerySet support functions 3 | Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic 4 | """ 5 | 6 | from django.db import models 7 | from django.contrib.contenttypes.models import ContentType 8 | from django.db.models import Q 9 | from compatibility_tools import compat_partition 10 | 11 | 12 | ################################################################################### 13 | ### PolymorphicQuerySet support functions 14 | 15 | # These functions implement the additional filter- and Q-object functionality. 16 | # They form a kind of small framework for easily adding more 17 | # functionality to filters and Q objects. 18 | # Probably a more general queryset enhancement class could be made out of them. 19 | 20 | def translate_polymorphic_filter_definitions_in_kwargs(queryset_model, kwargs): 21 | """ 22 | Translate the keyword argument list for PolymorphicQuerySet.filter() 23 | 24 | Any kwargs with special polymorphic functionality are replaced in the kwargs 25 | dict with their vanilla django equivalents. 26 | 27 | For some kwargs a direct replacement is not possible, as a Q object is needed 28 | instead to implement the required functionality. In these cases the kwarg is 29 | deleted from the kwargs dict and a Q object is added to the return list. 30 | 31 | Modifies: kwargs dict 32 | Returns: a list of non-keyword-arguments (Q objects) to be added to the filter() query. 33 | """ 34 | additional_args = [] 35 | for field_path, val in kwargs.items(): 36 | 37 | new_expr = _translate_polymorphic_filter_definition(queryset_model, field_path, val) 38 | 39 | if type(new_expr) == tuple: 40 | # replace kwargs element 41 | del(kwargs[field_path]) 42 | kwargs[new_expr[0]] = new_expr[1] 43 | 44 | elif isinstance(new_expr, models.Q): 45 | del(kwargs[field_path]) 46 | additional_args.append(new_expr) 47 | 48 | return additional_args 49 | 50 | def translate_polymorphic_Q_object(queryset_model, potential_q_object): 51 | def tree_node_correct_field_specs(my_model, node): 52 | " process all children of this Q node " 53 | for i in range(len(node.children)): 54 | child = node.children[i] 55 | 56 | if type(child) == tuple: 57 | # this Q object child is a tuple => a kwarg like Q( instance_of=ModelB ) 58 | key, val = child 59 | new_expr = _translate_polymorphic_filter_definition(my_model, key, val) 60 | if new_expr: 61 | node.children[i] = new_expr 62 | else: 63 | # this Q object child is another Q object, recursively process this as well 64 | tree_node_correct_field_specs(my_model, child) 65 | 66 | if isinstance(potential_q_object, models.Q): 67 | tree_node_correct_field_specs(queryset_model, potential_q_object) 68 | 69 | return potential_q_object 70 | 71 | def translate_polymorphic_filter_definitions_in_args(queryset_model, args): 72 | """ 73 | Translate the non-keyword argument list for PolymorphicQuerySet.filter() 74 | 75 | In the args list, we replace all kwargs to Q-objects that contain special 76 | polymorphic functionality with their vanilla django equivalents. 77 | We traverse the Q object tree for this (which is simple). 78 | 79 | TODO: investigate: we modify the Q-objects ina args in-place. Is this OK? 80 | 81 | Modifies: args list 82 | """ 83 | 84 | for q in args: 85 | translate_polymorphic_Q_object(queryset_model, q) 86 | 87 | 88 | def _translate_polymorphic_filter_definition(queryset_model, field_path, field_val): 89 | """ 90 | Translate a keyword argument (field_path=field_val), as used for 91 | PolymorphicQuerySet.filter()-like functions (and Q objects). 92 | 93 | A kwarg with special polymorphic functionality is translated into 94 | its vanilla django equivalent, which is returned, either as tuple 95 | (field_path, field_val) or as Q object. 96 | 97 | Returns: kwarg tuple or Q object or None (if no change is required) 98 | """ 99 | 100 | # handle instance_of expressions or alternatively, 101 | # if this is a normal Django filter expression, return None 102 | if field_path == 'instance_of': 103 | return _create_model_filter_Q(field_val) 104 | elif field_path == 'not_instance_of': 105 | return _create_model_filter_Q(field_val, not_instance_of=True) 106 | elif not '___' in field_path: 107 | return None #no change 108 | 109 | # filter expression contains '___' (i.e. filter for polymorphic field) 110 | # => get the model class specified in the filter expression 111 | newpath = translate_polymorphic_field_path(queryset_model, field_path) 112 | return (newpath, field_val) 113 | 114 | 115 | def translate_polymorphic_field_path(queryset_model, field_path): 116 | """ 117 | Translate a field path from a keyword argument, as used for 118 | PolymorphicQuerySet.filter()-like functions (and Q objects). 119 | Supports leading '-' (for order_by args). 120 | 121 | E.g.: if queryset_model is ModelA, then "ModelC___field3" is translated 122 | into modela__modelb__modelc__field3. 123 | Returns: translated path (unchanged, if no translation needed) 124 | """ 125 | classname, sep, pure_field_path = compat_partition(field_path, '___') 126 | if not sep: return field_path 127 | assert classname, 'PolymorphicModel: %s: bad field specification' % field_path 128 | 129 | negated = False 130 | if classname[0] == '-': 131 | negated = True 132 | classname = classname.lstrip('-') 133 | 134 | if '__' in classname: 135 | # the user has app label prepended to class name via __ => use Django's get_model function 136 | appname, sep, classname = compat_partition(classname, '__') 137 | model = models.get_model(appname, classname) 138 | assert model, 'PolymorphicModel: model %s (in app %s) not found!' % (model.__name__, appname) 139 | if not issubclass(model, queryset_model): 140 | e = 'PolymorphicModel: queryset filter error: "' + model.__name__ + '" is not derived from "' + queryset_model.__name__ + '"' 141 | raise AssertionError(e) 142 | 143 | else: 144 | # the user has only given us the class name via __ 145 | # => select the model from the sub models of the queryset base model 146 | 147 | # function to collect all sub-models, this should be optimized (cached) 148 | def add_all_sub_models(model, result): 149 | if issubclass(model, models.Model) and model != models.Model: 150 | # model name is occurring twice in submodel inheritance tree => Error 151 | if model.__name__ in result and model != result[model.__name__]: 152 | e = 'PolymorphicModel: model name alone is ambiguous: %s.%s and %s.%s!\n' 153 | e += 'In this case, please use the syntax: applabel__ModelName___field' 154 | assert model, e % ( 155 | model._meta.app_label, model.__name__, 156 | result[model.__name__]._meta.app_label, result[model.__name__].__name__) 157 | 158 | result[model.__name__] = model 159 | 160 | for b in model.__subclasses__(): 161 | add_all_sub_models(b, result) 162 | 163 | submodels = {} 164 | add_all_sub_models(queryset_model, submodels) 165 | model = submodels.get(classname, None) 166 | assert model, 'PolymorphicModel: model %s not found (not a subclass of %s)!' % (classname, queryset_model.__name__) 167 | 168 | # create new field path for expressions, e.g. for baseclass=ModelA, myclass=ModelC 169 | # 'modelb__modelc" is returned 170 | def _create_base_path(baseclass, myclass): 171 | bases = myclass.__bases__ 172 | for b in bases: 173 | if b == baseclass: 174 | return myclass.__name__.lower() 175 | path = _create_base_path(baseclass, b) 176 | if path: return path + '__' + myclass.__name__.lower() 177 | return '' 178 | 179 | basepath = _create_base_path(queryset_model, model) 180 | 181 | if negated: newpath = '-' 182 | else: newpath = '' 183 | 184 | newpath += basepath 185 | if basepath: newpath += '__' 186 | 187 | newpath += pure_field_path 188 | return newpath 189 | 190 | 191 | def _create_model_filter_Q(modellist, not_instance_of=False): 192 | """ 193 | Helper function for instance_of / not_instance_of 194 | Creates and returns a Q object that filters for the models in modellist, 195 | including all subclasses of these models (as we want to do the same 196 | as pythons isinstance() ). 197 | . 198 | We recursively collect all __subclasses__(), create a Q filter for each, 199 | and or-combine these Q objects. This could be done much more 200 | efficiently however (regarding the resulting sql), should an optimization 201 | be needed. 202 | """ 203 | 204 | if not modellist: return None 205 | 206 | from polymorphic_model import PolymorphicModel 207 | 208 | if type(modellist) != list and type(modellist) != tuple: 209 | if issubclass(modellist, PolymorphicModel): 210 | modellist = [modellist] 211 | else: 212 | assert False, 'PolymorphicModel: instance_of expects a list of (polymorphic) models or a single (polymorphic) model' 213 | 214 | def q_class_with_subclasses(model): 215 | q = Q(polymorphic_ctype=ContentType.objects.get_for_model(model)) 216 | for subclass in model.__subclasses__(): 217 | q = q | q_class_with_subclasses(subclass) 218 | return q 219 | 220 | qlist = [ q_class_with_subclasses(m) for m in modellist ] 221 | 222 | q_ored = reduce(lambda a, b: a | b, qlist) 223 | if not_instance_of: q_ored = ~q_ored 224 | return q_ored 225 | 226 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Polymorphic Models for Django 2 | ============================= 3 | 4 | 5 | Quick Start, Docs, Contributing 6 | ------------------------------- 7 | 8 | * `What is django_polymorphic good for?`_ 9 | * `Quickstart`_, or the complete `Installation and Usage Docs`_ 10 | * `Release Notes, News and Discussion`_ (Google Group) or Changelog_ 11 | * Download from GitHub_ or Bitbucket_, or as TGZ_ or ZIP_ 12 | * Improve django_polymorphic, report issues, discuss, patch or fork (GitHub_, Bitbucket_, Group_, Mail_) 13 | 14 | .. _What is django_polymorphic good for?: #good-for 15 | .. _release notes, news and discussion: http://groups.google.de/group/django-polymorphic/topics 16 | .. _Group: http://groups.google.de/group/django-polymorphic/topics 17 | .. _Mail: http://github.com/bconstantin/django_polymorphic/tree/master/setup.py 18 | .. _Installation and Usage Docs: http://bserve.webhop.org/django_polymorphic/DOCS.html 19 | .. _Quickstart: http://bserve.webhop.org/django_polymorphic/DOCS.html#quickstart 20 | .. _GitHub: http://github.com/bconstantin/django_polymorphic 21 | .. _Bitbucket: http://bitbucket.org/bconstantin/django_polymorphic 22 | .. _TGZ: http://github.com/bconstantin/django_polymorphic/tarball/master 23 | .. _ZIP: http://github.com/bconstantin/django_polymorphic/zipball/master 24 | .. _Overview: http://bserve.webhop.org/django_polymorphic 25 | .. _Changelog: http://bserve.webhop.org/django_polymorphic/CHANGES.html 26 | 27 | .. _good-for: 28 | 29 | What is django_polymorphic good for? 30 | ------------------------------------ 31 | 32 | Let's assume the models ``ArtProject`` and ``ResearchProject`` are derived 33 | from the model ``Project``, and let's store one of each into the database: 34 | 35 | >>> Project.objects.create(topic="Department Party") 36 | >>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner") 37 | >>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") 38 | 39 | If we want to retrieve all our projects, we do: 40 | 41 | >>> Project.objects.all() 42 | 43 | Using django_polymorphic, we simply get what we stored:: 44 | 45 | [ , 46 | , 47 | ] 48 | 49 | Using vanilla Django, we get incomplete objects, which is probably not what we wanted:: 50 | 51 | [ , 52 | , 53 | ] 54 | 55 | It's very similar for ForeignKeys, ManyToManyFields or OneToOneFields. 56 | 57 | In general, the effect of django_polymorphic is twofold: 58 | 59 | On one hand it makes sure that model inheritance just works as you 60 | expect, by simply ensuring that you always get back exactly the same 61 | objects from the database you stored there - regardless how you access 62 | them, making model inheritance much more "pythonic". 63 | This can save you a lot of unpleasant workarounds that tend to 64 | make your code messy, error-prone, and slow. 65 | 66 | On the other hand, together with some small API additions to the Django 67 | ORM, django_polymorphic enables a much more expressive and intuitive 68 | programming style and also very advanced object oriented designs 69 | that are not possible with vanilla Django. 70 | 71 | Fortunately, most of the heavy duty machinery that is needed for this 72 | functionality is already present in the original Django database layer. 73 | Django_polymorphic adds a rather thin layer above that in order 74 | to make real OO fully automatic and very easy to use. 75 | 76 | There is a catch however, which applies to concrete model inheritance 77 | in general: Current DBM systems like PostgreSQL or MySQL are not very 78 | good at processing the required sql queries and can be rather slow in 79 | many cases. Concrete benchmarks are forthcoming (please see 80 | discussion forum). 81 | 82 | For more information, please look at `Quickstart`_ or at the complete 83 | `Installation and Usage Docs`_ and also see the `restrictions and caveats`_. 84 | 85 | .. _restrictions and caveats: http://bserve.webhop.org/django_polymorphic/DOCS.html#restrictions 86 | 87 | 88 | This is a V1.0 Beta/Testing Release 89 | ----------------------------------- 90 | 91 | The release contains a considerable amount of changes in some of the more 92 | critical parts of the software. It's intended for testing and development 93 | environments and not for production environments. For these, it's best to 94 | wait a few weeks for the proper V1.0 release, to allow some time for any 95 | potential problems to turn up (if they exist). 96 | 97 | If you encounter any problems or have suggestions regarding the API or the 98 | changes in this beta, please post them in the `discussion group`_ 99 | or open an issue on GitHub_ or BitBucket_ (or send me an email). 100 | 101 | .. _discussion group: http://groups.google.de/group/django-polymorphic/topics 102 | 103 | 104 | License 105 | ======= 106 | 107 | Django_polymorphic uses the same license as Django (BSD-like). 108 | 109 | 110 | API Changes & Additions 111 | ======================= 112 | 113 | 114 | November 11 2010, V1.0 API Changes 115 | ------------------------------------------------------------------- 116 | 117 | extra() queryset method 118 | +++++++++++++++++++++++ 119 | 120 | ``.extra()`` has been re-implemented. Now it's polymorphic by 121 | default and works (nearly) without restrictions (please see docs). This is a (very) 122 | incompatible API change regarding previous versions of django_polymorphic. 123 | Support for the ``polymorphic`` keyword parameter has been removed. 124 | You can get back the non-polymorphic behaviour by using 125 | ``ModelA.objects.non_polymorphic().extra()``. 126 | 127 | No Pretty-Printing of Querysets by default 128 | ++++++++++++++++++++++++++++++++++++++++++ 129 | 130 | In order to improve compatibility with vanilla Django, printing quersets 131 | (__repr__ and __unicode__) does not use django_polymorphic's pretty printing 132 | by default anymore. To get the old behaviour when printing querysets, 133 | you need to replace your model definition: 134 | 135 | >>> class Project(PolymorphicModel): 136 | 137 | by: 138 | 139 | >>> class Project(PolymorphicModel, ShowFieldType): 140 | 141 | The mixin classes for pretty output have been renamed: 142 | 143 | ``ShowFieldTypes, ShowFields, ShowFieldsAndTypes`` 144 | 145 | are now: 146 | 147 | ``ShowFieldType, ShowFieldContent and ShowFieldTypeAndContent`` 148 | 149 | (the old ones still exist for compatibility) 150 | 151 | Pretty-Printing Output Format Changed 152 | +++++++++++++++++++++++++++++++++++++ 153 | 154 | ``ShowFieldContent`` and ``ShowFieldTypeAndContent`` now 155 | use a slightly different output format. If this causes too much trouble for 156 | your test cases, you can get the old behaviour back (mostly) by adding 157 | ``polymorphic_showfield_old_format = True`` to your model definitions. 158 | ``ShowField...`` now also produces more informative output for custom 159 | primary keys. 160 | 161 | polymorphic_dumpdata 162 | ++++++++++++++++++++ 163 | 164 | The ``polymorphic_dumpdata`` management command is not needed anymore 165 | and has been disabled, as the regular Django dumpdata command now automatically 166 | works correctly with polymorphic models (for all supported versions of Django). 167 | 168 | Running the Test suite with Django 1.3 169 | ++++++++++++++++++++++++++++++++++++++ 170 | 171 | Django 1.3 requires ``python manage.py test polymorphic`` instead of 172 | just ``python manage.py test``. 173 | 174 | 175 | November 01 2010, V1.0 API Additions 176 | ------------------------------------------------------------------- 177 | 178 | * ``.non_polymorphic()`` queryset member function added. This is preferable to 179 | using ``.base_objects...``, as it just makes the resulting queryset non-polymorphic 180 | and does not change anything else in the behaviour of the manager used (while 181 | ``.base_objects`` is just a different manager). 182 | 183 | * ``.get_real_instances()`` has been elevated to an official part of the API. 184 | It allows you to turn a queryset or list of base objects into a list of the real instances. 185 | This is useful if e.g. you use ``ModelA.objects.non_polymorphic().extra(...)`` and then want to 186 | transform the result to its polymorphic equivalent: 187 | 188 | >>> qs = ModelA.objects.all().non_polymorphic() 189 | >>> real_objects = qs.get_real_instances() 190 | 191 | is equivalent to: 192 | 193 | >>> real_objects = ModelA.objects.all() 194 | 195 | Instead of ``qs.get_real_instances()``, ``ModelA.objects.get_real_instances(qs)`` may be used 196 | as well. In the latter case, ``qs`` may be any list of objects of type ModelA. 197 | 198 | * ``translate_polymorphic_Q_object`` (see DOCS) 199 | 200 | 201 | February 22 2010, Installation Note 202 | ------------------------------------------------------------------- 203 | 204 | The django_polymorphic source code has been restructured 205 | and as a result needs to be installed like a normal Django App 206 | - either via copying the "polymorphic" directory into your 207 | Django project or by running setup.py. Adding 'polymorphic' 208 | to INSTALLED_APPS in settings.py is still optional, however. 209 | 210 | The file `polymorphic.py` cannot be used as a standalone 211 | extension module anymore (as is has been split into a number 212 | of smaller files). 213 | 214 | Importing works slightly different now: All relevant symbols are 215 | imported directly from 'polymorphic' instead from 216 | 'polymorphic.models':: 217 | 218 | # new way 219 | from polymorphic import PolymorphicModel, ... 220 | 221 | # old way, doesn't work anymore 222 | from polymorphic.models import PolymorphicModel, ... 223 | 224 | 225 | January 26 2010: Database Schema Change 226 | ------------------------------------------------------------------- 227 | 228 | The update from January 26 changed the database schema (more info in the commit-log_). 229 | Sorry for any inconvenience. But this should be the final DB schema now. 230 | 231 | .. _commit-log: http://github.com/bconstantin/django_polymorphic/commit/c2b420aea06637966a208329ef7ec853889fa4c7 232 | -------------------------------------------------------------------------------- /polymorphic/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ PolymorphicModel Meta Class 3 | Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic 4 | """ 5 | 6 | import sys 7 | import inspect 8 | 9 | from django.db import models 10 | from django.db.models.base import ModelBase 11 | 12 | from manager import PolymorphicManager 13 | from query import PolymorphicQuerySet 14 | 15 | # PolymorphicQuerySet Q objects (and filter()) support these additional key words. 16 | # These are forbidden as field names (a descriptive exception is raised) 17 | POLYMORPHIC_SPECIAL_Q_KWORDS = [ 'instance_of', 'not_instance_of'] 18 | 19 | 20 | ################################################################################### 21 | ### PolymorphicModel meta class 22 | 23 | class PolymorphicModelBase(ModelBase): 24 | """ 25 | Manager inheritance is a pretty complex topic which may need 26 | more thought regarding how this should be handled for polymorphic 27 | models. 28 | 29 | In any case, we probably should propagate 'objects' and 'base_objects' 30 | from PolymorphicModel to every subclass. We also want to somehow 31 | inherit/propagate _default_manager as well, as it needs to be polymorphic. 32 | 33 | The current implementation below is an experiment to solve this 34 | problem with a very simplistic approach: We unconditionally 35 | inherit/propagate any and all managers (using _copy_to_model), 36 | as long as they are defined on polymorphic models 37 | (the others are left alone). 38 | 39 | Like Django ModelBase, we special-case _default_manager: 40 | if there are any user-defined managers, it is set to the first of these. 41 | 42 | We also require that _default_manager as well as any user defined 43 | polymorphic managers produce querysets that are derived from 44 | PolymorphicQuerySet. 45 | """ 46 | 47 | def __new__(self, model_name, bases, attrs): 48 | #print; print '###', model_name, '- bases:', bases 49 | 50 | # create new model 51 | new_class = self.call_superclass_new_method(model_name, bases, attrs) 52 | 53 | # check if the model fields are all allowed 54 | self.validate_model_fields(new_class) 55 | 56 | # create list of all managers to be inherited from the base classes 57 | inherited_managers = new_class.get_inherited_managers(attrs) 58 | 59 | # add the managers to the new model 60 | for source_name, mgr_name, manager in inherited_managers: 61 | #print '** add inherited manager from model %s, manager %s, %s' % (source_name, mgr_name, manager.__class__.__name__) 62 | new_manager = manager._copy_to_model(new_class) 63 | new_class.add_to_class(mgr_name, new_manager) 64 | 65 | # get first user defined manager; if there is one, make it the _default_manager 66 | user_manager = self.get_first_user_defined_manager(model_name, attrs) 67 | if user_manager: 68 | def_mgr = user_manager._copy_to_model(new_class) 69 | #print '## add default manager', type(def_mgr) 70 | new_class.add_to_class('_default_manager', def_mgr) 71 | new_class._default_manager._inherited = False # the default mgr was defined by the user, not inherited 72 | 73 | # validate resulting default manager 74 | self.validate_model_manager(new_class._default_manager, model_name, '_default_manager') 75 | 76 | # for __init__ function of this class (monkeypatching inheritance accessors) 77 | new_class.polymorphic_super_sub_accessors_replaced = False 78 | 79 | # determine the name of the primary key field and store it into the class variable 80 | # polymorphic_primary_key_name (it is needed by query.py) 81 | for f in new_class._meta.fields: 82 | if f.primary_key and type(f)!=models.OneToOneField: 83 | new_class.polymorphic_primary_key_name=f.name 84 | break 85 | 86 | return new_class 87 | 88 | def get_inherited_managers(self, attrs): 89 | """ 90 | Return list of all managers to be inherited/propagated from the base classes; 91 | use correct mro, only use managers with _inherited==False (they are of no use), 92 | skip managers that are overwritten by the user with same-named class attributes (in attrs) 93 | """ 94 | add_managers = []; add_managers_keys = set() 95 | for base in self.__mro__[1:]: 96 | if not issubclass(base, models.Model): continue 97 | if not getattr(base, 'polymorphic_model_marker', None): continue # leave managers of non-polym. models alone 98 | 99 | for key, manager in base.__dict__.items(): 100 | if type(manager) == models.manager.ManagerDescriptor: manager = manager.manager 101 | if not isinstance(manager, models.Manager): continue 102 | if key in ['_base_manager']: continue # let Django handle _base_manager 103 | if key in attrs: continue 104 | if key in add_managers_keys: continue # manager with that name already added, skip 105 | if manager._inherited: continue # inherited managers (on the bases) have no significance, they are just copies 106 | #print >>sys.stderr,'##',self.__name__, key 107 | if isinstance(manager, PolymorphicManager): # validate any inherited polymorphic managers 108 | self.validate_model_manager(manager, self.__name__, key) 109 | add_managers.append((base.__name__, key, manager)) 110 | add_managers_keys.add(key) 111 | return add_managers 112 | 113 | @classmethod 114 | def get_first_user_defined_manager(self, model_name, attrs): 115 | mgr_list = [] 116 | for key, val in attrs.items(): 117 | if not isinstance(val, models.Manager): continue 118 | mgr_list.append((val.creation_counter, key, val)) 119 | # if there are user defined managers, use first one as _default_manager 120 | if mgr_list: # 121 | _, manager_name, manager = sorted(mgr_list)[0] 122 | #sys.stderr.write( '\n# first user defined manager for model "{model}":\n# "{mgrname}": {mgr}\n# manager model: {mgrmodel}\n\n' 123 | # .format( model=model_name, mgrname=manager_name, mgr=manager, mgrmodel=manager.model ) ) 124 | return manager 125 | return None 126 | 127 | @classmethod 128 | def call_superclass_new_method(self, model_name, bases, attrs): 129 | """call __new__ method of super class and return the newly created class. 130 | Also work around a limitation in Django's ModelBase.""" 131 | # There seems to be a general limitation in Django's app_label handling 132 | # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django 133 | # We run into this problem if polymorphic.py is located in a top-level directory 134 | # which is directly in the python path. To work around this we temporarily set 135 | # app_label here for PolymorphicModel. 136 | meta = attrs.get('Meta', None) 137 | model_module_name = attrs['__module__'] 138 | do_app_label_workaround = (meta 139 | and model_module_name == 'polymorphic' 140 | and model_name == 'PolymorphicModel' 141 | and getattr(meta, 'app_label', None) is None ) 142 | 143 | if do_app_label_workaround: meta.app_label = 'poly_dummy_app_label' 144 | new_class = super(PolymorphicModelBase, self).__new__(self, model_name, bases, attrs) 145 | if do_app_label_workaround: del(meta.app_label) 146 | return new_class 147 | 148 | def validate_model_fields(self): 149 | "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)" 150 | for f in self._meta.fields: 151 | if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS: 152 | e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models' 153 | raise AssertionError(e % (self.__name__, f.name) ) 154 | 155 | @classmethod 156 | def validate_model_manager(self, manager, model_name, manager_name): 157 | """check if the manager is derived from PolymorphicManager 158 | and its querysets from PolymorphicQuerySet - throw AssertionError if not""" 159 | 160 | if not issubclass(type(manager), PolymorphicManager): 161 | e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" manager is of type "' + type(manager).__name__ 162 | e += '", but must be a subclass of PolymorphicManager' 163 | raise AssertionError(e) 164 | if not getattr(manager, 'queryset_class', None) or not issubclass(manager.queryset_class, PolymorphicQuerySet): 165 | e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" (PolymorphicManager) has been instantiated with a queryset class which is' 166 | e += ' not a subclass of PolymorphicQuerySet (which is required)' 167 | raise AssertionError(e) 168 | return manager 169 | 170 | 171 | # hack: a small patch to Django would be a better solution. 172 | # Django's management command 'dumpdata' relies on non-polymorphic 173 | # behaviour of the _default_manager. Therefore, we catch any access to _default_manager 174 | # here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py' 175 | # (non-polymorphic default manager is 'base_objects' for polymorphic models). 176 | # This way we don't need to patch django.core.management.commands.dumpdata 177 | # for all supported Django versions. 178 | # TODO: investigate Django how this can be avoided 179 | _dumpdata_command_running = False 180 | if len(sys.argv)>1: _dumpdata_command_running = ( sys.argv[1] == 'dumpdata' ) 181 | def __getattribute__(self, name): 182 | if name=='_default_manager': 183 | if self._dumpdata_command_running: 184 | frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name 185 | if 'django/core/management/commands/dumpdata.py' in frm[1]: 186 | return self.base_objects 187 | #caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4 188 | #if caller_mod_name == 'django.core.management.commands.dumpdata': 189 | 190 | return super(PolymorphicModelBase, self).__getattribute__(name) 191 | 192 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | *django_polymorphic* 2 | ++++++++++++++++++++ 3 | Changelog 4 | ++++++++++ 5 | 6 | 2011-01-24 V1.0 Release Candidate 1 7 | =================================== 8 | 9 | Bugfixes 10 | ------------------------ 11 | 12 | * Fixed GitHub issue 15 (query result incomplete with inheritance). 13 | Thanks to John Debs for reporting and the test case. 14 | 15 | 16 | ------------------------------------------------------------------ 17 | 18 | 2010-11-11 V1.0 Beta 2 19 | ====================== 20 | 21 | This is a V1.0 Testing Release 22 | ------------------------------ 23 | 24 | Beta 2 accumulated somewhat more changes than intended, and also 25 | has been delayed by DBMS benchmark testing I wanted to do on model 26 | inheritance. These benchmarks show that there are considerable 27 | problems with concrete model inheritance and contemporary DBM systems. 28 | The results will be forthcoming on the google discussion forum. 29 | 30 | Please also see: 31 | http://www.jacobian.org/writing/concrete-inheritance/ 32 | 33 | The API should be stable now with Beta 2, so it's just about potential 34 | bugfixes from now on regarding V1.0. 35 | 36 | Beta 2 is still intended for testing and development environments and not 37 | for production. No complaints have been heard regarding Beta 1 however, 38 | and Beta 1 is used on a few production sites by some enterprising users. 39 | 40 | There will be a release candidate for V1.0 in the very near future. 41 | 42 | 43 | New Features and API changes in Beta 2 since Beta 1 44 | --------------------------------------------------- 45 | 46 | * API CHANGE: ``.extra()`` has been re-implemented. Now it's polymorphic by 47 | default and works (nearly) without restrictions (please see docs). This is a (very) 48 | incompatible API change regarding previous versions of django_polymorphic. 49 | Support for the ``polymorphic`` keyword parameter has been removed. 50 | You can get back the non-polymorphic behaviour by using 51 | ``ModelA.objects.non_polymorphic().extra(...)``. 52 | 53 | * API CHANGE: ``ShowFieldContent`` and ``ShowFieldTypeAndContent`` now 54 | use a slightly different output format. If this causes too much trouble for 55 | your test cases, you can get the old behaviour back (mostly) by adding 56 | ``polymorphic_showfield_old_format = True`` to your model definitions. 57 | ``ShowField...`` now also produces more informative output for custom 58 | primary keys. 59 | 60 | * ``.non_polymorphic()`` queryset member function added. This is preferable to 61 | using ``.base_objects...``, as it just makes the resulting queryset non-polymorphic 62 | and does not change anything else in the behaviour of the manager used (while 63 | ``.base_objects`` is just a different manager). 64 | 65 | * ``.get_real_instances()``: implementation modified to allow the following 66 | more simple and intuitive use:: 67 | 68 | >>> qs = ModelA.objects.all().non_polymorphic() 69 | >>> qs.get_real_instances() 70 | 71 | which is equivalent to:: 72 | 73 | >>> ModelA.objects.all() 74 | 75 | * added member function: 76 | ``normal_q_object = ModelA.translate_polymorphic_Q_object(enhanced_q_object)`` 77 | 78 | * misc changes/improvements 79 | 80 | Bugfixes 81 | ------------------------ 82 | 83 | * Custom fields could cause problems when used as the primary key. 84 | In derived models, Django's automatic ".pk" field does not always work 85 | correctly for such custom fields: "some_object.pk" and "some_object.id" 86 | return different results (which they shouldn't, as pk should always be just 87 | an alias for the primary key field). It's unclear yet if the problem lies in 88 | Django or the affected custom fields. Regardless, the problem resulting 89 | from this has been fixed with a small workaround. 90 | "python manage.py test polymorphic" also tests and reports on this problem now. 91 | Thanks to Mathieu Steele for reporting and the test case. 92 | 93 | 94 | ------------------------------------------------------------------ 95 | 96 | 2010-10-18 V1.0 Beta 1 97 | ====================== 98 | 99 | This is a V1.0 Beta/Testing Release 100 | ----------------------------------- 101 | 102 | This release is mostly a cleanup and maintenance release that also 103 | improves a number of minor things and fixes one (non-critical) bug. 104 | 105 | Some pending API changes and corrections have been folded into this release 106 | in order to make the upcoming V1.0 API as stable as possible. 107 | 108 | This release is also about getting feedback from you in case you don't 109 | approve of any of these changes or would like to get additional 110 | API fixes into V1.0. 111 | 112 | The release contains a considerable amount of changes in some of the more 113 | critical parts of the software. It's intended for testing and development 114 | environments and not for production environments. For these, it's best to 115 | wait a few weeks for the proper V1.0 release, to allow some time for any 116 | potential problems to show up (if they exist). 117 | 118 | If you encounter any such problems, please post them in the discussion group 119 | or open an issue on GitHub or BitBucket (or send me an email). 120 | 121 | There also have been a number of minor API changes. 122 | Please see the README for more information. 123 | 124 | New Features 125 | ------------------------ 126 | 127 | * official Django 1.3 alpha compatibility 128 | 129 | * ``PolymorphicModel.__getattribute__`` hack removed. 130 | This improves performance considerably as python's __getattribute__ 131 | generally causes a pretty large processing overhead. It's gone now. 132 | 133 | * the ``polymorphic_dumpdata`` management command is not needed anymore 134 | and has been disabled, as the regular Django dumpdata command now automatically 135 | works correctly with polymorphic models (for all supported versions of Django). 136 | 137 | * ``.get_real_instances()`` has been elevated to an official part of the API:: 138 | 139 | real_objects = ModelA.objects.get_real_instances(base_objects_list_or_queryset) 140 | 141 | allows you to turn a queryset or list of base objects into a list of the real instances. 142 | This is useful if e.g. you use ``ModelA.base_objects.extra(...)`` and then want to 143 | transform the result to its polymorphic equivalent. 144 | 145 | * ``translate_polymorphic_Q_object`` (see DOCS) 146 | 147 | * improved testing 148 | 149 | * Changelog added: CHANGES.rst/html 150 | 151 | Bugfixes 152 | ------------------------ 153 | 154 | * Removed requirement for primary key to be an IntegerField. 155 | Thanks to Mathieu Steele and Malthe Borch. 156 | 157 | API Changes 158 | ----------- 159 | 160 | **polymorphic_dumpdata** 161 | 162 | The management command ``polymorphic_dumpdata`` is not needed anymore 163 | and has been disabled, as the regular Django dumpdata command now automatically 164 | works correctly with polymorphic models (for all supported versions of Django). 165 | 166 | **Output of Queryset or Object Printing** 167 | 168 | In order to improve compatibility with vanilla Django, printing quersets 169 | (__repr__ and __unicode__) does not use django_polymorphic's pretty printing 170 | by default anymore. To get the old behaviour when printing querysets, 171 | you need to replace your model definition: 172 | 173 | >>> class Project(PolymorphicModel): 174 | 175 | by: 176 | 177 | >>> class Project(PolymorphicModel, ShowFieldType): 178 | 179 | The mixin classes for pretty output have been renamed: 180 | 181 | ``ShowFieldTypes, ShowFields, ShowFieldsAndTypes`` 182 | 183 | are now: 184 | 185 | ``ShowFieldType, ShowFieldContent and ShowFieldTypeAndContent`` 186 | 187 | (the old ones still exist for compatibility) 188 | 189 | **Running the Test suite with Django 1.3** 190 | 191 | Django 1.3 requires ``python manage.py test polymorphic`` instead of 192 | just ``python manage.py test``. 193 | 194 | 195 | ------------------------------------------------------------------ 196 | 197 | 2010-2-22 198 | ========== 199 | 200 | IMPORTANT: API Changed (import path changed), and Installation Note 201 | 202 | The django_polymorphic source code has been restructured 203 | and as a result needs to be installed like a normal Django App 204 | - either via copying the "polymorphic" directory into your 205 | Django project or by running setup.py. Adding 'polymorphic' 206 | to INSTALLED_APPS in settings.py is still optional, however. 207 | 208 | The file `polymorphic.py` cannot be used as a standalone 209 | extension module anymore, as is has been split into a number 210 | of smaller files. 211 | 212 | Importing works slightly different now: All relevant symbols are 213 | imported directly from 'polymorphic' instead from 214 | 'polymorphic.models':: 215 | 216 | # new way 217 | from polymorphic import PolymorphicModel, ... 218 | 219 | # old way, doesn't work anymore 220 | from polymorphic.models import PolymorphicModel, ... 221 | 222 | + minor API addition: 'from polymorphic import VERSION, get_version' 223 | 224 | New Features 225 | ------------------------ 226 | 227 | Python 2.4 compatibility, contributed by Charles Leifer. Thanks! 228 | 229 | Bugfixes 230 | ------------------------ 231 | 232 | Fix: The exception "...has no attribute 'sub_and_superclass_dict'" 233 | could be raised. (This occurred if a subclass defined __init__ 234 | and accessed class members before calling the superclass __init__). 235 | Thanks to Mattias Brändström. 236 | 237 | Fix: There could be name conflicts if 238 | field_name == model_name.lower() or similar. 239 | Now it is possible to give a field the same name as the class 240 | (like with normal Django models). 241 | (Found through the example provided by Mattias Brändström) 242 | 243 | 244 | 245 | ------------------------------------------------------------------ 246 | 247 | 2010-2-4 248 | ========== 249 | 250 | New features (and documentation) 251 | ----------------------------------------- 252 | 253 | queryset order_by method added 254 | 255 | queryset aggregate() and extra() methods implemented 256 | 257 | queryset annotate() method implemented 258 | 259 | queryset values(), values_list(), distinct() documented; defer(), 260 | only() allowed (but not yet supported) 261 | 262 | setup.py added. Thanks to Andrew Ingram. 263 | 264 | More about these additions in the docs: 265 | http://bserve.webhop.org/wiki/django_polymorphic/doc 266 | 267 | Bugfixes 268 | ------------------------ 269 | 270 | * fix remaining potential accessor name clashes (but this only works 271 | with Django 1.2+, for 1.1 no changes). Thanks to Andrew Ingram. 272 | 273 | * fix use of 'id' model field, replaced with 'pk'. 274 | 275 | * fix select_related bug for objects from derived classes (till now 276 | sel.-r. was just ignored) 277 | 278 | "Restrictions & Caveats" updated 279 | ---------------------------------------- 280 | 281 | * Django 1.1 only - the names of polymorphic models must be unique 282 | in the whole project, even if they are in two different apps. 283 | This results from a restriction in the Django 1.1 "related_name" 284 | option (fixed in Django 1.2). 285 | 286 | * Django 1.1 only - when ContentType is used in models, Django's 287 | seralisation or fixtures cannot be used. This issue seems to be 288 | resolved for Django 1.2 (changeset 11863: Fixed #7052, Added 289 | support for natural keys in serialization). 290 | 291 | 292 | 293 | ------------------------------------------------------------------ 294 | 295 | 2010-1-30 296 | ========== 297 | 298 | Fixed ContentType related field accessor clash (an error emitted 299 | by model validation) by adding related_name to the ContentType 300 | ForeignKey. This happened if your polymorphc model used a ContentType 301 | ForeignKey. Thanks to Andrew Ingram. 302 | 303 | 304 | 305 | ------------------------------------------------------------------ 306 | 307 | 2010-1-29 308 | ========== 309 | 310 | Restructured django_polymorphic into a regular Django add-on 311 | application. This is needed for the management commands, and 312 | also seems to be a generally good idea for future enhancements 313 | as well (and it makes sure the tests are always included). 314 | 315 | The ``poly`` app - until now being used for test purposes only 316 | - has been renamed to ``polymorphic``. See DOCS.rst 317 | ("installation/testing") for more info. 318 | 319 | 320 | 321 | ------------------------------------------------------------------ 322 | 323 | 2010-1-28 324 | ========== 325 | 326 | Added the polymorphic_dumpdata management command (github issue 4), 327 | for creating fixtures, this should be used instead of 328 | the normal Django dumpdata command. 329 | Thanks to Charles Leifer. 330 | 331 | Important: Using ContentType together with dumpdata generally 332 | needs Django 1.2 (important as any polymorphic model uses 333 | ContentType). 334 | 335 | 336 | 337 | ------------------------------------------------------------------ 338 | 339 | 2010-1-26 340 | ========== 341 | 342 | IMPORTANT - database schema change (more info in change log). 343 | I hope I got this change in early enough before anyone started 344 | to use polymorphic.py in earnest. Sorry for any inconvenience. 345 | This should be the final DB schema now. 346 | 347 | Django's ContentType is now used instead of app-label and model-name 348 | This is a cleaner and more efficient solution 349 | Thanks to Ilya Semenov for the suggestion. -------------------------------------------------------------------------------- /polymorphic/query.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ QuerySet for PolymorphicModel 3 | Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic 4 | """ 5 | 6 | from compatibility_tools import defaultdict 7 | 8 | from django.db.models.query import QuerySet 9 | from django.contrib.contenttypes.models import ContentType 10 | 11 | from query_translate import translate_polymorphic_filter_definitions_in_kwargs, translate_polymorphic_filter_definitions_in_args 12 | from query_translate import translate_polymorphic_field_path 13 | 14 | # chunk-size: maximum number of objects requested per db-request 15 | # by the polymorphic queryset.iterator() implementation; we use the same chunk size as Django 16 | from django.db.models.query import CHUNK_SIZE # this is 100 for Django 1.1/1.2 17 | Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE 18 | 19 | 20 | 21 | ################################################################################### 22 | ### PolymorphicQuerySet 23 | 24 | class PolymorphicQuerySet(QuerySet): 25 | """ 26 | QuerySet for PolymorphicModel 27 | 28 | Contains the core functionality for PolymorphicModel 29 | 30 | Usually not explicitly needed, except if a custom queryset class 31 | is to be used. 32 | """ 33 | 34 | def __init__(self, *args, **kwargs): 35 | "init our queryset object member variables" 36 | self.polymorphic_disabled = False 37 | super(PolymorphicQuerySet, self).__init__(*args, **kwargs) 38 | 39 | def _clone(self, *args, **kwargs): 40 | "Django's _clone only copies its own variables, so we need to copy ours here" 41 | new = super(PolymorphicQuerySet, self)._clone(*args, **kwargs) 42 | new.polymorphic_disabled = self.polymorphic_disabled 43 | return new 44 | 45 | def non_polymorphic(self, *args, **kwargs): 46 | """switch off polymorphic behaviour for this query. 47 | When the queryset is evaluated, only objects of the type of the 48 | base class used for this query are returned.""" 49 | self.polymorphic_disabled = True 50 | return self 51 | 52 | def instance_of(self, *args): 53 | """Filter the queryset to only include the classes in args (and their subclasses). 54 | Implementation in _translate_polymorphic_filter_defnition.""" 55 | return self.filter(instance_of=args) 56 | 57 | def not_instance_of(self, *args): 58 | """Filter the queryset to exclude the classes in args (and their subclasses). 59 | Implementation in _translate_polymorphic_filter_defnition.""" 60 | return self.filter(not_instance_of=args) 61 | 62 | def _filter_or_exclude(self, negate, *args, **kwargs): 63 | "We override this internal Django functon as it is used for all filter member functions." 64 | translate_polymorphic_filter_definitions_in_args(self.model, args) # the Q objects 65 | additional_args = translate_polymorphic_filter_definitions_in_kwargs(self.model, kwargs) # filter_field='data' 66 | return super(PolymorphicQuerySet, self)._filter_or_exclude(negate, *(list(args) + additional_args), **kwargs) 67 | 68 | def order_by(self, *args, **kwargs): 69 | """translate the field paths in the args, then call vanilla order_by.""" 70 | new_args = [ translate_polymorphic_field_path(self.model, a) for a in args ] 71 | return super(PolymorphicQuerySet, self).order_by(*new_args, **kwargs) 72 | 73 | def _process_aggregate_args(self, args, kwargs): 74 | """for aggregate and annotate kwargs: allow ModelX___field syntax for kwargs, forbid it for args. 75 | Modifies kwargs if needed (these are Aggregate objects, we translate the lookup member variable)""" 76 | for a in args: 77 | assert not '___' in a.lookup, 'PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only' 78 | for a in kwargs.values(): 79 | a.lookup = translate_polymorphic_field_path(self.model, a.lookup) 80 | 81 | def annotate(self, *args, **kwargs): 82 | """translate the polymorphic field paths in the kwargs, then call vanilla annotate. 83 | _get_real_instances will do the rest of the job after executing the query.""" 84 | self._process_aggregate_args(args, kwargs) 85 | return super(PolymorphicQuerySet, self).annotate(*args, **kwargs) 86 | 87 | def aggregate(self, *args, **kwargs): 88 | """translate the polymorphic field paths in the kwargs, then call vanilla aggregate. 89 | We need no polymorphic object retrieval for aggregate => switch it off.""" 90 | self._process_aggregate_args(args, kwargs) 91 | self.polymorphic_disabled = True 92 | return super(PolymorphicQuerySet, self).aggregate(*args, **kwargs) 93 | 94 | # Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.^ 95 | # The resulting objects are required to have a unique primary key within the result set 96 | # (otherwise an error is thrown). 97 | # The "polymorphic" keyword argument is not supported anymore. 98 | #def extra(self, *args, **kwargs): 99 | 100 | 101 | def _get_real_instances(self, base_result_objects): 102 | """ 103 | Polymorphic object loader 104 | 105 | Does the same as: 106 | 107 | return [ o.get_real_instance() for o in base_result_objects ] 108 | 109 | but more efficiently. 110 | 111 | The list base_result_objects contains the objects from the executed 112 | base class query. The class of all of them is self.model (our base model). 113 | 114 | Some, many or all of these objects were not created and stored as 115 | class self.model, but as a class derived from self.model. We want to re-fetch 116 | these objects from the db as their original class so we can return them 117 | just as they were created/saved. 118 | 119 | We identify these objects by looking at o.polymorphic_ctype, which specifies 120 | the real class of these objects (the class at the time they were saved). 121 | 122 | First, we sort the result objects in base_result_objects for their 123 | subclass (from o.polymorphic_ctype), and then we execute one db query per 124 | subclass of objects. Here, we handle any annotations from annotate(). 125 | 126 | Finally we re-sort the resulting objects into the correct order and 127 | return them as a list. 128 | """ 129 | ordered_id_list = [] # list of ids of result-objects in correct order 130 | results = {} # polymorphic dict of result-objects, keyed with their id (no order) 131 | 132 | # dict contains one entry per unique model type occurring in result, 133 | # in the format idlist_per_model[modelclass]=[list-of-object-ids] 134 | idlist_per_model = defaultdict(list) 135 | 136 | # - sort base_result_object ids into idlist_per_model lists, depending on their real class; 137 | # - also record the correct result order in "ordered_id_list" 138 | # - store objects that already have the correct class into "results" 139 | base_result_objects_by_id = {} 140 | self_model_content_type_id = ContentType.objects.get_for_model(self.model).pk 141 | for base_object in base_result_objects: 142 | ordered_id_list.append(base_object.pk) 143 | 144 | # check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...) 145 | assert not base_object.pk in base_result_objects_by_id, ( 146 | "django_polymorphic: result objects do not have unique primary keys - model "+unicode(self.model) ) 147 | 148 | base_result_objects_by_id[base_object.pk] = base_object 149 | 150 | # this object is not a derived object and already the real instance => store it right away 151 | if (base_object.polymorphic_ctype_id == self_model_content_type_id): 152 | results[base_object.pk] = base_object 153 | 154 | # this object is derived and its real instance needs to be retrieved 155 | # => store it's id into the bin for this model type 156 | else: 157 | idlist_per_model[base_object.get_real_instance_class()].append(base_object.pk) 158 | 159 | # django's automatic ".pk" field does not always work correctly for 160 | # custom fields in derived objects (unclear yet who to put the blame on). 161 | # We get different type(o.pk) in this case. 162 | # We work around this by using the real name of the field directly 163 | # for accessing the primary key of the the derived objects. 164 | # We might assume that self.model._meta.pk.name gives us the name of the primary key field, 165 | # but it doesn't. Therefore we use polymorphic_primary_key_name, which we set up in base.py. 166 | pk_name = self.model.polymorphic_primary_key_name 167 | 168 | # For each model in "idlist_per_model" request its objects (the real model) 169 | # from the db and store them in results[]. 170 | # Then we copy the annotate fields from the base objects to the real objects. 171 | # Then we copy the extra() select fields from the base objects to the real objects. 172 | # TODO: defer(), only(): support for these would be around here 173 | for modelclass, idlist in idlist_per_model.items(): 174 | qs = modelclass.base_objects.filter(pk__in=idlist) # use pk__in instead #### 175 | qs.dup_select_related(self) # copy select related configuration to new qs 176 | 177 | for o in qs: 178 | o_pk=getattr(o,pk_name) 179 | 180 | if self.query.aggregates: 181 | for anno_field_name in self.query.aggregates.keys(): 182 | attr = getattr(base_result_objects_by_id[o_pk], anno_field_name) 183 | setattr(o, anno_field_name, attr) 184 | 185 | if self.query.extra_select: 186 | for select_field_name in self.query.extra_select.keys(): 187 | attr = getattr(base_result_objects_by_id[o_pk], select_field_name) 188 | setattr(o, select_field_name, attr) 189 | 190 | results[o_pk] = o 191 | 192 | # re-create correct order and return result list 193 | resultlist = [ results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results ] 194 | 195 | # set polymorphic_annotate_names in all objects (currently just used for debugging/printing) 196 | if self.query.aggregates: 197 | annotate_names=self.query.aggregates.keys() # get annotate field list 198 | for o in resultlist: 199 | o.polymorphic_annotate_names=annotate_names 200 | 201 | # set polymorphic_extra_select_names in all objects (currently just used for debugging/printing) 202 | if self.query.extra_select: 203 | extra_select_names=self.query.extra_select.keys() # get extra select field list 204 | for o in resultlist: 205 | o.polymorphic_extra_select_names=extra_select_names 206 | 207 | return resultlist 208 | 209 | def iterator(self): 210 | """ 211 | This function is used by Django for all object retrieval. 212 | By overriding it, we modify the objects that this queryset returns 213 | when it is evaluated (or its get method or other object-returning methods are called). 214 | 215 | Here we do the same as: 216 | 217 | base_result_objects=list(super(PolymorphicQuerySet, self).iterator()) 218 | real_results=self._get_real_instances(base_result_objects) 219 | for o in real_results: yield o 220 | 221 | but it requests the objects in chunks from the database, 222 | with Polymorphic_QuerySet_objects_per_request per chunk 223 | """ 224 | base_iter = super(PolymorphicQuerySet, self).iterator() 225 | 226 | # disabled => work just like a normal queryset 227 | if self.polymorphic_disabled: 228 | for o in base_iter: yield o 229 | raise StopIteration 230 | 231 | while True: 232 | base_result_objects = [] 233 | reached_end = False 234 | 235 | for i in range(Polymorphic_QuerySet_objects_per_request): 236 | try: 237 | o=base_iter.next() 238 | base_result_objects.append(o) 239 | except StopIteration: 240 | reached_end = True 241 | break 242 | 243 | real_results = self._get_real_instances(base_result_objects) 244 | 245 | for o in real_results: 246 | yield o 247 | 248 | if reached_end: raise StopIteration 249 | 250 | def __repr__(self, *args, **kwargs): 251 | if self.model.polymorphic_query_multiline_output: 252 | result = [ repr(o) for o in self.all() ] 253 | return '[ ' + ',\n '.join(result) + ' ]' 254 | else: 255 | return super(PolymorphicQuerySet,self).__repr__(*args, **kwargs) 256 | 257 | class _p_list_class(list): 258 | def __repr__(self, *args, **kwargs): 259 | result = [ repr(o) for o in self ] 260 | return '[ ' + ',\n '.join(result) + ' ]' 261 | 262 | def get_real_instances(self, base_result_objects=None): 263 | "same as _get_real_instances, but make sure that __repr__ for ShowField... creates correct output" 264 | if not base_result_objects: base_result_objects=self 265 | olist = self._get_real_instances(base_result_objects) 266 | if not self.model.polymorphic_query_multiline_output: 267 | return olist 268 | clist=PolymorphicQuerySet._p_list_class(olist) 269 | return clist 270 | 271 | -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 218 | 219 | 220 |
221 | 222 | 223 |
224 |

Polymorphic Models for Django

225 |
226 |

Quick Start, Docs, Contributing

227 | 234 |
235 |
236 |

What is django_polymorphic good for?

237 |

Let's assume the models ArtProject and ResearchProject are derived 238 | from the model Project, and let's store one of each into the database:

239 |
240 | >>> Project.objects.create(topic="Department Party")
241 | >>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner")
242 | >>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
243 | 
244 |

If we want to retrieve all our projects, we do:

245 |
246 | >>> Project.objects.all()
247 | 
248 |

Using django_polymorphic, we simply get what we stored:

249 |
250 | [ <Project:         id 1, topic "Department Party">,
251 |   <ArtProject:      id 2, topic "Painting with Tim", artist "T. Turner">,
252 |   <ResearchProject: id 3, topic "Swallow Aerodynamics", supervisor "Dr. Winter"> ]
253 | 
254 |

Using vanilla Django, we get incomplete objects, which is probably not what we wanted:

255 |
256 | [ <Project: id 1, topic "Department Party">,
257 |   <Project: id 2, topic "Painting with Tim">,
258 |   <Project: id 3, topic "Swallow Aerodynamics"> ]
259 | 
260 |

It's very similar for ForeignKeys, ManyToManyFields or OneToOneFields.

261 |

In general, the effect of django_polymorphic is twofold:

262 |

On one hand it makes sure that model inheritance just works as you 263 | expect, by simply ensuring that you always get back exactly the same 264 | objects from the database you stored there - regardless how you access 265 | them, making model inheritance much more "pythonic". 266 | This can save you a lot of unpleasant workarounds that tend to 267 | make your code messy, error-prone, and slow.

268 |

On the other hand, together with some small API additions to the Django 269 | ORM, django_polymorphic enables a much more expressive and intuitive 270 | programming style and also very advanced object oriented designs 271 | that are not possible with vanilla Django.

272 |

Fortunately, most of the heavy duty machinery that is needed for this 273 | functionality is already present in the original Django database layer. 274 | Django_polymorphic adds a rather thin layer above that in order 275 | to make real OO fully automatic and very easy to use.

276 |

There is a catch however, which applies to concrete model inheritance 277 | in general: Current DBM systems like PostgreSQL or MySQL are not very 278 | good at processing the required sql queries and can be rather slow in 279 | many cases. Concrete benchmarks are forthcoming (please see 280 | discussion forum).

281 |

For more information, please look at Quickstart or at the complete 282 | Installation and Usage Docs and also see the restrictions and caveats.

283 |
284 |
285 |

This is a V1.0 Beta/Testing Release

286 |

The release contains a considerable amount of changes in some of the more 287 | critical parts of the software. It's intended for testing and development 288 | environments and not for production environments. For these, it's best to 289 | wait a few weeks for the proper V1.0 release, to allow some time for any 290 | potential problems to turn up (if they exist).

291 |

If you encounter any problems or have suggestions regarding the API or the 292 | changes in this beta, please post them in the discussion group 293 | or open an issue on GitHub or BitBucket (or send me an email).

294 |
295 |
296 |
297 |

License

298 |

Django_polymorphic uses the same license as Django (BSD-like).

299 |
300 |
301 |

API Changes & Additions

302 |
303 |

November 11 2010, V1.0 API Changes

304 |
305 |

extra() queryset method

306 |

.extra() has been re-implemented. Now it's polymorphic by 307 | default and works (nearly) without restrictions (please see docs). This is a (very) 308 | incompatible API change regarding previous versions of django_polymorphic. 309 | Support for the polymorphic keyword parameter has been removed. 310 | You can get back the non-polymorphic behaviour by using 311 | ModelA.objects.non_polymorphic().extra().

312 |
313 |
314 |

No Pretty-Printing of Querysets by default

315 |

In order to improve compatibility with vanilla Django, printing quersets 316 | (__repr__ and __unicode__) does not use django_polymorphic's pretty printing 317 | by default anymore. To get the old behaviour when printing querysets, 318 | you need to replace your model definition:

319 |
320 | >>> class Project(PolymorphicModel):
321 | 
322 |

by:

323 |
324 | >>> class Project(PolymorphicModel, ShowFieldType):
325 | 
326 |

The mixin classes for pretty output have been renamed:

327 |
328 | ShowFieldTypes, ShowFields, ShowFieldsAndTypes
329 |

are now:

330 |
331 | ShowFieldType, ShowFieldContent and ShowFieldTypeAndContent
332 |

(the old ones still exist for compatibility)

333 |
334 |
335 |

Pretty-Printing Output Format Changed

336 |

ShowFieldContent and ShowFieldTypeAndContent now 337 | use a slightly different output format. If this causes too much trouble for 338 | your test cases, you can get the old behaviour back (mostly) by adding 339 | polymorphic_showfield_old_format = True to your model definitions. 340 | ShowField... now also produces more informative output for custom 341 | primary keys.

342 |
343 |
344 |

polymorphic_dumpdata

345 |

The polymorphic_dumpdata management command is not needed anymore 346 | and has been disabled, as the regular Django dumpdata command now automatically 347 | works correctly with polymorphic models (for all supported versions of Django).

348 |
349 |
350 |

Running the Test suite with Django 1.3

351 |

Django 1.3 requires python manage.py test polymorphic instead of 352 | just python manage.py test.

353 |
354 |
355 |
356 |

November 01 2010, V1.0 API Additions

357 |
    358 |
  • .non_polymorphic() queryset member function added. This is preferable to 359 | using .base_objects..., as it just makes the resulting queryset non-polymorphic 360 | and does not change anything else in the behaviour of the manager used (while 361 | .base_objects is just a different manager).

    362 |
  • 363 |
  • .get_real_instances() has been elevated to an official part of the API. 364 | It allows you to turn a queryset or list of base objects into a list of the real instances. 365 | This is useful if e.g. you use ModelA.objects.non_polymorphic().extra(...) and then want to 366 | transform the result to its polymorphic equivalent:

    367 |
    368 | >>> qs = ModelA.objects.all().non_polymorphic()
    369 | >>> real_objects = qs.get_real_instances()
    370 | 
    371 |

    is equivalent to:

    372 |
    373 | >>> real_objects = ModelA.objects.all()
    374 | 
    375 |

    Instead of qs.get_real_instances(), ModelA.objects.get_real_instances(qs) may be used 376 | as well. In the latter case, qs may be any list of objects of type ModelA.

    377 |
  • 378 |
  • translate_polymorphic_Q_object (see DOCS)

    379 |
  • 380 |
381 |
382 |
383 |

February 22 2010, Installation Note

384 |

The django_polymorphic source code has been restructured 385 | and as a result needs to be installed like a normal Django App 386 | - either via copying the "polymorphic" directory into your 387 | Django project or by running setup.py. Adding 'polymorphic' 388 | to INSTALLED_APPS in settings.py is still optional, however.

389 |

The file polymorphic.py cannot be used as a standalone 390 | extension module anymore (as is has been split into a number 391 | of smaller files).

392 |

Importing works slightly different now: All relevant symbols are 393 | imported directly from 'polymorphic' instead from 394 | 'polymorphic.models':

395 |
396 | # new way
397 | from polymorphic import PolymorphicModel, ...
398 | 
399 | # old way, doesn't work anymore
400 | from polymorphic.models import PolymorphicModel, ...
401 | 
402 |
403 |
404 |

January 26 2010: Database Schema Change

405 |

The update from January 26 changed the database schema (more info in the commit-log). 406 | Sorry for any inconvenience. But this should be the final DB schema now.

407 |
408 |
409 |
410 | 411 | 412 | -------------------------------------------------------------------------------- /CHANGES.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 218 | 219 | 220 |
221 | 222 | 223 |
224 |

django_polymorphic

225 |
226 |
227 |

Changelog

228 |
229 |

2011-01-24 V1.0 Release Candidate 1

230 |
231 |

Bugfixes

232 |
    233 |
  • Fixed GitHub issue 15 (query result incomplete with inheritance). 234 | Thanks to John Debs for reporting and the test case.
  • 235 |
236 |
237 |
238 |
239 |
240 |

2010-11-11 V1.0 Beta 2

241 |
242 |

This is a V1.0 Testing Release

243 |

Beta 2 accumulated somewhat more changes than intended, and also 244 | has been delayed by DBMS benchmark testing I wanted to do on model 245 | inheritance. These benchmarks show that there are considerable 246 | problems with concrete model inheritance and contemporary DBM systems. 247 | The results will be forthcoming on the google discussion forum.

248 |

Please also see: 249 | http://www.jacobian.org/writing/concrete-inheritance/

250 |

The API should be stable now with Beta 2, so it's just about potential 251 | bugfixes from now on regarding V1.0.

252 |

Beta 2 is still intended for testing and development environments and not 253 | for production. No complaints have been heard regarding Beta 1 however, 254 | and Beta 1 is used on a few production sites by some enterprising users.

255 |

There will be a release candidate for V1.0 in the very near future.

256 |
257 |
258 |

New Features and API changes in Beta 2 since Beta 1

259 |
    260 |
  • API CHANGE: .extra() has been re-implemented. Now it's polymorphic by 261 | default and works (nearly) without restrictions (please see docs). This is a (very) 262 | incompatible API change regarding previous versions of django_polymorphic. 263 | Support for the polymorphic keyword parameter has been removed. 264 | You can get back the non-polymorphic behaviour by using 265 | ModelA.objects.non_polymorphic().extra(...).

    266 |
  • 267 |
  • API CHANGE: ShowFieldContent and ShowFieldTypeAndContent now 268 | use a slightly different output format. If this causes too much trouble for 269 | your test cases, you can get the old behaviour back (mostly) by adding 270 | polymorphic_showfield_old_format = True to your model definitions. 271 | ShowField... now also produces more informative output for custom 272 | primary keys.

    273 |
  • 274 |
  • .non_polymorphic() queryset member function added. This is preferable to 275 | using .base_objects..., as it just makes the resulting queryset non-polymorphic 276 | and does not change anything else in the behaviour of the manager used (while 277 | .base_objects is just a different manager).

    278 |
  • 279 |
  • .get_real_instances(): implementation modified to allow the following 280 | more simple and intuitive use:

    281 |
    282 | >>> qs = ModelA.objects.all().non_polymorphic()
    283 | >>> qs.get_real_instances()
    284 | 
    285 |

    which is equivalent to:

    286 |
    287 | >>> ModelA.objects.all()
    288 | 
    289 |
  • 290 |
  • added member function: 291 | normal_q_object = ModelA.translate_polymorphic_Q_object(enhanced_q_object)

    292 |
  • 293 |
  • misc changes/improvements

    294 |
  • 295 |
296 |
297 |
298 |

Bugfixes

299 |
    300 |
  • Custom fields could cause problems when used as the primary key. 301 | In derived models, Django's automatic ".pk" field does not always work 302 | correctly for such custom fields: "some_object.pk" and "some_object.id" 303 | return different results (which they shouldn't, as pk should always be just 304 | an alias for the primary key field). It's unclear yet if the problem lies in 305 | Django or the affected custom fields. Regardless, the problem resulting 306 | from this has been fixed with a small workaround. 307 | "python manage.py test polymorphic" also tests and reports on this problem now. 308 | Thanks to Mathieu Steele for reporting and the test case.
  • 309 |
310 |
311 |
312 |
313 |
314 |

2010-10-18 V1.0 Beta 1

315 |
316 |

This is a V1.0 Beta/Testing Release

317 |

This release is mostly a cleanup and maintenance release that also 318 | improves a number of minor things and fixes one (non-critical) bug.

319 |

Some pending API changes and corrections have been folded into this release 320 | in order to make the upcoming V1.0 API as stable as possible.

321 |

This release is also about getting feedback from you in case you don't 322 | approve of any of these changes or would like to get additional 323 | API fixes into V1.0.

324 |

The release contains a considerable amount of changes in some of the more 325 | critical parts of the software. It's intended for testing and development 326 | environments and not for production environments. For these, it's best to 327 | wait a few weeks for the proper V1.0 release, to allow some time for any 328 | potential problems to show up (if they exist).

329 |

If you encounter any such problems, please post them in the discussion group 330 | or open an issue on GitHub or BitBucket (or send me an email).

331 |

There also have been a number of minor API changes. 332 | Please see the README for more information.

333 |
334 |
335 |

New Features

336 |
    337 |
  • official Django 1.3 alpha compatibility

    338 |
  • 339 |
  • PolymorphicModel.__getattribute__ hack removed. 340 | This improves performance considerably as python's __getattribute__ 341 | generally causes a pretty large processing overhead. It's gone now.

    342 |
  • 343 |
  • the polymorphic_dumpdata management command is not needed anymore 344 | and has been disabled, as the regular Django dumpdata command now automatically 345 | works correctly with polymorphic models (for all supported versions of Django).

    346 |
  • 347 |
  • .get_real_instances() has been elevated to an official part of the API:

    348 |
    349 | real_objects = ModelA.objects.get_real_instances(base_objects_list_or_queryset)
    350 | 
    351 |

    allows you to turn a queryset or list of base objects into a list of the real instances. 352 | This is useful if e.g. you use ModelA.base_objects.extra(...) and then want to 353 | transform the result to its polymorphic equivalent.

    354 |
  • 355 |
  • translate_polymorphic_Q_object (see DOCS)

    356 |
  • 357 |
  • improved testing

    358 |
  • 359 |
  • Changelog added: CHANGES.rst/html

    360 |
  • 361 |
362 |
363 |
364 |

Bugfixes

365 |
    366 |
  • Removed requirement for primary key to be an IntegerField. 367 | Thanks to Mathieu Steele and Malthe Borch.
  • 368 |
369 |
370 |
371 |

API Changes

372 |

polymorphic_dumpdata

373 |

The management command polymorphic_dumpdata is not needed anymore 374 | and has been disabled, as the regular Django dumpdata command now automatically 375 | works correctly with polymorphic models (for all supported versions of Django).

376 |

Output of Queryset or Object Printing

377 |

In order to improve compatibility with vanilla Django, printing quersets 378 | (__repr__ and __unicode__) does not use django_polymorphic's pretty printing 379 | by default anymore. To get the old behaviour when printing querysets, 380 | you need to replace your model definition:

381 |
382 | >>> class Project(PolymorphicModel):
383 | 
384 |

by:

385 |
386 | >>> class Project(PolymorphicModel, ShowFieldType):
387 | 
388 |

The mixin classes for pretty output have been renamed:

389 |
390 | ShowFieldTypes, ShowFields, ShowFieldsAndTypes
391 |

are now:

392 |
393 | ShowFieldType, ShowFieldContent and ShowFieldTypeAndContent
394 |

(the old ones still exist for compatibility)

395 |

Running the Test suite with Django 1.3

396 |

Django 1.3 requires python manage.py test polymorphic instead of 397 | just python manage.py test.

398 |
399 |
400 |
401 |
402 |

2010-2-22

403 |

IMPORTANT: API Changed (import path changed), and Installation Note

404 |

The django_polymorphic source code has been restructured 405 | and as a result needs to be installed like a normal Django App 406 | - either via copying the "polymorphic" directory into your 407 | Django project or by running setup.py. Adding 'polymorphic' 408 | to INSTALLED_APPS in settings.py is still optional, however.

409 |

The file polymorphic.py cannot be used as a standalone 410 | extension module anymore, as is has been split into a number 411 | of smaller files.

412 |

Importing works slightly different now: All relevant symbols are 413 | imported directly from 'polymorphic' instead from 414 | 'polymorphic.models':

415 |
416 | # new way
417 | from polymorphic import PolymorphicModel, ...
418 | 
419 | # old way, doesn't work anymore
420 | from polymorphic.models import PolymorphicModel, ...
421 | 
422 |
    423 |
  • minor API addition: 'from polymorphic import VERSION, get_version'
  • 424 |
425 |
426 |

New Features

427 |

Python 2.4 compatibility, contributed by Charles Leifer. Thanks!

428 |
429 |
430 |

Bugfixes

431 |

Fix: The exception "...has no attribute 'sub_and_superclass_dict'" 432 | could be raised. (This occurred if a subclass defined __init__ 433 | and accessed class members before calling the superclass __init__). 434 | Thanks to Mattias Brändström.

435 |

Fix: There could be name conflicts if 436 | field_name == model_name.lower() or similar. 437 | Now it is possible to give a field the same name as the class 438 | (like with normal Django models). 439 | (Found through the example provided by Mattias Brändström)

440 |
441 |
442 |
443 |
444 |

2010-2-4

445 |
446 |

New features (and documentation)

447 |

queryset order_by method added

448 |

queryset aggregate() and extra() methods implemented

449 |

queryset annotate() method implemented

450 |

queryset values(), values_list(), distinct() documented; defer(), 451 | only() allowed (but not yet supported)

452 |

setup.py added. Thanks to Andrew Ingram.

453 |

More about these additions in the docs: 454 | http://bserve.webhop.org/wiki/django_polymorphic/doc

455 |
456 |
457 |

Bugfixes

458 |
    459 |
  • fix remaining potential accessor name clashes (but this only works 460 | with Django 1.2+, for 1.1 no changes). Thanks to Andrew Ingram.
  • 461 |
  • fix use of 'id' model field, replaced with 'pk'.
  • 462 |
  • fix select_related bug for objects from derived classes (till now 463 | sel.-r. was just ignored)
  • 464 |
465 |
466 |
467 |

"Restrictions & Caveats" updated

468 |
    469 |
  • Django 1.1 only - the names of polymorphic models must be unique 470 | in the whole project, even if they are in two different apps. 471 | This results from a restriction in the Django 1.1 "related_name" 472 | option (fixed in Django 1.2).
  • 473 |
  • Django 1.1 only - when ContentType is used in models, Django's 474 | seralisation or fixtures cannot be used. This issue seems to be 475 | resolved for Django 1.2 (changeset 11863: Fixed #7052, Added 476 | support for natural keys in serialization).
  • 477 |
478 |
479 |
480 |
481 |
482 |

2010-1-30

483 |

Fixed ContentType related field accessor clash (an error emitted 484 | by model validation) by adding related_name to the ContentType 485 | ForeignKey. This happened if your polymorphc model used a ContentType 486 | ForeignKey. Thanks to Andrew Ingram.

487 |
488 |
489 |
490 |

2010-1-29

491 |

Restructured django_polymorphic into a regular Django add-on 492 | application. This is needed for the management commands, and 493 | also seems to be a generally good idea for future enhancements 494 | as well (and it makes sure the tests are always included).

495 |

The poly app - until now being used for test purposes only 496 | - has been renamed to polymorphic. See DOCS.rst 497 | ("installation/testing") for more info.

498 |
499 |
500 |
501 |

2010-1-28

502 |

Added the polymorphic_dumpdata management command (github issue 4), 503 | for creating fixtures, this should be used instead of 504 | the normal Django dumpdata command. 505 | Thanks to Charles Leifer.

506 |

Important: Using ContentType together with dumpdata generally 507 | needs Django 1.2 (important as any polymorphic model uses 508 | ContentType).

509 |
510 |
511 |
512 |

2010-1-26

513 |

IMPORTANT - database schema change (more info in change log). 514 | I hope I got this change in early enough before anyone started 515 | to use polymorphic.py in earnest. Sorry for any inconvenience. 516 | This should be the final DB schema now.

517 |

Django's ContentType is now used instead of app-label and model-name 518 | This is a cleaner and more efficient solution 519 | Thanks to Ilya Semenov for the suggestion.

520 |
521 |
522 |
523 | 524 | 525 | -------------------------------------------------------------------------------- /DOCS.rst: -------------------------------------------------------------------------------- 1 | Polymorphic Models for Django 2 | ============================= 3 | 4 | .. contents:: Table of Contents 5 | :depth: 1 6 | 7 | 8 | Quickstart 9 | =========== 10 | 11 | Install 12 | ------- 13 | 14 | After uncompressing (if necessary), in the directory "...django_polymorphic", 15 | execute (on Unix-like systems):: 16 | 17 | sudo python setup.py install 18 | 19 | Make Your Models Polymorphic 20 | ---------------------------- 21 | 22 | Use ``PolymorphicModel`` instead of Django's ``models.Model``, like so:: 23 | 24 | from polymorphic import PolymorphicModel 25 | 26 | class Project(PolymorphicModel): 27 | topic = models.CharField(max_length=30) 28 | 29 | class ArtProject(Project): 30 | artist = models.CharField(max_length=30) 31 | 32 | class ResearchProject(Project): 33 | supervisor = models.CharField(max_length=30) 34 | 35 | All models inheriting from your polymorphic models will be polymorphic as well. 36 | 37 | Create some objects 38 | ------------------- 39 | 40 | >>> Project.objects.create(topic="Department Party") 41 | >>> ArtProject.objects.create(topic="Painting with Tim", artist="T. Turner") 42 | >>> ResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") 43 | 44 | Get polymorphic query results 45 | ----------------------------- 46 | 47 | >>> Project.objects.all() 48 | [ , 49 | , 50 | ] 51 | 52 | use ``instance_of`` or ``not_instance_of`` for narrowing the result to specific subtypes: 53 | 54 | >>> Project.objects.instance_of(ArtProject) 55 | [ ] 56 | 57 | >>> Project.objects.instance_of(ArtProject) | Project.objects.instance_of(ResearchProject) 58 | [ , 59 | ] 60 | 61 | Polymorphic filtering: Get all projects where Mr. Turner is involved as an artist 62 | or supervisor (note the three underscores): 63 | 64 | >>> Project.objects.filter( Q(ArtProject___artist = 'T. Turner') | Q(ResearchProject___supervisor = 'T. Turner') ) 65 | [ , 66 | ] 67 | 68 | This is basically all you need to know, as django_polymorphic mostly 69 | works fully automatic and just delivers the expected ("pythonic") results. 70 | 71 | Note: In all example output, above and below, for a nicer and more informative 72 | output the ``ShowFieldType`` mixin has been used (documented below). 73 | 74 | 75 | List of Features 76 | ================ 77 | 78 | * Fully automatic - generally makes sure that the same objects are 79 | returned from the database that were stored there, regardless how 80 | they are retrieved 81 | * Only on models that request polymorphic behaviour (and the 82 | models inheriting from them) 83 | * Full support for ForeignKeys, ManyToManyFields and OneToToneFields 84 | * Filtering for classes, equivalent to python's isinstance(): 85 | ``instance_of(...)`` and ``not_instance_of(...)`` 86 | * Polymorphic filtering/ordering etc., allowing the use of fields of 87 | derived models ("ArtProject___artist") 88 | * Support for user-defined custom managers 89 | * Automatic inheritance of custom managers 90 | * Support for user-defined custom queryset classes 91 | * Non-polymorphic queries if needed, with no other change in 92 | features/behaviour 93 | * Combining querysets of different types/models ("qs3 = qs1 | qs2") 94 | * Nice/informative display of polymorphic queryset results 95 | 96 | 97 | More about Installation / Testing 98 | ================================= 99 | 100 | Requirements 101 | ------------ 102 | 103 | Django 1.1 (or later) and Python 2.4 or later. This code has been tested 104 | on Django 1.1 / 1.2 / 1.3 and Python 2.4.6 / 2.5.4 / 2.6.4 on Linux. 105 | 106 | Included Test Suite 107 | ------------------- 108 | 109 | The repository (or tar file) contains a complete Django project 110 | that may be used for tests or experiments, without any installation needed. 111 | 112 | To run the included test suite, in the directory "...django_polymorphic" execute:: 113 | 114 | ./manage test polymorphic 115 | 116 | The management command ``pcmd.py`` in the app ``pexp`` can be used 117 | for quick tests or experiments - modify this file (pexp/management/commands/pcmd.py) 118 | to your liking, then run:: 119 | 120 | ./manage syncdb # db is created in /var/tmp/... (settings.py) 121 | ./manage pcmd 122 | 123 | Installation 124 | ------------ 125 | 126 | In the directory "...django_polymorphic", execute ``sudo python setup.py install``. 127 | 128 | Alternatively you can simply copy the ``polymorphic`` subdirectory 129 | (under "django_polymorphic") into your Django project dir 130 | (e.g. if you want to distribute your project with more 'batteries included'). 131 | 132 | If you want to run the test cases in `polymorphic/tests.py`, you need to add 133 | ``polymorphic`` to your INSTALLED_APPS setting. 134 | 135 | Django's ContentType framework (``django.contrib.contenttypes``) 136 | needs to be listed in INSTALLED_APPS (usually it already is). 137 | 138 | 139 | More Polymorphic Functionality 140 | ============================== 141 | 142 | In the examples below, these models are being used:: 143 | 144 | from polymorphic import PolymorphicModel 145 | 146 | class ModelA(PolymorphicModel): 147 | field1 = models.CharField(max_length=10) 148 | 149 | class ModelB(ModelA): 150 | field2 = models.CharField(max_length=10) 151 | 152 | class ModelC(ModelB): 153 | field3 = models.CharField(max_length=10) 154 | 155 | 156 | Filtering for classes (equivalent to python's isinstance() ): 157 | ------------------------------------------------------------- 158 | 159 | >>> ModelA.objects.instance_of(ModelB) 160 | . 161 | [ , 162 | ] 163 | 164 | In general, including or excluding parts of the inheritance tree:: 165 | 166 | ModelA.objects.instance_of(ModelB [, ModelC ...]) 167 | ModelA.objects.not_instance_of(ModelB [, ModelC ...]) 168 | 169 | You can also use this feature in Q-objects (with the same result as above): 170 | 171 | >>> ModelA.objects.filter( Q(instance_of=ModelB) ) 172 | 173 | 174 | Polymorphic filtering (for fields in derived classes) 175 | ----------------------------------------------------- 176 | 177 | For example, cherrypicking objects from multiple derived classes 178 | anywhere in the inheritance tree, using Q objects (with the 179 | syntax: ``exact model name + three _ + field name``): 180 | 181 | >>> ModelA.objects.filter( Q(ModelB___field2 = 'B2') | Q(ModelC___field3 = 'C3') ) 182 | . 183 | [ , 184 | ] 185 | 186 | 187 | Combining Querysets 188 | ------------------- 189 | 190 | Querysets could now be regarded as object containers that allow the 191 | aggregation of different object types, very similar to python 192 | lists - as long as the objects are accessed through the manager of 193 | a common base class: 194 | 195 | >>> Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY) 196 | . 197 | [ , 198 | ] 199 | 200 | 201 | ManyToManyField, ForeignKey, OneToOneField 202 | ------------------------------------------ 203 | 204 | Relationship fields referring to polymorphic models work as 205 | expected: like polymorphic querysets they now always return the 206 | referred objects with the same type/class these were created and 207 | saved as. 208 | 209 | E.g., if in your model you define:: 210 | 211 | field1 = OneToOneField(ModelA) 212 | 213 | then field1 may now also refer to objects of type ``ModelB`` or ``ModelC``. 214 | 215 | A ManyToManyField example:: 216 | 217 | # The model holding the relation may be any kind of model, polymorphic or not 218 | class RelatingModel(models.Model): 219 | many2many = models.ManyToManyField('ModelA') # ManyToMany relation to a polymorphic model 220 | 221 | >>> o=RelatingModel.objects.create() 222 | >>> o.many2many.add(ModelA.objects.get(id=1)) 223 | >>> o.many2many.add(ModelB.objects.get(id=2)) 224 | >>> o.many2many.add(ModelC.objects.get(id=3)) 225 | 226 | >>> o.many2many.all() 227 | [ , 228 | , 229 | ] 230 | 231 | 232 | Using Third Party Models (without modifying them) 233 | ------------------------------------------------- 234 | 235 | Third party models can be used as polymorphic models without 236 | restrictions by subclassing them. E.g. using a third party 237 | model as the root of a polymorphic inheritance tree:: 238 | 239 | from thirdparty import ThirdPartyModel 240 | 241 | class MyThirdPartyBaseModel(PolymorhpicModel, ThirdPartyModel): 242 | pass # or add fields 243 | 244 | Or instead integrating the third party model anywhere into an 245 | existing polymorphic inheritance tree:: 246 | 247 | class MyBaseModel(SomePolymorphicModel): 248 | my_field = models.CharField(max_length=10) 249 | 250 | class MyModelWithThirdParty(MyBaseModel, ThirdPartyModel): 251 | pass # or add fields 252 | 253 | 254 | Non-Polymorphic Queries 255 | ----------------------- 256 | 257 | If you insert ``.non_polymorphic()`` anywhere into the query chain, then 258 | django_polymorphic will simply leave out the final step of retrieving the 259 | real objects, and the manager/queryset will return objects of the type of 260 | the base class you used for the query, like vanilla Django would 261 | (``ModelA`` in this example). 262 | 263 | >>> qs=ModelA.objects.non_polymorphic().all() 264 | >>> qs 265 | [ , 266 | , 267 | ] 268 | 269 | There are no other changes in the behaviour of the queryset. For example, 270 | enhancements for ``filter()`` or ``instance_of()`` etc. still work as expected. 271 | If you do the final step yourself, you get the usual polymorphic result: 272 | 273 | >>> ModelA.objects.get_real_instances(qs) 274 | [ , 275 | , 276 | ] 277 | 278 | 279 | About Queryset Methods 280 | ---------------------- 281 | 282 | * ``annotate()`` and ``aggregate()`` work just as usual, with the 283 | addition that the ``ModelX___field`` syntax can be used for the 284 | keyword arguments (but not for the non-keyword arguments). 285 | 286 | * ``order_by()`` now similarly supports the ``ModelX___field`` syntax 287 | for specifying ordering through a field in a submodel. 288 | 289 | * ``distinct()`` works as expected. It only regards the fields of 290 | the base class, but this should never make a difference. 291 | 292 | * ``select_related()`` works just as usual, but it can not (yet) be used 293 | to select relations in derived models 294 | (like ``ModelA.objects.select_related('ModelC___fieldxy')`` ) 295 | 296 | * ``extra()`` works as expected (it returns polymorphic results) but 297 | currently has one restriction: The resulting objects are required to have 298 | a unique primary key within the result set - otherwise an error is thrown 299 | (this case could be made to work, however it may be mostly unneeded).. 300 | The keyword-argument "polymorphic" is no longer supported. 301 | You can get back the old non-polymorphic behaviour (before V1.0) 302 | by using ``ModelA.objects.non_polymorphic().extra(...)``. 303 | 304 | * ``get_real_instances()`` allows you to turn a 305 | queryset or list of base model objects efficiently into the real objects. 306 | For example, you could do ``base_objects_queryset=ModelA.extra(...).non_polymorphic()`` 307 | and then call ``real_objects=base_objects_queryset.get_real_instances()``.Or alternatively 308 | .``real_objects=ModelA.objects..get_real_instances(base_objects_queryset_or_object_list)`` 309 | 310 | * ``values()`` & ``values_list()`` currently do not return polymorphic 311 | results. This may change in the future however. If you want to use these 312 | methods now, it's best if you use ``Model.base_objects.values...`` as 313 | this is guaranteed to not change. 314 | 315 | * ``defer()`` and ``only()`` are not yet supported (support will be added 316 | in the future). 317 | 318 | 319 | Using enhanced Q-objects in any Places 320 | -------------------------------------- 321 | 322 | The queryset enhancements (e.g. ``instance_of``) only work as arguments 323 | to the member functions of a polymorphic queryset. Occationally it may 324 | be useful to be able to use Q objects with these enhancements in other places. 325 | As Django doesn't understand these enhanced Q objects, you need to 326 | transform them manually into normal Q objects before you can feed them 327 | to a Django queryset or function:: 328 | 329 | normal_q_object = ModelA.translate_polymorphic_Q_object( Q(instance_of=Model2B) ) 330 | 331 | This function cannot be used at model creation time however (in models.py), 332 | as it may need to access the ContentTypes database table. 333 | 334 | 335 | Nicely Displaying Polymorphic Querysets 336 | --------------------------------------- 337 | 338 | In order to get the output as seen in all examples here, you need to use the 339 | ShowFieldType class mixin:: 340 | 341 | from polymorphic import PolymorphicModel, ShowFieldType 342 | 343 | class ModelA(ShowFieldType, PolymorphicModel): 344 | field1 = models.CharField(max_length=10) 345 | 346 | You may also use ShowFieldContent or ShowFieldTypeAndContent to display 347 | additional information when printing querysets (or converting them to text). 348 | 349 | When showing field contents, they will be truncated to 20 characters. You can 350 | modify this behaviour by setting a class variable in your model like this:: 351 | 352 | class ModelA(ShowFieldType, PolymorphicModel): 353 | polymorphic_showfield_max_field_width = 20 354 | ... 355 | 356 | Similarly, pre-V1.0 output formatting can be re-estated by using 357 | ``polymorphic_showfield_old_format = True``. 358 | 359 | Custom Managers, Querysets & Manager Inheritance 360 | ================================================ 361 | 362 | Using a Custom Manager 363 | ---------------------- 364 | 365 | A nice feature of Django is the possibility to define one's own custom object managers. 366 | This is fully supported with django_polymorphic: For creating a custom polymorphic 367 | manager class, just derive your manager from ``PolymorphicManager`` instead of 368 | ``models.Manager``. As with vanilla Django, in your model class, you should 369 | explicitly add the default manager first, and then your custom manager:: 370 | 371 | from polymorphic import PolymorphicModel, PolymorphicManager 372 | 373 | class TimeOrderedManager(PolymorphicManager): 374 | def get_query_set(self): 375 | qs = super(TimeOrderedManager,self).get_query_set() 376 | return qs.order_by('-start_date') # order the queryset 377 | 378 | def most_recent(self): 379 | qs = self.get_query_set() # get my ordered queryset 380 | return qs[:10] # limit => get ten most recent entries 381 | 382 | class Project(PolymorphicModel): 383 | objects = PolymorphicManager() # add the default polymorphic manager first 384 | objects_ordered = TimeOrderedManager() # then add your own manager 385 | start_date = DateTimeField() # project start is this date/time 386 | 387 | The first manager defined ('objects' in the example) is used by 388 | Django as automatic manager for several purposes, including accessing 389 | related objects. It must not filter objects and it's safest to use 390 | the plain ``PolymorphicManager`` here. 391 | 392 | Manager Inheritance 393 | ------------------- 394 | 395 | Polymorphic models inherit/propagate all managers from their 396 | base models, as long as these are polymorphic. This means that all 397 | managers defined in polymorphic base models continue to work as 398 | expected in models inheriting from this base model:: 399 | 400 | from polymorphic import PolymorphicModel, PolymorphicManager 401 | 402 | class TimeOrderedManager(PolymorphicManager): 403 | def get_query_set(self): 404 | qs = super(TimeOrderedManager,self).get_query_set() 405 | return qs.order_by('-start_date') # order the queryset 406 | 407 | def most_recent(self): 408 | qs = self.get_query_set() # get my ordered queryset 409 | return qs[:10] # limit => get ten most recent entries 410 | 411 | class Project(PolymorphicModel): 412 | objects = PolymorphicManager() # add the default polymorphic manager first 413 | objects_ordered = TimeOrderedManager() # then add your own manager 414 | start_date = DateTimeField() # project start is this date/time 415 | 416 | class ArtProject(Project): # inherit from Project, inheriting its fields and managers 417 | artist = models.CharField(max_length=30) 418 | 419 | ArtProject inherited the managers ``objects`` and ``objects_ordered`` from Project. 420 | 421 | ``ArtProject.objects_ordered.all()`` will return all art projects ordered 422 | regarding their start time and ``ArtProject.objects_ordered.most_recent()`` 423 | will return the ten most recent art projects. 424 | . 425 | 426 | Using a Custom Queryset Class 427 | ----------------------------- 428 | 429 | The ``PolymorphicManager`` class accepts one initialization argument, 430 | which is the queryset class the manager should use. Just as with vanilla Django, 431 | you may define your own custom queryset classes. Just use PolymorphicQuerySet 432 | instead of Django's QuerySet as the base class:: 433 | 434 | from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet 435 | 436 | class MyQuerySet(PolymorphicQuerySet): 437 | def my_queryset_method(...): 438 | ... 439 | 440 | class MyModel(PolymorphicModel): 441 | my_objects=PolymorphicManager(MyQuerySet) 442 | ... 443 | 444 | 445 | Performance Considerations 446 | ========================== 447 | 448 | The current implementation is rather simple and does not use any 449 | custom SQL or Django DB layer internals - it is purely based on the 450 | standard Django ORM. 451 | 452 | Specifically, the query:: 453 | 454 | result_objects = list( ModelA.objects.filter(...) ) 455 | 456 | performs one SQL query to retrieve ``ModelA`` objects and one additional 457 | query for each unique derived class occurring in result_objects. 458 | The best case for retrieving 100 objects is 1 SQL query if all are 459 | class ``ModelA``. If 50 objects are ``ModelA`` and 50 are ``ModelB``, then 460 | two queries are executed. The pathological worst case is 101 db queries if 461 | result_objects contains 100 different object types (with all of them 462 | subclasses of ``ModelA``). 463 | 464 | Usually, when Django users create their own polymorphic ad-hoc solution 465 | without a tool like django_polymorphic, this usually results in a variation of :: 466 | 467 | result_objects = [ o.get_real_instance() for o in BaseModel.objects.filter(...) ] 468 | 469 | which has very bad performance, as it introduces one additional 470 | SQL query for every object in the result which is not of class ``BaseModel``. 471 | 472 | Compared to these solutions, django_polymorphic has the advantage 473 | that it only needs one sql request per *object type*, and not *per object*. 474 | 475 | .. _performance: 476 | 477 | Performance Problems with PostgreSQL, MySQL and SQLite3 478 | ------------------------------------------------------- 479 | 480 | Current relational DBM systems seem to have general problems with 481 | the SQL queries produced by object relational mappers like the Django 482 | ORM, if these use multi-table inheritance like Django's ORM does. 483 | The "inner joins" in these queries can perform very badly. 484 | This is independent of django_polymorphic and affects all uses of 485 | multi table Model inheritance. 486 | 487 | Concrete benchmark results are forthcoming (please see discussion forum). 488 | 489 | Please also see this `post (and comments) from Jacob Kaplan-Moss`_. 490 | 491 | .. _post (and comments) from Jacob Kaplan-Moss: http://www.jacobian.org/writing/concrete-inheritance/ 492 | 493 | 494 | .. _restrictions: 495 | 496 | Restrictions & Caveats 497 | ====================== 498 | 499 | * Database Performance regarding concrete Model inheritance in general. 500 | Please see "Performance Problems" above. 501 | 502 | * Queryset methods ``values()``, ``values_list()``, ``select_related()``, 503 | ``defer()`` and ``only()`` are not yet fully supported (see above). 504 | ``extra()`` has one restriction: the resulting objects are required to have 505 | a unique primary key within the result set. 506 | 507 | * Django Admin Integration: There currently is no specific admin integration, 508 | but it would most likely make sense to have one. 509 | 510 | * Diamond shaped inheritance: There seems to be a general problem 511 | with diamond shaped multiple model inheritance with Django models 512 | (tested with V1.1 - V1.3). 513 | An example is here: http://code.djangoproject.com/ticket/10808. 514 | This problem is aggravated when trying to enhance models.Model 515 | by subclassing it instead of modifying Django core (as we do here 516 | with PolymorphicModel). 517 | 518 | * The enhanced filter-definitions/Q-objects only work as arguments 519 | for the methods of the polymorphic querysets. Please see above 520 | for ``translate_polymorphic_Q_object``. 521 | 522 | * A reference (``ContentType``) to the real/leaf model is stored 523 | in the base model (the base model directly inheriting from 524 | PolymorphicModel). You need to be aware of this when using the 525 | ``dumpdata`` management command or any other low-level 526 | database operations. E.g. if you rename models or apps or copy 527 | objects from one database to another, then Django's ContentType 528 | table needs to be corrected/copied too. This is of course generally 529 | the case for any models using Django's ContentType. 530 | 531 | * Django 1.1 only - the names of polymorphic models must be unique 532 | in the whole project, even if they are in two different apps. 533 | This results from a restriction in the Django 1.1 "related_name" 534 | option (fixed in Django 1.2). 535 | 536 | * Django 1.1 only - when ContentType is used in models, Django's 537 | seralisation or fixtures cannot be used (all polymorphic models 538 | use ContentType). This issue seems to be resolved for Django 1.2 539 | (changeset 11863: Fixed #7052, Added support for natural keys in serialization). 540 | 541 | + http://code.djangoproject.com/ticket/7052 542 | + http://stackoverflow.com/questions/853796/problems-with-contenttypes-when-loading-a-fixture-in-django 543 | 544 | 545 | Project Status 546 | ============== 547 | 548 | Django_polymorphic works well for a considerable number of users now, 549 | and no major problems have shown up for many months. 550 | The API can be considered stable beginning with the V1.0 release. 551 | 552 | 553 | Links 554 | ===== 555 | 556 | - http://code.djangoproject.com/wiki/ModelInheritance 557 | - http://lazypython.blogspot.com/2009/02/second-look-at-inheritance-and.html 558 | - http://www.djangosnippets.org/snippets/1031/ 559 | - http://www.djangosnippets.org/snippets/1034/ 560 | - http://groups.google.com/group/django-developers/browse_frm/thread/7d40ad373ebfa912/a20fabc661b7035d?lnk=gst&q=model+inheritance+CORBA#a20fabc661b7035d 561 | - http://groups.google.com/group/django-developers/browse_thread/thread/9bc2aaec0796f4e0/0b92971ffc0aa6f8?lnk=gst&q=inheritance#0b92971ffc0aa6f8 562 | - http://groups.google.com/group/django-developers/browse_thread/thread/3947c594100c4adb/d8c0af3dacad412d?lnk=gst&q=inheritance#d8c0af3dacad412d 563 | - http://groups.google.com/group/django-users/browse_thread/thread/52f72cffebb705e/b76c9d8c89a5574f 564 | - http://peterbraden.co.uk/article/django-inheritance 565 | - http://www.hopelessgeek.com/2009/11/25/a-hack-for-multi-table-inheritance-in-django 566 | - http://stackoverflow.com/questions/929029/how-do-i-access-the-child-classes-of-an-object-in-django-without-knowing-the-name/929982#929982 567 | - http://stackoverflow.com/questions/1581024/django-inheritance-how-to-have-one-method-for-all-subclasses 568 | - http://groups.google.com/group/django-users/browse_thread/thread/cbdaf2273781ccab/e676a537d735d9ef?lnk=gst&q=polymorphic#e676a537d735d9ef 569 | - http://groups.google.com/group/django-users/browse_thread/thread/52f72cffebb705e/bc18c18b2e83881e?lnk=gst&q=model+inheritance#bc18c18b2e83881e 570 | - http://code.djangoproject.com/ticket/10808 571 | - http://code.djangoproject.com/ticket/7270 572 | 573 | -------------------------------------------------------------------------------- /polymorphic/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Test Cases 3 | Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic 4 | """ 5 | 6 | import settings 7 | import sys 8 | from pprint import pprint 9 | 10 | from django import VERSION as django_VERSION 11 | from django.test import TestCase 12 | from django.db.models.query import QuerySet 13 | from django.db.models import Q,Count 14 | from django.db import models 15 | from django.contrib.contenttypes.models import ContentType 16 | 17 | from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet 18 | from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent, get_version 19 | from polymorphic import translate_polymorphic_Q_object 20 | 21 | class PlainA(models.Model): 22 | field1 = models.CharField(max_length=10) 23 | class PlainB(PlainA): 24 | field2 = models.CharField(max_length=10) 25 | class PlainC(PlainB): 26 | field3 = models.CharField(max_length=10) 27 | 28 | class Model2A(ShowFieldType, PolymorphicModel): 29 | field1 = models.CharField(max_length=10) 30 | class Model2B(Model2A): 31 | field2 = models.CharField(max_length=10) 32 | class Model2C(Model2B): 33 | field3 = models.CharField(max_length=10) 34 | class Model2D(Model2C): 35 | field4 = models.CharField(max_length=10) 36 | 37 | class ModelExtraA(ShowFieldTypeAndContent, PolymorphicModel): 38 | field1 = models.CharField(max_length=10) 39 | class ModelExtraB(ModelExtraA): 40 | field2 = models.CharField(max_length=10) 41 | class ModelExtraC(ModelExtraB): 42 | field3 = models.CharField(max_length=10) 43 | class ModelExtraExternal(models.Model): 44 | topic = models.CharField(max_length=10) 45 | 46 | class ModelShow1(ShowFieldType,PolymorphicModel): 47 | field1 = models.CharField(max_length=10) 48 | m2m = models.ManyToManyField('self') 49 | class ModelShow2(ShowFieldContent, PolymorphicModel): 50 | field1 = models.CharField(max_length=10) 51 | m2m = models.ManyToManyField('self') 52 | class ModelShow3(ShowFieldTypeAndContent, PolymorphicModel): 53 | field1 = models.CharField(max_length=10) 54 | m2m = models.ManyToManyField('self') 55 | 56 | class ModelShow1_plain(PolymorphicModel): 57 | field1 = models.CharField(max_length=10) 58 | class ModelShow2_plain(ModelShow1_plain): 59 | field2 = models.CharField(max_length=10) 60 | 61 | 62 | class Base(ShowFieldType, PolymorphicModel): 63 | field_b = models.CharField(max_length=10) 64 | class ModelX(Base): 65 | field_x = models.CharField(max_length=10) 66 | class ModelY(Base): 67 | field_y = models.CharField(max_length=10) 68 | 69 | class Enhance_Plain(models.Model): 70 | field_p = models.CharField(max_length=10) 71 | class Enhance_Base(ShowFieldTypeAndContent, PolymorphicModel): 72 | field_b = models.CharField(max_length=10) 73 | class Enhance_Inherit(Enhance_Base, Enhance_Plain): 74 | field_i = models.CharField(max_length=10) 75 | 76 | class DiamondBase(models.Model): 77 | field_b = models.CharField(max_length=10) 78 | class DiamondX(DiamondBase): 79 | field_x = models.CharField(max_length=10) 80 | class DiamondY(DiamondBase): 81 | field_y = models.CharField(max_length=10) 82 | class DiamondXY(DiamondX, DiamondY): 83 | pass 84 | 85 | class RelationBase(ShowFieldTypeAndContent, PolymorphicModel): 86 | field_base = models.CharField(max_length=10) 87 | fk = models.ForeignKey('self', null=True) 88 | m2m = models.ManyToManyField('self') 89 | class RelationA(RelationBase): 90 | field_a = models.CharField(max_length=10) 91 | class RelationB(RelationBase): 92 | field_b = models.CharField(max_length=10) 93 | class RelationBC(RelationB): 94 | field_c = models.CharField(max_length=10) 95 | 96 | class RelatingModel(models.Model): 97 | many2many = models.ManyToManyField(Model2A) 98 | 99 | class One2OneRelatingModel(PolymorphicModel): 100 | one2one = models.OneToOneField(Model2A) 101 | field1 = models.CharField(max_length=10) 102 | 103 | class One2OneRelatingModelDerived(One2OneRelatingModel): 104 | field2 = models.CharField(max_length=10) 105 | 106 | class MyManager(PolymorphicManager): 107 | def get_query_set(self): 108 | return super(MyManager, self).get_query_set().order_by('-field1') 109 | class ModelWithMyManager(ShowFieldTypeAndContent, Model2A): 110 | objects = MyManager() 111 | field4 = models.CharField(max_length=10) 112 | 113 | class MROBase1(ShowFieldType, PolymorphicModel): 114 | objects = MyManager() 115 | field1 = models.CharField(max_length=10) # needed as MyManager uses it 116 | class MROBase2(MROBase1): 117 | pass # Django vanilla inheritance does not inherit MyManager as _default_manager here 118 | class MROBase3(models.Model): 119 | objects = PolymorphicManager() 120 | class MRODerived(MROBase2, MROBase3): 121 | pass 122 | 123 | class MgrInheritA(models.Model): 124 | mgrA = models.Manager() 125 | mgrA2 = models.Manager() 126 | field1 = models.CharField(max_length=10) 127 | class MgrInheritB(MgrInheritA): 128 | mgrB = models.Manager() 129 | field2 = models.CharField(max_length=10) 130 | class MgrInheritC(ShowFieldTypeAndContent, MgrInheritB): 131 | pass 132 | 133 | class BlogBase(ShowFieldTypeAndContent, PolymorphicModel): 134 | name = models.CharField(max_length=10) 135 | class BlogA(BlogBase): 136 | info = models.CharField(max_length=10) 137 | class BlogB(BlogBase): 138 | pass 139 | class BlogEntry(ShowFieldTypeAndContent, PolymorphicModel): 140 | blog = models.ForeignKey(BlogA) 141 | text = models.CharField(max_length=10) 142 | 143 | class BlogEntry_limit_choices_to(ShowFieldTypeAndContent, PolymorphicModel): 144 | blog = models.ForeignKey(BlogBase) 145 | text = models.CharField(max_length=10) 146 | 147 | class ModelFieldNameTest(ShowFieldType, PolymorphicModel): 148 | modelfieldnametest = models.CharField(max_length=10) 149 | 150 | class InitTestModel(ShowFieldType, PolymorphicModel): 151 | bar = models.CharField(max_length=100) 152 | def __init__(self, *args, **kwargs): 153 | kwargs['bar'] = self.x() 154 | super(InitTestModel, self).__init__(*args, **kwargs) 155 | class InitTestModelSubclass(InitTestModel): 156 | def x(self): 157 | return 'XYZ' 158 | 159 | # models from github issue 160 | class Top(PolymorphicModel): 161 | name = models.CharField(max_length=50) 162 | class Middle(Top): 163 | description = models.TextField() 164 | class Bottom(Middle): 165 | author = models.CharField(max_length=50) 166 | 167 | 168 | # UUID tests won't work with Django 1.1 169 | if not (django_VERSION[0] <= 1 and django_VERSION[1] <= 1): 170 | try: from polymorphic.tools_for_tests import UUIDField 171 | except: pass 172 | if 'UUIDField' in globals(): 173 | import uuid 174 | 175 | class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel): 176 | uuid_primary_key = UUIDField(primary_key = True) 177 | topic = models.CharField(max_length = 30) 178 | class UUIDArtProject(UUIDProject): 179 | artist = models.CharField(max_length = 30) 180 | class UUIDResearchProject(UUIDProject): 181 | supervisor = models.CharField(max_length = 30) 182 | 183 | class UUIDPlainA(models.Model): 184 | uuid_primary_key = UUIDField(primary_key = True) 185 | field1 = models.CharField(max_length=10) 186 | class UUIDPlainB(UUIDPlainA): 187 | field2 = models.CharField(max_length=10) 188 | class UUIDPlainC(UUIDPlainB): 189 | field3 = models.CharField(max_length=10) 190 | 191 | 192 | # test bad field name 193 | #class TestBadFieldModel(ShowFieldType, PolymorphicModel): 194 | # instance_of = models.CharField(max_length=10) 195 | 196 | # validation error: "polymorphic.relatednameclash: Accessor for field 'polymorphic_ctype' clashes 197 | # with related field 'ContentType.relatednameclash_set'." (reported by Andrew Ingram) 198 | # fixed with related_name 199 | class RelatedNameClash(ShowFieldType, PolymorphicModel): 200 | ctype = models.ForeignKey(ContentType, null=True, editable=False) 201 | 202 | 203 | class testclass(TestCase): 204 | def test_diamond_inheritance(self): 205 | # Django diamond problem 206 | o = DiamondXY.objects.create(field_b='b', field_x='x', field_y='y') 207 | print 'DiamondXY fields 1: field_b "%s", field_x "%s", field_y "%s"' % (o.field_b, o.field_x, o.field_y) 208 | o = DiamondXY.objects.get() 209 | print 'DiamondXY fields 2: field_b "%s", field_x "%s", field_y "%s"' % (o.field_b, o.field_x, o.field_y) 210 | if o.field_b != 'b': 211 | print 212 | print '# known django model inheritance diamond problem detected' 213 | 214 | def test_annotate_aggregate_order(self): 215 | 216 | # create a blog of type BlogA 217 | blog = BlogA.objects.create(name='B1', info='i1') 218 | # create two blog entries in BlogA 219 | entry1 = blog.blogentry_set.create(text='bla') 220 | entry2 = BlogEntry.objects.create(blog=blog, text='bla2') 221 | 222 | # create some blogs of type BlogB to make the BlogBase table data really polymorphic 223 | o = BlogB.objects.create(name='Bb1') 224 | o = BlogB.objects.create(name='Bb2') 225 | o = BlogB.objects.create(name='Bb3') 226 | 227 | qs = BlogBase.objects.annotate(entrycount=Count('BlogA___blogentry')) 228 | 229 | assert len(qs)==4 230 | 231 | for o in qs: 232 | if o.name=='B1': 233 | assert o.entrycount == 2 234 | else: 235 | assert o.entrycount == 0 236 | 237 | x = BlogBase.objects.aggregate(entrycount=Count('BlogA___blogentry')) 238 | assert x['entrycount'] == 2 239 | 240 | # create some more blogs for next test 241 | b2 = BlogA.objects.create(name='B2', info='i2') 242 | b2 = BlogA.objects.create(name='B3', info='i3') 243 | b2 = BlogA.objects.create(name='B4', info='i4') 244 | b2 = BlogA.objects.create(name='B5', info='i5') 245 | 246 | ### test ordering for field in all entries 247 | 248 | expected = ''' 249 | [ , 250 | , 251 | , 252 | , 253 | , 254 | , 255 | , 256 | ]''' 257 | x = '\n' + repr(BlogBase.objects.order_by('-name')) 258 | assert x == expected 259 | 260 | ### test ordering for field in one subclass only 261 | 262 | # MySQL and SQLite return this order 263 | expected1=''' 264 | [ , 265 | , 266 | , 267 | , 268 | , 269 | , 270 | , 271 | ]''' 272 | 273 | # PostgreSQL returns this order 274 | expected2=''' 275 | [ , 276 | , 277 | , 278 | , 279 | , 280 | , 281 | , 282 | ]''' 283 | 284 | x = '\n' + repr(BlogBase.objects.order_by('-BlogA___info')) 285 | assert x == expected1 or x == expected2 286 | 287 | 288 | def test_limit_choices_to(self): 289 | "this is not really a testcase, as limit_choices_to only affects the Django admin" 290 | # create a blog of type BlogA 291 | blog_a = BlogA.objects.create(name='aa', info='aa') 292 | blog_b = BlogB.objects.create(name='bb') 293 | # create two blog entries 294 | entry1 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text='bla2') 295 | entry2 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text='bla2') 296 | 297 | 298 | def test_primary_key_custom_field_problem(self): 299 | "object retrieval problem occuring with some custom primary key fields (UUIDField as test case)" 300 | if not 'UUIDField' in globals(): return 301 | a=UUIDProject.objects.create(topic="John's gathering") 302 | b=UUIDArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner") 303 | c=UUIDResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter") 304 | qs=UUIDProject.objects.all() 305 | ol=list(qs) 306 | a=qs[0] 307 | b=qs[1] 308 | c=qs[2] 309 | assert len(qs)==3 310 | assert type(a.uuid_primary_key)==uuid.UUID and type(a.pk)==uuid.UUID 311 | res=repr(qs) 312 | import re 313 | res=re.sub(' "(.*?)..", topic',', topic',res) 314 | res_exp="""[ , 315 | , 316 | ]""" 317 | assert res==res_exp, res 318 | #if (a.pk!= uuid.UUID or c.pk!= uuid.UUID): 319 | # print 320 | # print '# known inconstency with custom primary key field detected (django problem?)' 321 | 322 | a=UUIDPlainA.objects.create(field1='A1') 323 | b=UUIDPlainB.objects.create(field1='B1', field2='B2') 324 | c=UUIDPlainC.objects.create(field1='C1', field2='C2', field3='C3') 325 | qs=UUIDPlainA.objects.all() 326 | if (a.pk!= uuid.UUID or c.pk!= uuid.UUID): 327 | print 328 | print '# known type inconstency with custom primary key field detected (django problem?)' 329 | 330 | 331 | def show_base_manager(model): 332 | print type(model._base_manager),model._base_manager.model 333 | 334 | __test__ = {"doctest": """ 335 | ####################################################### 336 | ### Tests 337 | 338 | >>> settings.DEBUG=True 339 | 340 | 341 | ### simple inheritance 342 | 343 | >>> o=Model2A.objects.create(field1='A1') 344 | >>> o=Model2B.objects.create(field1='B1', field2='B2') 345 | >>> o=Model2C.objects.create(field1='C1', field2='C2', field3='C3') 346 | >>> o=Model2D.objects.create(field1='D1', field2='D2', field3='D3', field4='D4') 347 | >>> Model2A.objects.all() 348 | [ , 349 | , 350 | , 351 | ] 352 | 353 | # manual get_real_instance() 354 | >>> o=Model2A.objects.non_polymorphic().get(field1='C1') 355 | >>> o.get_real_instance() 356 | 357 | 358 | # non_polymorphic() 359 | >>> qs=Model2A.objects.all().non_polymorphic() 360 | >>> qs 361 | [ , 362 | , 363 | , 364 | ] 365 | 366 | # get_real_instances() 367 | >>> qs.get_real_instances() 368 | [ , 369 | , 370 | , 371 | ] 372 | 373 | >>> l=list(qs) 374 | >>> Model2A.objects.get_real_instances(l) 375 | [ , 376 | , 377 | , 378 | ] 379 | 380 | # translate_polymorphic_Q_object 381 | >>> q=Model2A.translate_polymorphic_Q_object( Q(instance_of=Model2C) ) 382 | >>> Model2A.objects.filter(q) 383 | [ , 384 | ] 385 | 386 | 387 | ### test inheritance pointers & _base_managers 388 | 389 | >>> show_base_manager(PlainA) 390 | 391 | >>> show_base_manager(PlainB) 392 | 393 | >>> show_base_manager(PlainC) 394 | 395 | >>> show_base_manager(Model2A) 396 | 397 | >>> show_base_manager(Model2B) 398 | 399 | >>> show_base_manager(Model2C) 400 | 401 | >>> show_base_manager(One2OneRelatingModel) 402 | 403 | >>> show_base_manager(One2OneRelatingModelDerived) 404 | 405 | 406 | >>> o=Model2A.base_objects.get(field1='C1') 407 | >>> o.model2b 408 | 409 | 410 | >>> o=Model2B.base_objects.get(field1='C1') 411 | >>> o.model2c 412 | 413 | 414 | 415 | ### OneToOneField, test both directions for polymorphism 416 | 417 | >>> a=Model2A.base_objects.get(field1='C1') 418 | >>> b=One2OneRelatingModelDerived.objects.create(one2one=a, field1='f1', field2='f2') 419 | >>> b.one2one # this result is basically wrong, probably due to Django cacheing (we used base_objects), but should not be a problem 420 | 421 | >>> c=One2OneRelatingModelDerived.objects.get(field1='f1') 422 | >>> c.one2one 423 | 424 | 425 | >>> a.one2onerelatingmodel 426 | 427 | 428 | 429 | ### ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent, also with annotate() 430 | 431 | >>> o=ModelShow1.objects.create(field1='abc') 432 | >>> o.m2m.add(o) ; o.save() 433 | >>> ModelShow1.objects.all() 434 | [ ] 435 | 436 | >>> o=ModelShow2.objects.create(field1='abc') 437 | >>> o.m2m.add(o) ; o.save() 438 | >>> ModelShow2.objects.all() 439 | [ ] 440 | 441 | >>> o=ModelShow3.objects.create(field1='abc') 442 | >>> o.m2m.add(o) ; o.save() 443 | >>> ModelShow3.objects.all() 444 | [ ] 445 | 446 | >>> ModelShow1.objects.all().annotate(Count('m2m')) 447 | [ ] 448 | >>> ModelShow2.objects.all().annotate(Count('m2m')) 449 | [ ] 450 | >>> ModelShow3.objects.all().annotate(Count('m2m')) 451 | [ ] 452 | 453 | # no pretty printing 454 | >>> o=ModelShow1_plain.objects.create(field1='abc') 455 | >>> o=ModelShow2_plain.objects.create(field1='abc', field2='def') 456 | >>> ModelShow1_plain.objects.all() 457 | [, ] 458 | 459 | 460 | ### extra() method 461 | 462 | >>> Model2A.objects.extra(where=['id IN (2, 3)']) 463 | [ , 464 | ] 465 | 466 | >>> Model2A.objects.extra(select={"select_test": "field1 = 'A1'"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by = ['-id'] ) 467 | [ , 468 | ] 469 | 470 | >>> o=ModelExtraA.objects.create(field1='A1') 471 | >>> o=ModelExtraB.objects.create(field1='B1', field2='B2') 472 | >>> o=ModelExtraC.objects.create(field1='C1', field2='C2', field3='C3') 473 | >>> o=ModelExtraExternal.objects.create(topic='extra1') 474 | >>> o=ModelExtraExternal.objects.create(topic='extra2') 475 | >>> o=ModelExtraExternal.objects.create(topic='extra3') 476 | >>> ModelExtraA.objects.extra(tables=["polymorphic_modelextraexternal"], select={"topic":"polymorphic_modelextraexternal.topic"}, where=["polymorphic_modelextraa.id = polymorphic_modelextraexternal.id"] ) 477 | [ , 478 | , 479 | ] 480 | 481 | ### class filtering, instance_of, not_instance_of 482 | 483 | >>> Model2A.objects.instance_of(Model2B) 484 | [ , 485 | , 486 | ] 487 | 488 | >>> Model2A.objects.filter(instance_of=Model2B) 489 | [ , 490 | , 491 | ] 492 | 493 | >>> Model2A.objects.filter(Q(instance_of=Model2B)) 494 | [ , 495 | , 496 | ] 497 | 498 | >>> Model2A.objects.not_instance_of(Model2B) 499 | [ ] 500 | 501 | 502 | ### polymorphic filtering 503 | 504 | >>> Model2A.objects.filter( Q( Model2B___field2 = 'B2' ) | Q( Model2C___field3 = 'C3' ) ) 505 | [ , 506 | ] 507 | 508 | 509 | ### get & delete 510 | 511 | >>> oa=Model2A.objects.get(id=2) 512 | >>> oa 513 | 514 | 515 | >>> oa.delete() 516 | >>> Model2A.objects.all() 517 | [ , 518 | , 519 | ] 520 | 521 | 522 | ### queryset combining 523 | 524 | >>> o=ModelX.objects.create(field_x='x') 525 | >>> o=ModelY.objects.create(field_y='y') 526 | 527 | >>> Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY) 528 | [ , 529 | ] 530 | 531 | 532 | ### multiple inheritance, subclassing third party models (mix PolymorphicModel with models.Model) 533 | 534 | >>> o = Enhance_Base.objects.create(field_b='b-base') 535 | >>> o = Enhance_Inherit.objects.create(field_b='b-inherit', field_p='p', field_i='i') 536 | 537 | >>> Enhance_Base.objects.all() 538 | [ , 539 | ] 540 | 541 | 542 | ### ForeignKey, ManyToManyField 543 | 544 | >>> obase=RelationBase.objects.create(field_base='base') 545 | >>> oa=RelationA.objects.create(field_base='A1', field_a='A2', fk=obase) 546 | >>> ob=RelationB.objects.create(field_base='B1', field_b='B2', fk=oa) 547 | >>> oc=RelationBC.objects.create(field_base='C1', field_b='C2', field_c='C3', fk=oa) 548 | >>> oa.m2m.add(oa); oa.m2m.add(ob) 549 | 550 | >>> RelationBase.objects.all() 551 | [ , 552 | , 553 | , 554 | ] 555 | 556 | >>> oa=RelationBase.objects.get(id=2) 557 | >>> oa.fk 558 | 559 | 560 | >>> oa.relationbase_set.all() 561 | [ , 562 | ] 563 | 564 | >>> ob=RelationBase.objects.get(id=3) 565 | >>> ob.fk 566 | 567 | 568 | >>> oa=RelationA.objects.get() 569 | >>> oa.m2m.all() 570 | [ , 571 | ] 572 | 573 | ### user-defined manager 574 | 575 | >>> o=ModelWithMyManager.objects.create(field1='D1a', field4='D4a') 576 | >>> o=ModelWithMyManager.objects.create(field1='D1b', field4='D4b') 577 | 578 | >>> ModelWithMyManager.objects.all() 579 | [ , 580 | ] 581 | 582 | >>> type(ModelWithMyManager.objects) 583 | 584 | >>> type(ModelWithMyManager._default_manager) 585 | 586 | 587 | 588 | ### Manager Inheritance 589 | 590 | >>> type(MRODerived.objects) # MRO 591 | 592 | 593 | # check for correct default manager 594 | >>> type(MROBase1._default_manager) 595 | 596 | 597 | # Django vanilla inheritance does not inherit MyManager as _default_manager here 598 | >>> type(MROBase2._default_manager) 599 | 600 | 601 | 602 | ### fixed issue in PolymorphicModel.__getattribute__: field name same as model name 603 | >>> ModelFieldNameTest.objects.create(modelfieldnametest='1') 604 | 605 | 606 | 607 | ### fixed issue in PolymorphicModel.__getattribute__: 608 | # if subclass defined __init__ and accessed class members, __getattribute__ had a problem: "...has no attribute 'sub_and_superclass_dict'" 609 | #>>> o 610 | >>> o = InitTestModelSubclass.objects.create() 611 | >>> o.bar 612 | 'XYZ' 613 | 614 | 615 | ### Django model inheritance diamond problem, fails for Django 1.1 616 | 617 | #>>> o=DiamondXY.objects.create(field_b='b', field_x='x', field_y='y') 618 | #>>> print 'DiamondXY fields 1: field_b "%s", field_x "%s", field_y "%s"' % (o.field_b, o.field_x, o.field_y) 619 | #DiamondXY fields 1: field_b "a", field_x "x", field_y "y" 620 | 621 | # test for github issue 622 | >>> t = Top() 623 | >>> t.save() 624 | >>> m = Middle() 625 | >>> m.save() 626 | >>> b = Bottom() 627 | >>> b.save() 628 | >>> Top.objects.all() 629 | [, , ] 630 | >>> Middle.objects.all() 631 | [, ] 632 | >>> Bottom.objects.all() 633 | [] 634 | 635 | 636 | >>> settings.DEBUG=False 637 | 638 | """} 639 | 640 | --------------------------------------------------------------------------------