diff --git a/.gitignore b/.gitignore index ae35267..680811d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,11 +15,14 @@ pushhg pushreg pbackup mcmd.py +dbconfig_local.py pip-log.txt build ppreadme.py ppdocs.py +common.css +screen.css README.html DOCS.html diff --git a/pexp/management/commands/pcmd.py b/pexp/management/commands/pcmd.py index cd0fd65..90c7c29 100644 --- a/pexp/management/commands/pcmd.py +++ b/pexp/management/commands/pcmd.py @@ -22,13 +22,15 @@ class Command(NoArgsCommand): def handle_noargs(self, **options): print 'polycmd - sqlite test db is stored in:',settings.DATABASE_NAME print - + + """ ModelA.objects.all().delete() o=ModelA.objects.create(field1='A1') o=ModelB.objects.create(field1='B1', field2='B2') o=ModelC.objects.create(field1='C1', field2='C2', field3='C3') print ModelA.objects.all() print + """ Project.objects.all().delete() o=Project.objects.create(topic="John's gathering") diff --git a/pexp/models.py b/pexp/models.py index ab878d9..3471b64 100644 --- a/pexp/models.py +++ b/pexp/models.py @@ -2,29 +2,24 @@ from django.db import models -from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet, ShowFields, ShowFieldsAndTypes +from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet +from polymorphic.showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent -class Project(ShowFields, PolymorphicModel): +class Project(ShowFieldContent, PolymorphicModel): topic = models.CharField(max_length=30) class ArtProject(Project): artist = models.CharField(max_length=30) class ResearchProject(Project): supervisor = models.CharField(max_length=30) -class ModelA(PolymorphicModel): +class ModelA(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=10) class ModelB(ModelA): field2 = models.CharField(max_length=10) class ModelC(ModelB): field3 = models.CharField(max_length=10) -class SModelA(ShowFieldsAndTypes, PolymorphicModel): - field1 = models.CharField(max_length=10) -class SModelB(SModelA): - field2 = models.CharField(max_length=10) -class SModelC(SModelB): - field3 = models.CharField(max_length=10) # for Django 1.2+, test models with same names in different apps # (the other models with identical names are in polymorphic/tests.py) diff --git a/polymorphic/__init__.py b/polymorphic/__init__.py index 4272521..15e3430 100644 --- a/polymorphic/__init__.py +++ b/polymorphic/__init__.py @@ -10,7 +10,8 @@ Please see LICENSE and AUTHORS for more information. from polymorphic_model import PolymorphicModel from manager import PolymorphicManager from query import PolymorphicQuerySet -from showfields import ShowFields, ShowFieldsAndTypes +from showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent +#from showfields import ShowFieldTypes, ShowFields, ShowFieldsAndTypes # import old names for compatibility VERSION = (0, 5, 0, 'beta') diff --git a/polymorphic/polymorphic_model.py b/polymorphic/polymorphic_model.py index 1b00c79..2b7bdd4 100644 --- a/polymorphic/polymorphic_model.py +++ b/polymorphic/polymorphic_model.py @@ -26,13 +26,13 @@ from django import VERSION as django_VERSION from base import PolymorphicModelBase from manager import PolymorphicManager from query import PolymorphicQuerySet -from showfields import ShowFieldTypes +from showfields import ShowFieldType ################################################################################### ### PolymorphicModel -class PolymorphicModel(ShowFieldTypes, models.Model): +class PolymorphicModel(models.Model): """ Abstract base class that provides polymorphic behaviour for any model directly or indirectly derived from it. @@ -53,7 +53,11 @@ class PolymorphicModel(ShowFieldTypes, models.Model): """ __metaclass__ = PolymorphicModelBase - polymorphic_model_marker = True # for PolymorphicModelBase + # for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing) + polymorphic_model_marker = True + + # for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery + polymorphic_query_multiline_output = False class Meta: abstract = True @@ -66,7 +70,7 @@ class PolymorphicModel(ShowFieldTypes, models.Model): p_related_name_template = 'polymorphic_%(app_label)s.%(class)s_set' polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False, related_name=p_related_name_template) - + # some applications want to know the name of the fields that are added to its models polymorphic_internal_model_fields = [ 'polymorphic_ctype' ] @@ -130,10 +134,12 @@ class PolymorphicModel(ShowFieldTypes, models.Model): name = model.__name__.lower() if as_ptr: name+='_ptr' result[name] = model + def add_all_base_models(model, result): add_if_regular_sub_or_super_class(model, True, result) for b in model.__bases__: add_all_base_models(b, result) + def add_sub_models(model, result): for b in model.__subclasses__(): add_if_regular_sub_or_super_class(b, False, result) @@ -150,6 +156,6 @@ class PolymorphicModel(ShowFieldTypes, models.Model): attr = model.base_objects.get(id=id) #print '---',self.__class__.__name__,name return attr + return super(PolymorphicModel, self).__getattribute__(name) - diff --git a/polymorphic/query.py b/polymorphic/query.py index d9b5f2d..113ec90 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -144,6 +144,7 @@ class PolymorphicQuerySet(QuerySet): for modelclass, idlist in idlist_per_model.items(): qs = modelclass.base_objects.filter(id__in=idlist) qs.dup_select_related(self) # copy select related configuration to new qs + for o in qs: if self.query.aggregates: for anno in self.query.aggregates.keys(): @@ -153,6 +154,13 @@ class PolymorphicQuerySet(QuerySet): # re-create correct order and return result list resultlist = [ results[ordered_id] for ordered_id in ordered_id_list if ordered_id in results ] + + # set polymorphic_annotate_names in all objects (currently just used for debugging/printing) + if self.query.aggregates: + annotate_names=self.query.aggregates.keys() # get annotate fields list + for o in resultlist: + o.polymorphic_annotate_names=annotate_names + return resultlist def iterator(self): @@ -182,7 +190,9 @@ class PolymorphicQuerySet(QuerySet): reached_end = False for i in range(Polymorphic_QuerySet_objects_per_request): - try: base_result_objects.append(base_iter.next()) + try: + o=base_iter.next() + base_result_objects.append(o) except StopIteration: reached_end = True break @@ -194,8 +204,10 @@ class PolymorphicQuerySet(QuerySet): if reached_end: raise StopIteration - def __repr__(self): - result = [ repr(o) for o in self.all() ] - return '[ ' + ',\n '.join(result) + ' ]' - + def __repr__(self, *args, **kwargs): + if self.model.polymorphic_query_multiline_output: + result = [ repr(o) for o in self.all() ] + return '[ ' + ',\n '.join(result) + ' ]' + else: + return super(PolymorphicQuerySet,self).__repr__(*args, **kwargs) diff --git a/polymorphic/showfields.py b/polymorphic/showfields.py index c00fc97..464f9eb 100644 --- a/polymorphic/showfields.py +++ b/polymorphic/showfields.py @@ -2,49 +2,67 @@ from django.db import models -def _represent_foreign_key(o): - if o is None: - out = '"None"' - else: - out = '"' + o.__class__.__name__ + '"' - return out -class ShowFieldsAndTypes(object): - """ model mixin, like ShowFields, but also show field types """ - def __repr__(self): - out = 'id ' + str(self.pk) - for f in self._meta.fields: - if f.name in [ 'id' ] + self.polymorphic_internal_model_fields or 'ptr' in f.name: continue - out += ', ' + f.name + ' (' + type(f).__name__ + ')' - if isinstance(f, (models.ForeignKey)): - o = getattr(self, f.name) - out += ': ' + _represent_foreign_key(o) - else: - out += ': "' + getattr(self, f.name) + '"' - return '<' + self.__class__.__name__ + ': ' + out + '>' +class ShowFieldBase(object): + """ base class for the ShowField... model mixins, does the work """ + polymorphic_query_multiline_output = True # cause nicer multiline PolymorphicQuery output + + polymorphic_showfield_type = False + polymorphic_showfield_content = False -class ShowFields(object): - """ model mixin that shows the object's class, it's fields and field contents """ def __repr__(self): - out = 'id ' + str(self.pk) + ', ' - for f in self._meta.fields: + return self.__unicode__() + + def __unicode__(self): + out = u'<'+self.__class__.__name__+': id %s' % unicode(self.pk) + for f in self._meta.fields + self._meta.many_to_many: + if f.name in [ 'id' ] + self.polymorphic_internal_model_fields or 'ptr' in f.name: continue out += ', ' + f.name - if isinstance(f, (models.ForeignKey)): + + if self.polymorphic_showfield_type: + out += ' (' + type(f).__name__ + ')' + + if self.polymorphic_showfield_content: o = getattr(self, f.name) - out += ': ' + _represent_foreign_key(o) - else: - out += ': "' + getattr(self, f.name) + '"' - return '<' + (self.__class__.__name__ + ': ') + out + '>' -class ShowFieldTypes(object): - """ INTERNAL; don't use this! - This mixin is already used by default by PolymorphicModel. - (model mixin that shows the object's class and it's field types) """ - def __repr__(self): - out = self.__class__.__name__ + ': id ' + str(self.pk) - for f in self._meta.fields: - if f.name in [ 'id' ] + self.polymorphic_internal_model_fields or 'ptr' in f.name: continue - out += ', ' + f.name + ' (' + type(f).__name__ + ')' - return '<' + out + '>' + if isinstance(f, (models.ForeignKey)): + out += ': ' + ( '"None"' if o is None else '"' + o.__class__.__name__ + '"' ) + elif isinstance(f, (models.ManyToManyField)): + out += ': %d' % o.count() + + else: + out += ': "' + unicode(o) + '"' + + if hasattr(self,'polymorphic_annotate_names'): + out += ' - Ann: ' + for an in self.polymorphic_annotate_names: + if an != self.polymorphic_annotate_names[0]: + out += ', ' + out += an + if self.polymorphic_showfield_type: + out += ' (' + type(getattr(self, an)).__name__ + ')' + if self.polymorphic_showfield_content: + out += ': "' + unicode(getattr(self, an)) + '"' + + return out+'>' + +class ShowFieldType(ShowFieldBase): + """ model mixin that shows the object's class and it's field types """ + polymorphic_showfield_type = True + +class ShowFieldContent(ShowFieldBase): + """ model mixin that shows the object's class, it's fields and field contents """ + polymorphic_showfield_content = True + +class ShowFieldTypeAndContent(ShowFieldBase): + """ model mixin, like ShowFieldContent, but also show field types """ + polymorphic_showfield_type = True + polymorphic_showfield_content = True + + +# compatibility with old class names +ShowFieldTypes = ShowFieldType +ShowFields = ShowFieldContent +ShowFieldsAndTypes = ShowFieldTypeAndContent diff --git a/polymorphic/tests.py b/polymorphic/tests.py index 179cd67..5dba6dc 100644 --- a/polymorphic/tests.py +++ b/polymorphic/tests.py @@ -7,11 +7,12 @@ import settings from django.test import TestCase from django.db.models.query import QuerySet -from django.db.models import Q +from django.db.models import Q,Count from django.db import models from django.contrib.contenttypes.models import ContentType +from pprint import pprint -from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet, ShowFields, ShowFieldsAndTypes, get_version +from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet, ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent, get_version class PlainA(models.Model): field1 = models.CharField(max_length=10) @@ -20,14 +21,24 @@ class PlainB(PlainA): class PlainC(PlainB): field3 = models.CharField(max_length=10) -class Model2A(PolymorphicModel): +class Model2A(ShowFieldType, PolymorphicModel): field1 = models.CharField(max_length=10) class Model2B(Model2A): field2 = models.CharField(max_length=10) class Model2C(Model2B): field3 = models.CharField(max_length=10) -class Base(PolymorphicModel): +class ModelShow1(ShowFieldType,PolymorphicModel): + field1 = models.CharField(max_length=10) + m2m = models.ManyToManyField('self') +class ModelShow2(ShowFieldContent, PolymorphicModel): + field1 = models.CharField(max_length=10) + m2m = models.ManyToManyField('self') +class ModelShow3(ShowFieldTypeAndContent, PolymorphicModel): + field1 = models.CharField(max_length=10) + m2m = models.ManyToManyField('self') + +class Base(ShowFieldType, PolymorphicModel): field_b = models.CharField(max_length=10) class ModelX(Base): field_x = models.CharField(max_length=10) @@ -36,7 +47,7 @@ class ModelY(Base): class Enhance_Plain(models.Model): field_p = models.CharField(max_length=10) -class Enhance_Base(ShowFieldsAndTypes, PolymorphicModel): +class Enhance_Base(ShowFieldTypeAndContent, PolymorphicModel): field_b = models.CharField(max_length=10) class Enhance_Inherit(Enhance_Base, Enhance_Plain): field_i = models.CharField(max_length=10) @@ -51,7 +62,7 @@ class DiamondY(DiamondBase): class DiamondXY(DiamondX, DiamondY): pass -class RelationBase(ShowFieldsAndTypes, PolymorphicModel): +class RelationBase(ShowFieldTypeAndContent, PolymorphicModel): field_base = models.CharField(max_length=10) fk = models.ForeignKey('self', null=True) m2m = models.ManyToManyField('self') @@ -68,11 +79,11 @@ class RelatingModel(models.Model): class MyManager(PolymorphicManager): def get_query_set(self): return super(MyManager, self).get_query_set().order_by('-field1') -class ModelWithMyManager(ShowFieldsAndTypes, Model2A): +class ModelWithMyManager(ShowFieldTypeAndContent, Model2A): objects = MyManager() field4 = models.CharField(max_length=10) -class MROBase1(PolymorphicModel): +class MROBase1(ShowFieldType, PolymorphicModel): objects = MyManager() field1 = models.CharField(max_length=10) # needed as MyManager uses it class MROBase2(MROBase1): @@ -89,23 +100,23 @@ class MgrInheritA(models.Model): class MgrInheritB(MgrInheritA): mgrB = models.Manager() field2 = models.CharField(max_length=10) -class MgrInheritC(ShowFieldsAndTypes, MgrInheritB): +class MgrInheritC(ShowFieldTypeAndContent, MgrInheritB): pass -class BlogBase(ShowFieldsAndTypes, PolymorphicModel): +class BlogBase(ShowFieldTypeAndContent, PolymorphicModel): name = models.CharField(max_length=10) class BlogA(BlogBase): info = models.CharField(max_length=10) class BlogB(BlogBase): pass -class BlogA_Entry(ShowFieldsAndTypes, PolymorphicModel): +class BlogEntry(ShowFieldTypeAndContent, PolymorphicModel): blog = models.ForeignKey(BlogA) text = models.CharField(max_length=10) -class ModelFieldNameTest(PolymorphicModel): +class ModelFieldNameTest(ShowFieldType, PolymorphicModel): modelfieldnametest = models.CharField(max_length=10) -class InitTestModel(PolymorphicModel): +class InitTestModel(ShowFieldType, PolymorphicModel): bar = models.CharField(max_length=100) def __init__(self, *args, **kwargs): kwargs['bar'] = self.x() @@ -115,13 +126,13 @@ class InitTestModelSubclass(InitTestModel): return 'XYZ' # test bad field name -#class TestBadFieldModel(PolymorphicModel): +#class TestBadFieldModel(ShowFieldType, PolymorphicModel): # instance_of = models.CharField(max_length=10) # validation error: "polymorphic.relatednameclash: Accessor for field 'polymorphic_ctype' clashes # with related field 'ContentType.relatednameclash_set'." (reported by Andrew Ingram) # fixed with related_name -class RelatedNameClash(PolymorphicModel): +class RelatedNameClash(ShowFieldType, PolymorphicModel): ctype = models.ForeignKey(ContentType, null=True, editable=False) @@ -135,22 +146,29 @@ class testclass(TestCase): if o.field_b != 'b': print '# Django model inheritance diamond problem detected' def test_annotate_aggregate_order(self): - from django.db.models import Count - BlogA.objects.all().delete() + # create a blog of type BlogA blog = BlogA.objects.create(name='B1', info='i1') - entry1 = blog.bloga_entry_set.create(text='bla') - entry2 = BlogA_Entry.objects.create(blog=blog, text='bla2') + # create two blog entries in BlogA + entry1 = blog.blogentry_set.create(text='bla') + entry2 = BlogEntry.objects.create(blog=blog, text='bla2') - # create some BlogB to make the table more diverse + # create some blogs of type BlogB to make the BlogBase table data really polymorphic o = BlogB.objects.create(name='Bb1') o = BlogB.objects.create(name='Bb2') o = BlogB.objects.create(name='Bb3') - qs = BlogBase.objects.annotate(entrycount=Count('BlogA___bloga_entry')) - assert qs[0].entrycount == 2 + qs = BlogBase.objects.annotate(entrycount=Count('BlogA___blogentry')) - x = BlogBase.objects.aggregate(entrycount=Count('BlogA___bloga_entry')) + assert len(qs)==4 + + for o in qs: + if o.name=='B1': + assert o.entrycount == 2 + else: + assert o.entrycount == 0 + + x = BlogBase.objects.aggregate(entrycount=Count('BlogA___blogentry')) assert x['entrycount'] == 2 # create some more blogs for next test @@ -159,7 +177,8 @@ class testclass(TestCase): b2 = BlogA.objects.create(name='B4', info='i4') b2 = BlogA.objects.create(name='B5', info='i5') - # test ordering + ### test ordering for field in all entries + expected = ''' [ , , @@ -171,8 +190,11 @@ class testclass(TestCase): ]''' x = '\n' + repr(BlogBase.objects.order_by('-name')) assert x == expected - - expected=''' + + ### test ordering for field in one subclass only + + # MySQL and SQLite return this order + expected1=''' [ , , , @@ -181,8 +203,20 @@ class testclass(TestCase): , , ]''' + + # PostgreSQL returns this order + expected2=''' +[ , + , + , + , + , + , + , + ]''' + x = '\n' + repr(BlogBase.objects.order_by('-BlogA___info')) - assert x == expected + assert x == expected1 or x == expected2 #assert False @@ -196,6 +230,7 @@ __test__ = {"doctest": """ >>> get_version() '0.5 beta' + ### simple inheritance >>> o=Model2A.objects.create(field1='A1') @@ -212,6 +247,32 @@ __test__ = {"doctest": """ >>> o.get_real_instance() + +### ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent, also with annotate() + +>>> o=ModelShow1.objects.create(field1='abc') +>>> o.m2m.add(o) ; o.save() +>>> ModelShow1.objects.all() +[ ] + +>>> o=ModelShow2.objects.create(field1='abc') +>>> o.m2m.add(o) ; o.save() +>>> ModelShow2.objects.all() +[ ] + +>>> o=ModelShow3.objects.create(field1='abc') +>>> o.m2m.add(o) ; o.save() +>>> ModelShow3.objects.all() +[ ] + +>>> ModelShow1.objects.all().annotate(Count('m2m')) +[ ] +>>> ModelShow2.objects.all().annotate(Count('m2m')) +[ ] +>>> ModelShow3.objects.all().annotate(Count('m2m')) +[ ] + + ### extra() method >>> Model2A.objects.extra(where=['id IN (2, 3)']) @@ -222,6 +283,7 @@ __test__ = {"doctest": """ [ , ] + ### class filtering, instance_of, not_instance_of >>> Model2A.objects.instance_of(Model2B) @@ -246,6 +308,7 @@ __test__ = {"doctest": """ [ , ] + ### get & delete >>> oa=Model2A.objects.get(id=2) @@ -257,6 +320,7 @@ __test__ = {"doctest": """ [ , ] + ### queryset combining >>> o=ModelX.objects.create(field_x='x') @@ -266,6 +330,7 @@ __test__ = {"doctest": """ [ , ] + ### multiple inheritance, subclassing third party models (mix PolymorphicModel with models.Model) >>> o = Enhance_Base.objects.create(field_b='b-base') @@ -275,6 +340,7 @@ __test__ = {"doctest": """ [ , ] + ### ForeignKey, ManyToManyField >>> obase=RelationBase.objects.create(field_base='base') @@ -284,27 +350,27 @@ __test__ = {"doctest": """ >>> oa.m2m.add(oa); oa.m2m.add(ob) >>> RelationBase.objects.all() -[ , - , - , - ] +[ , + , + , + ] >>> oa=RelationBase.objects.get(id=2) >>> oa.fk - + >>> oa.relationbase_set.all() -[ , - ] +[ , + ] >>> ob=RelationBase.objects.get(id=3) >>> ob.fk - + >>> oa=RelationA.objects.get() >>> oa.m2m.all() -[ , - ] +[ , + ] ### user-defined manager @@ -320,6 +386,7 @@ __test__ = {"doctest": """ >>> type(ModelWithMyManager._default_manager) + ### Manager Inheritance >>> type(MRODerived.objects) # MRO @@ -333,10 +400,12 @@ __test__ = {"doctest": """ >>> type(MROBase2._default_manager) + ### fixed issue in PolymorphicModel.__getattribute__: field name same as model name >>> ModelFieldNameTest.objects.create(modelfieldnametest='1') + ### fixed issue in PolymorphicModel.__getattribute__: # if subclass defined __init__ and accessed class members, __getattribute__ had a problem: "...has no attribute 'sub_and_superclass_dict'" #>>> o @@ -344,6 +413,7 @@ __test__ = {"doctest": """ >>> o.bar 'XYZ' + ### Django model inheritance diamond problem, fails for Django 1.1 #>>> o=DiamondXY.objects.create(field_b='b', field_x='x', field_y='y')