AC-1040 Add django-polymorphic as prerequisite for moving toward unified jobs.

This commit is contained in:
Chris Church 2014-02-27 17:12:32 -05:00
parent 61ab197b3a
commit e957de2016
22 changed files with 2806 additions and 0 deletions

View File

@ -18,6 +18,7 @@ django-auth-ldap==1.1.7 (django_auth_ldap/*)
django-celery==3.1.1 (djcelery/*)
django-extensions==1.2.5 (django_extensions/*)
django-jsonfield==0.9.12 (jsonfield/*, minor fix in jsonfield/fields.py)
django-polymorphic==0.5.3 (polymorphic/*)
django-split-settings==0.1.1 (split_settings/*)
django-taggit==0.11.2 (taggit/*)
djangorestframework==2.3.10 (rest_framework/*)

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""
Seamless Polymorphic Inheritance for Django Models
Copyright:
This code and affiliated files are (C) by Bert Constantin and individual contributors.
Please see LICENSE and AUTHORS for more information.
"""
from __future__ import absolute_import
import django
from .polymorphic_model import PolymorphicModel
from .manager import PolymorphicManager
from .query import PolymorphicQuerySet
from .query_translate import translate_polymorphic_Q_object
from .showfields import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
from .showfields import ShowFields, ShowFieldTypes, ShowFieldsAndTypes # import old names for compatibility
# Monkey-patch Django < 1.5 to allow ContentTypes for proxy models.
if django.VERSION[:2] < (1, 5):
from django.contrib.contenttypes.models import ContentTypeManager
from django.utils.encoding import smart_text
def get_for_model(self, model, for_concrete_model=True):
if for_concrete_model:
model = model._meta.concrete_model
elif model._deferred:
model = model._meta.proxy_for_model
opts = model._meta
try:
ct = self._get_from_cache(opts)
except KeyError:
ct, created = self.get_or_create(
app_label = opts.app_label,
model = opts.object_name.lower(),
defaults = {'name': smart_text(opts.verbose_name_raw)},
)
self._add_to_cache(self.db, ct)
return ct
ContentTypeManager.get_for_model__original = ContentTypeManager.get_for_model
ContentTypeManager.get_for_model = get_for_model

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""
See PEP 386 (https://www.python.org/dev/peps/pep-0386/)
Release logic:
1. Remove "dev#" from current (this file, now AND setup.py!).
2. git commit
3. git tag <version>
4. push to pypi + push --tags to github
5. bump the version, append ".dev0"
6. git commit
7. push to github
"""
__version__ = "0.5.3"

View File

@ -0,0 +1,507 @@
"""
ModelAdmin code to display polymorphic models.
"""
from django import forms
from django.conf.urls import patterns, url
from django.contrib import admin
from django.contrib.admin.helpers import AdminForm, AdminErrorList
from django.contrib.admin.sites import AdminSite
from django.contrib.admin.widgets import AdminRadioSelect
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import RegexURLResolver
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.utils import six
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
__all__ = (
'PolymorphicModelChoiceForm', 'PolymorphicParentModelAdmin',
'PolymorphicChildModelAdmin', 'PolymorphicChildModelFilter'
)
class RegistrationClosed(RuntimeError):
"The admin model can't be registered anymore at this point."
pass
class ChildAdminNotRegistered(RuntimeError):
"The admin site for the model is not registered."
pass
class PolymorphicModelChoiceForm(forms.Form):
"""
The default form for the ``add_type_form``. Can be overwritten and replaced.
"""
#: Define the label for the radiofield
type_label = _("Type")
ct_id = forms.ChoiceField(label=type_label, widget=AdminRadioSelect(attrs={'class': 'radiolist'}))
def __init__(self, *args, **kwargs):
# Allow to easily redefine the label (a commonly expected usecase)
super(PolymorphicModelChoiceForm, self).__init__(*args, **kwargs)
self.fields['ct_id'].label = self.type_label
class PolymorphicChildModelFilter(admin.SimpleListFilter):
"""
An admin list filter for the PolymorphicParentModelAdmin which enables
filtering by its child models.
"""
title = _('Content type')
parameter_name = 'polymorphic_ctype'
def lookups(self, request, model_admin):
return model_admin.get_child_type_choices()
def queryset(self, request, queryset):
try:
value = int(self.value())
except TypeError:
value = None
if value:
# ensure the content type is allowed
for choice_value, _ in self.lookup_choices:
if choice_value == value:
return queryset.filter(polymorphic_ctype_id=choice_value)
raise PermissionDenied(
'Invalid ContentType "{0}". It must be registered as child model.'.format(value))
return queryset
class PolymorphicParentModelAdmin(admin.ModelAdmin):
"""
A admin interface that can displays different change/delete pages, depending on the polymorphic model.
To use this class, two variables need to be defined:
* :attr:`base_model` should
* :attr:`child_models` should be a list of (Model, Admin) tuples
Alternatively, the following methods can be implemented:
* :func:`get_child_models` should return a list of (Model, ModelAdmin) tuples
* optionally, :func:`get_child_type_choices` can be overwritten to refine the choices for the add dialog.
This class needs to be inherited by the model admin base class that is registered in the site.
The derived models should *not* register the ModelAdmin, but instead it should be returned by :func:`get_child_models`.
"""
#: The base model that the class uses
base_model = None
#: The child models that should be displayed
child_models = None
#: Whether the list should be polymorphic too, leave to ``False`` to optimize
polymorphic_list = False
add_type_template = None
add_type_form = PolymorphicModelChoiceForm
def __init__(self, model, admin_site, *args, **kwargs):
super(PolymorphicParentModelAdmin, self).__init__(model, admin_site, *args, **kwargs)
self._child_admin_site = AdminSite(name=self.admin_site.name)
self._is_setup = False
def _lazy_setup(self):
if self._is_setup:
return
# By not having this in __init__() there is less stress on import dependencies as well,
# considering an advanced use cases where a plugin system scans for the child models.
child_models = self.get_child_models()
for Model, Admin in child_models:
self.register_child(Model, Admin)
self._child_models = dict(child_models)
# This is needed to deal with the improved ForeignKeyRawIdWidget in Django 1.4 and perhaps other widgets too.
# The ForeignKeyRawIdWidget checks whether the referenced model is registered in the admin, otherwise it displays itself as a textfield.
# As simple solution, just make sure all parent admin models are also know in the child admin site.
# This should be done after all parent models are registered off course.
complete_registry = self.admin_site._registry.copy()
complete_registry.update(self._child_admin_site._registry)
self._child_admin_site._registry = complete_registry
self._is_setup = True
def register_child(self, model, model_admin):
"""
Register a model with admin to display.
"""
# After the get_urls() is called, the URLs of the child model can't be exposed anymore to the Django URLconf,
# which also means that a "Save and continue editing" button won't work.
if self._is_setup:
raise RegistrationClosed("The admin model can't be registered anymore at this point.")
if not issubclass(model, self.base_model):
raise TypeError("{0} should be a subclass of {1}".format(model.__name__, self.base_model.__name__))
if not issubclass(model_admin, admin.ModelAdmin):
raise TypeError("{0} should be a subclass of {1}".format(model_admin.__name__, admin.ModelAdmin.__name__))
self._child_admin_site.register(model, model_admin)
def get_child_models(self):
"""
Return the derived model classes which this admin should handle.
This should return a list of tuples, exactly like :attr:`child_models` is.
The model classes can be retrieved as ``base_model.__subclasses__()``,
a setting in a config file, or a query of a plugin registration system at your option
"""
if self.child_models is None:
raise NotImplementedError("Implement get_child_models() or child_models")
return self.child_models
def get_child_type_choices(self):
"""
Return a list of polymorphic types which can be added.
"""
choices = []
for model, _ in self.get_child_models():
ct = ContentType.objects.get_for_model(model, for_concrete_model=False)
choices.append((ct.id, model._meta.verbose_name))
return choices
def _get_real_admin(self, object_id):
obj = self.model.objects.non_polymorphic().values('polymorphic_ctype').get(pk=object_id)
return self._get_real_admin_by_ct(obj['polymorphic_ctype'])
def _get_real_admin_by_ct(self, ct_id):
try:
ct = ContentType.objects.get_for_id(ct_id)
except ContentType.DoesNotExist as e:
raise Http404(e) # Handle invalid GET parameters
model_class = ct.model_class()
if not model_class:
raise Http404("No model found for '{0}.{1}'.".format(*ct.natural_key())) # Handle model deletion
return self._get_real_admin_by_model(model_class)
def _get_real_admin_by_model(self, model_class):
# In case of a ?ct_id=### parameter, the view is already checked for permissions.
# Hence, make sure this is a derived object, or risk exposing other admin interfaces.
if model_class not in self._child_models:
raise PermissionDenied("Invalid model '{0}', it must be registered as child model.".format(model_class))
try:
# HACK: the only way to get the instance of an model admin,
# is to read the registry of the AdminSite.
return self._child_admin_site._registry[model_class]
except KeyError:
raise ChildAdminNotRegistered("No child admin site was registered for a '{0}' model.".format(model_class))
def queryset(self, request):
# optimize the list display.
qs = super(PolymorphicParentModelAdmin, self).queryset(request)
if not self.polymorphic_list:
qs = qs.non_polymorphic()
return qs
def add_view(self, request, form_url='', extra_context=None):
"""Redirect the add view to the real admin."""
ct_id = int(request.GET.get('ct_id', 0))
if not ct_id:
# Display choices
return self.add_type_view(request)
else:
real_admin = self._get_real_admin_by_ct(ct_id)
return real_admin.add_view(request, form_url, extra_context)
def change_view(self, request, object_id, *args, **kwargs):
"""Redirect the change view to the real admin."""
# between Django 1.3 and 1.4 this method signature differs. Hence the *args, **kwargs
real_admin = self._get_real_admin(object_id)
return real_admin.change_view(request, object_id, *args, **kwargs)
def delete_view(self, request, object_id, extra_context=None):
"""Redirect the delete view to the real admin."""
real_admin = self._get_real_admin(object_id)
return real_admin.delete_view(request, object_id, extra_context)
def get_urls(self):
"""
Expose the custom URLs for the subclasses and the URL resolver.
"""
urls = super(PolymorphicParentModelAdmin, self).get_urls()
info = self.model._meta.app_label, self.model._meta.module_name
# Patch the change URL so it's not a big catch-all; allowing all custom URLs to be added to the end.
# The url needs to be recreated, patching url.regex is not an option Django 1.4's LocaleRegexProvider changed it.
new_change_url = url(r'^(\d+)/$', self.admin_site.admin_view(self.change_view), name='{0}_{1}_change'.format(*info))
for i, oldurl in enumerate(urls):
if oldurl.name == new_change_url.name:
urls[i] = new_change_url
# Define the catch-all for custom views
custom_urls = patterns('',
url(r'^(?P<path>.+)$', self.admin_site.admin_view(self.subclass_view))
)
# At this point. all admin code needs to be known.
self._lazy_setup()
# Add reverse names for all polymorphic models, so the delete button and "save and add" just work.
# These definitions are masked by the definition above, since it needs special handling (and a ct_id parameter).
dummy_urls = []
for model, _ in self.get_child_models():
admin = self._get_real_admin_by_model(model)
dummy_urls += admin.get_urls()
return urls + custom_urls + dummy_urls
def subclass_view(self, request, path):
"""
Forward any request to a custom view of the real admin.
"""
ct_id = int(request.GET.get('ct_id', 0))
if not ct_id:
# See if the path started with an ID.
try:
pos = path.find('/')
object_id = long(path[0:pos])
except ValueError:
raise Http404("No ct_id parameter, unable to find admin subclass for path '{0}'.".format(path))
ct_id = self.model.objects.values_list('polymorphic_ctype_id', flat=True).get(pk=object_id)
real_admin = self._get_real_admin_by_ct(ct_id)
resolver = RegexURLResolver('^', real_admin.urls)
resolvermatch = resolver.resolve(path) # May raise Resolver404
if not resolvermatch:
raise Http404("No match for path '{0}' in admin subclass.".format(path))
return resolvermatch.func(request, *resolvermatch.args, **resolvermatch.kwargs)
def add_type_view(self, request, form_url=''):
"""
Display a choice form to select which page type to add.
"""
if not self.has_add_permission(request):
raise PermissionDenied
extra_qs = ''
if request.META['QUERY_STRING']:
extra_qs = '&' + request.META['QUERY_STRING']
choices = self.get_child_type_choices()
if len(choices) == 1:
return HttpResponseRedirect('?ct_id={0}{1}'.format(choices[0][0], extra_qs))
# Create form
form = self.add_type_form(
data=request.POST if request.method == 'POST' else None,
initial={'ct_id': choices[0][0]}
)
form.fields['ct_id'].choices = choices
if form.is_valid():
return HttpResponseRedirect('?ct_id={0}{1}'.format(form.cleaned_data['ct_id'], extra_qs))
# Wrap in all admin layout
fieldsets = ((None, {'fields': ('ct_id',)}),)
adminForm = AdminForm(form, fieldsets, {}, model_admin=self)
media = self.media + adminForm.media
opts = self.model._meta
context = {
'title': _('Add %s') % force_text(opts.verbose_name),
'adminform': adminForm,
'is_popup': "_popup" in request.REQUEST,
'media': mark_safe(media),
'errors': AdminErrorList(form, ()),
'app_label': opts.app_label,
}
return self.render_add_type_form(request, context, form_url)
def render_add_type_form(self, request, context, form_url=''):
"""
Render the page type choice form.
"""
opts = self.model._meta
app_label = opts.app_label
context.update({
'has_change_permission': self.has_change_permission(request),
'form_url': mark_safe(form_url),
'opts': opts,
'add': True,
'save_on_top': self.save_on_top,
})
if hasattr(self.admin_site, 'root_path'):
context['root_path'] = self.admin_site.root_path # Django < 1.4
context_instance = RequestContext(request, current_app=self.admin_site.name)
return render_to_response(self.add_type_template or [
"admin/%s/%s/add_type_form.html" % (app_label, opts.object_name.lower()),
"admin/%s/add_type_form.html" % app_label,
"admin/polymorphic/add_type_form.html", # added default here
"admin/add_type_form.html"
], context, context_instance=context_instance)
@property
def change_list_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/change_list.html" % (app_label, opts.object_name.lower()),
"admin/%s/change_list.html" % app_label,
# Added base class:
"admin/%s/%s/change_list.html" % (base_app_label, base_opts.object_name.lower()),
"admin/%s/change_list.html" % base_app_label,
"admin/change_list.html"
]
class PolymorphicChildModelAdmin(admin.ModelAdmin):
"""
The *optional* base class for the admin interface of derived models.
This base class defines some convenience behavior for the admin interface:
* It corrects the breadcrumbs in the admin pages.
* It adds the base model to the template lookup paths.
* It allows to set ``base_form`` so the derived class will automatically include other fields in the form.
* It allows to set ``base_fieldsets`` so the derived class will automatically display any extra fields.
The ``base_model`` attribute must be set.
"""
base_model = None
base_form = None
base_fieldsets = None
extra_fieldset_title = _("Contents") # Default title for extra fieldset
def get_form(self, request, obj=None, **kwargs):
# The django admin validation requires the form to have a 'class Meta: model = ..'
# attribute, or it will complain that the fields are missing.
# However, this enforces all derived ModelAdmin classes to redefine the model as well,
# because they need to explicitly set the model again - it will stick with the base model.
#
# Instead, pass the form unchecked here, because the standard ModelForm will just work.
# If the derived class sets the model explicitly, respect that setting.
kwargs.setdefault('form', self.base_form or self.form)
return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
@property
def change_form_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
"admin/%s/change_form.html" % app_label,
# Added:
"admin/%s/%s/change_form.html" % (base_app_label, base_opts.object_name.lower()),
"admin/%s/change_form.html" % base_app_label,
"admin/polymorphic/change_form.html",
"admin/change_form.html"
]
@property
def delete_confirmation_template(self):
opts = self.model._meta
app_label = opts.app_label
# Pass the base options
base_opts = self.base_model._meta
base_app_label = base_opts.app_label
return [
"admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
"admin/%s/delete_confirmation.html" % app_label,
# Added:
"admin/%s/%s/delete_confirmation.html" % (base_app_label, base_opts.object_name.lower()),
"admin/%s/delete_confirmation.html" % base_app_label,
"admin/polymorphic/delete_confirmation.html",
"admin/delete_confirmation.html"
]
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
context.update({
'base_opts': self.base_model._meta,
})
return super(PolymorphicChildModelAdmin, self).render_change_form(request, context, add=add, change=change, form_url=form_url, obj=obj)
def delete_view(self, request, object_id, context=None):
extra_context = {
'base_opts': self.base_model._meta,
}
return super(PolymorphicChildModelAdmin, self).delete_view(request, object_id, extra_context)
# ---- Extra: improving the form/fieldset default display ----
def get_fieldsets(self, request, obj=None):
# If subclass declares fieldsets, this is respected
if self.declared_fieldsets or not self.base_fieldsets:
return super(PolymorphicChildModelAdmin, self).get_fieldsets(request, obj)
# Have a reasonable default fieldsets,
# where the subclass fields are automatically included.
other_fields = self.get_subclass_fields(request, obj)
if other_fields:
return (
self.base_fieldsets[0],
(self.extra_fieldset_title, {'fields': other_fields}),
) + self.base_fieldsets[1:]
else:
return self.base_fieldsets
def get_subclass_fields(self, request, obj=None):
# Find out how many fields would really be on the form,
# if it weren't restricted by declared fields.
exclude = list(self.exclude or [])
exclude.extend(self.get_readonly_fields(request, obj))
# By not declaring the fields/form in the base class,
# get_form() will populate the form with all available fields.
form = self.get_form(request, obj, exclude=exclude)
subclass_fields = list(six.iterkeys(form.base_fields)) + list(self.get_readonly_fields(request, obj))
# Find which fields are not part of the common fields.
for fieldset in self.base_fieldsets:
for field in fieldset[1]['fields']:
try:
subclass_fields.remove(field)
except ValueError:
pass # field not found in form, Django will raise exception later.
return subclass_fields

View File

@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
""" PolymorphicModel Meta Class
Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/
"""
from __future__ import absolute_import
import sys
import inspect
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.manager import ManagerDescriptor
from .manager import PolymorphicManager
from .query import PolymorphicQuerySet
# PolymorphicQuerySet Q objects (and filter()) support these additional key words.
# These are forbidden as field names (a descriptive exception is raised)
POLYMORPHIC_SPECIAL_Q_KWORDS = ['instance_of', 'not_instance_of']
try:
from django.db.models.manager import AbstractManagerDescriptor # Django 1.5
except ImportError:
AbstractManagerDescriptor = None
###################################################################################
### PolymorphicModel meta class
class PolymorphicModelBase(ModelBase):
"""
Manager inheritance is a pretty complex topic which may need
more thought regarding how this should be handled for polymorphic
models.
In any case, we probably should propagate 'objects' and 'base_objects'
from PolymorphicModel to every subclass. We also want to somehow
inherit/propagate _default_manager as well, as it needs to be polymorphic.
The current implementation below is an experiment to solve this
problem with a very simplistic approach: We unconditionally
inherit/propagate any and all managers (using _copy_to_model),
as long as they are defined on polymorphic models
(the others are left alone).
Like Django ModelBase, we special-case _default_manager:
if there are any user-defined managers, it is set to the first of these.
We also require that _default_manager as well as any user defined
polymorphic managers produce querysets that are derived from
PolymorphicQuerySet.
"""
def __new__(self, model_name, bases, attrs):
#print; print '###', model_name, '- bases:', bases
# Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
# Let Django fully ignore the class which is inserted in between.
if not attrs and model_name == 'NewBase':
attrs['__module__'] = 'django.utils.six'
attrs['Meta'] = type('Meta', (), {'abstract': True})
return super(PolymorphicModelBase, self).__new__(self, model_name, bases, attrs)
# create new model
new_class = self.call_superclass_new_method(model_name, bases, attrs)
# check if the model fields are all allowed
self.validate_model_fields(new_class)
# create list of all managers to be inherited from the base classes
inherited_managers = new_class.get_inherited_managers(attrs)
# add the managers to the new model
for source_name, mgr_name, manager in inherited_managers:
#print '** add inherited manager from model %s, manager %s, %s' % (source_name, mgr_name, manager.__class__.__name__)
new_manager = manager._copy_to_model(new_class)
new_class.add_to_class(mgr_name, new_manager)
# get first user defined manager; if there is one, make it the _default_manager
# this value is used by the related objects, restoring access to custom queryset methods on related objects.
user_manager = self.get_first_user_defined_manager(new_class)
if user_manager:
def_mgr = user_manager._copy_to_model(new_class)
#print '## add default manager', type(def_mgr)
new_class.add_to_class('_default_manager', def_mgr)
new_class._default_manager._inherited = False # the default mgr was defined by the user, not inherited
# validate resulting default manager
self.validate_model_manager(new_class._default_manager, model_name, '_default_manager')
# for __init__ function of this class (monkeypatching inheritance accessors)
new_class.polymorphic_super_sub_accessors_replaced = False
# determine the name of the primary key field and store it into the class variable
# polymorphic_primary_key_name (it is needed by query.py)
for f in new_class._meta.fields:
if f.primary_key and type(f) != models.OneToOneField:
new_class.polymorphic_primary_key_name = f.name
break
return new_class
def get_inherited_managers(self, attrs):
"""
Return list of all managers to be inherited/propagated from the base classes;
use correct mro, only use managers with _inherited==False (they are of no use),
skip managers that are overwritten by the user with same-named class attributes (in attrs)
"""
#print "** ", self.__name__
add_managers = []
add_managers_keys = set()
for base in self.__mro__[1:]:
if not issubclass(base, models.Model):
continue
if not getattr(base, 'polymorphic_model_marker', None):
continue # leave managers of non-polym. models alone
for key, manager in base.__dict__.items():
if type(manager) == models.manager.ManagerDescriptor:
manager = manager.manager
if AbstractManagerDescriptor is not None:
# Django 1.4 unconditionally assigned managers to a model. As of Django 1.5 however,
# the abstract models don't get any managers, only a AbstractManagerDescriptor as substitute.
# Pretend that the manager is still there, so all code works like it used to.
if type(manager) == AbstractManagerDescriptor and base.__name__ == 'PolymorphicModel':
model = manager.model
if key == 'objects':
manager = PolymorphicManager()
manager.model = model
elif key == 'base_objects':
manager = models.Manager()
manager.model = model
if not isinstance(manager, models.Manager):
continue
if key == '_base_manager':
continue # let Django handle _base_manager
if key in attrs:
continue
if key in add_managers_keys:
continue # manager with that name already added, skip
if manager._inherited:
continue # inherited managers (on the bases) have no significance, they are just copies
#print '## {0} {1}'.format(self.__name__, key)
if isinstance(manager, PolymorphicManager): # validate any inherited polymorphic managers
self.validate_model_manager(manager, self.__name__, key)
add_managers.append((base.__name__, key, manager))
add_managers_keys.add(key)
# The ordering in the base.__dict__ may randomly change depending on which method is added.
# Make sure base_objects is on top, and 'objects' and '_default_manager' follow afterwards.
# This makes sure that the _base_manager is also assigned properly.
add_managers = sorted(add_managers, key=lambda item: (item[1].startswith('_'), item[1]))
return add_managers
@classmethod
def get_first_user_defined_manager(mcs, new_class):
# See if there is a manager attribute directly stored at this inheritance level.
mgr_list = []
for key, val in new_class.__dict__.items():
if isinstance(val, ManagerDescriptor):
val = val.manager
if not isinstance(val, PolymorphicManager) or type(val) is PolymorphicManager:
continue
mgr_list.append((val.creation_counter, key, val))
# if there are user defined managers, use first one as _default_manager
if mgr_list:
_, manager_name, manager = sorted(mgr_list)[0]
#sys.stderr.write( '\n# first user defined manager for model "{model}":\n# "{mgrname}": {mgr}\n# manager model: {mgrmodel}\n\n'
# .format( model=self.__name__, mgrname=manager_name, mgr=manager, mgrmodel=manager.model ) )
return manager
return None
@classmethod
def call_superclass_new_method(self, model_name, bases, attrs):
"""call __new__ method of super class and return the newly created class.
Also work around a limitation in Django's ModelBase."""
# There seems to be a general limitation in Django's app_label handling
# regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
# We run into this problem if polymorphic.py is located in a top-level directory
# which is directly in the python path. To work around this we temporarily set
# app_label here for PolymorphicModel.
meta = attrs.get('Meta', None)
do_app_label_workaround = (meta
and attrs['__module__'] == 'polymorphic'
and model_name == 'PolymorphicModel'
and getattr(meta, 'app_label', None) is None)
if do_app_label_workaround:
meta.app_label = 'poly_dummy_app_label'
new_class = super(PolymorphicModelBase, self).__new__(self, model_name, bases, attrs)
if do_app_label_workaround:
del(meta.app_label)
return new_class
def validate_model_fields(self):
"check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
for f in self._meta.fields:
if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS:
e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
raise AssertionError(e % (self.__name__, f.name))
@classmethod
def validate_model_manager(self, manager, model_name, manager_name):
"""check if the manager is derived from PolymorphicManager
and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
if not issubclass(type(manager), PolymorphicManager):
e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" manager is of type "' + type(manager).__name__
e += '", but must be a subclass of PolymorphicManager'
raise AssertionError(e)
if not getattr(manager, 'queryset_class', None) or not issubclass(manager.queryset_class, PolymorphicQuerySet):
e = 'PolymorphicModel: "' + model_name + '.' + manager_name + '" (PolymorphicManager) has been instantiated with a queryset class which is'
e += ' not a subclass of PolymorphicQuerySet (which is required)'
raise AssertionError(e)
return manager
# hack: a small patch to Django would be a better solution.
# Django's management command 'dumpdata' relies on non-polymorphic
# behaviour of the _default_manager. Therefore, we catch any access to _default_manager
# here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
# (non-polymorphic default manager is 'base_objects' for polymorphic models).
# This way we don't need to patch django.core.management.commands.dumpdata
# for all supported Django versions.
# TODO: investigate Django how this can be avoided
_dumpdata_command_running = False
if len(sys.argv) > 1:
_dumpdata_command_running = (sys.argv[1] == 'dumpdata')
def __getattribute__(self, name):
if name == '_default_manager':
if self._dumpdata_command_running:
frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name
if 'django/core/management/commands/dumpdata.py' in frm[1]:
return self.base_objects
#caller_mod_name = inspect.getmodule(frm[0]).__name__ # does not work with python 2.4
#if caller_mod_name == 'django.core.management.commands.dumpdata':
return super(PolymorphicModelBase, self).__getattribute__(name)

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
""" PolymorphicManager
Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/
"""
from __future__ import unicode_literals
import warnings
from django.db import models
from polymorphic.query import PolymorphicQuerySet
class PolymorphicManager(models.Manager):
"""
Manager for PolymorphicModel
Usually not explicitly needed, except if a custom manager or
a custom queryset class is to be used.
"""
# Tell Django that related fields also need to use this manager:
use_for_related_fields = True
queryset_class = PolymorphicQuerySet
def __init__(self, queryset_class=None, *args, **kwrags):
# Up till polymorphic 0.4, the queryset class could be specified as parameter to __init__.
# However, this doesn't work for related managers which instantiate a new version of this class.
# Hence, for custom managers the new default is using the 'queryset_class' attribute at class level instead.
if queryset_class:
warnings.warn("Using PolymorphicManager(queryset_class=..) is deprecated; override the queryset_class attribute instead", DeprecationWarning)
# For backwards compatibility, still allow the parameter:
self.queryset_class = queryset_class
super(PolymorphicManager, self).__init__(*args, **kwrags)
def get_query_set(self):
return self.queryset_class(self.model, using=self._db)
# Proxy all unknown method calls to the queryset, so that its members are
# directly accessible as PolymorphicModel.objects.*
# The advantage of this method is that not yet known member functions of derived querysets will be proxied as well.
# We exclude any special functions (__) from this automatic proxying.
def __getattr__(self, name):
if name.startswith('__'):
return super(PolymorphicManager, self).__getattr__(self, name)
return getattr(self.get_query_set(), name)
def __unicode__(self):
return '%s (PolymorphicManager) using %s' % (self.__class__.__name__, self.queryset_class.__name__)

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
IMPORTANT:
The models.py module is not used anymore.
Please use the following import method in your apps:
from polymorphic import PolymorphicModel, ...
"""

View File

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
"""
Seamless Polymorphic Inheritance for Django Models
==================================================
Please see README.rst and DOCS.rst for further information.
Or on the Web:
http://chrisglass.github.com/django_polymorphic/
http://github.com/chrisglass/django_polymorphic
Copyright:
This code and affiliated files are (C) by Bert Constantin and individual contributors.
Please see LICENSE and AUTHORS for more information.
"""
from __future__ import absolute_import
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils import six
from .base import PolymorphicModelBase
from .manager import PolymorphicManager
from .query_translate import translate_polymorphic_Q_object
###################################################################################
### PolymorphicModel
class PolymorphicModel(six.with_metaclass(PolymorphicModelBase, models.Model)):
"""
Abstract base class that provides polymorphic behaviour
for any model directly or indirectly derived from it.
For usage instructions & examples please see documentation.
PolymorphicModel declares one field for internal use (polymorphic_ctype)
and provides a polymorphic manager as the default manager
(and as 'objects').
PolymorphicModel overrides the save() and __init__ methods.
If your derived class overrides any of these methods as well, then you need
to take care that you correctly call the method of the superclass, like:
super(YourClass,self).save(*args,**kwargs)
"""
# for PolymorphicModelBase, so it can tell which models are polymorphic and which are not (duck typing)
polymorphic_model_marker = True
# for PolymorphicQuery, True => an overloaded __repr__ with nicer multi-line output is used by PolymorphicQuery
polymorphic_query_multiline_output = False
class Meta:
abstract = True
# avoid ContentType related field accessor clash (an error emitted by model validation)
polymorphic_ctype = models.ForeignKey(ContentType, null=True, editable=False,
related_name='polymorphic_%(app_label)s.%(class)s_set')
# some applications want to know the name of the fields that are added to its models
polymorphic_internal_model_fields = ['polymorphic_ctype']
# Note that Django 1.5 removes these managers because the model is abstract.
# They are pretended to be there by the metaclass in PolymorphicModelBase.get_inherited_managers()
objects = PolymorphicManager()
base_objects = models.Manager()
@classmethod
def translate_polymorphic_Q_object(self_class, q):
return translate_polymorphic_Q_object(self_class, q)
def pre_save_polymorphic(self):
"""Normally not needed.
This function may be called manually in special use-cases. When the object
is saved for the first time, we store its real class in polymorphic_ctype.
When the object later is retrieved by PolymorphicQuerySet, it uses this
field to figure out the real class of this object
(used by PolymorphicQuerySet._get_real_instances)
"""
if not self.polymorphic_ctype_id:
self.polymorphic_ctype = ContentType.objects.get_for_model(self, for_concrete_model=False)
def save(self, *args, **kwargs):
"""Overridden model save function which supports the polymorphism
functionality (through pre_save_polymorphic)."""
self.pre_save_polymorphic()
return super(PolymorphicModel, self).save(*args, **kwargs)
def get_real_instance_class(self):
"""
Normally not needed.
If a non-polymorphic manager (like base_objects) has been used to
retrieve objects, then the real class/type of these objects may be
determined using this method.
"""
# the following line would be the easiest way to do this, but it produces sql queries
# return self.polymorphic_ctype.model_class()
# so we use the following version, which uses the CopntentType manager cache.
# Note that model_class() can return None for stale content types;
# when the content type record still exists but no longer refers to an existing model.
try:
return ContentType.objects.get_for_id(self.polymorphic_ctype_id).model_class()
except AttributeError:
# Django <1.6 workaround
return None
def get_real_concrete_instance_class_id(self):
model_class = self.get_real_instance_class()
if model_class is None:
return None
return ContentType.objects.get_for_model(model_class, for_concrete_model=True).pk
def get_real_concrete_instance_class(self):
model_class = self.get_real_instance_class()
if model_class is None:
return None
return ContentType.objects.get_for_model(model_class, for_concrete_model=True).model_class()
def get_real_instance(self):
"""Normally not needed.
If a non-polymorphic manager (like base_objects) has been used to
retrieve objects, then the complete object with it's real class/type
and all fields may be retrieved with this method.
Each method call executes one db query (if necessary)."""
real_model = self.get_real_instance_class()
if real_model == self.__class__:
return self
return real_model.objects.get(pk=self.pk)
def __init__(self, * args, ** kwargs):
"""Replace Django's inheritance accessor member functions for our model
(self.__class__) with our own versions.
We monkey patch them until a patch can be added to Django
(which would probably be very small and make all of this obsolete).
If we have inheritance of the form ModelA -> ModelB ->ModelC then
Django creates accessors like this:
- ModelA: modelb
- ModelB: modela_ptr, modelb, modelc
- ModelC: modela_ptr, modelb, modelb_ptr, modelc
These accessors allow Django (and everyone else) to travel up and down
the inheritance tree for the db object at hand.
The original Django accessors use our polymorphic manager.
But they should not. So we replace them with our own accessors that use
our appropriate base_objects manager.
"""
super(PolymorphicModel, self).__init__(*args, ** kwargs)
if self.__class__.polymorphic_super_sub_accessors_replaced:
return
self.__class__.polymorphic_super_sub_accessors_replaced = True
def create_accessor_function_for_model(model, accessor_name):
def accessor_function(self):
attr = model.base_objects.get(pk=self.pk)
return attr
return accessor_function
subclasses_and_superclasses_accessors = self._get_inheritance_relation_fields_and_models()
from django.db.models.fields.related import SingleRelatedObjectDescriptor, ReverseSingleRelatedObjectDescriptor
for name, model in subclasses_and_superclasses_accessors.items():
orig_accessor = getattr(self.__class__, name, None)
if type(orig_accessor) in [SingleRelatedObjectDescriptor, ReverseSingleRelatedObjectDescriptor]:
#print >>sys.stderr, '---------- replacing', name, orig_accessor, '->', model
setattr(self.__class__, name, property(create_accessor_function_for_model(model, name)))
def _get_inheritance_relation_fields_and_models(self):
"""helper function for __init__:
determine names of all Django inheritance accessor member functions for type(self)"""
def add_model(model, as_ptr, result):
name = model.__name__.lower()
if as_ptr:
name += '_ptr'
result[name] = model
def add_model_if_regular(model, as_ptr, result):
if (issubclass(model, models.Model)
and model != models.Model
and model != self.__class__
and model != PolymorphicModel):
add_model(model, as_ptr, result)
def add_all_super_models(model, result):
add_model_if_regular(model, True, result)
for b in model.__bases__:
add_all_super_models(b, result)
def add_all_sub_models(model, result):
for b in model.__subclasses__():
add_model_if_regular(b, False, result)
result = {}
add_all_super_models(self.__class__, result)
add_all_sub_models(self.__class__, result)
return result

View File

@ -0,0 +1,307 @@
# -*- coding: utf-8 -*-
""" QuerySet for PolymorphicModel
Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/
"""
from __future__ import absolute_import
from collections import defaultdict
from django.db.models.query import QuerySet
from django.contrib.contenttypes.models import ContentType
from django.utils import six
from .query_translate import translate_polymorphic_filter_definitions_in_kwargs, translate_polymorphic_filter_definitions_in_args
from .query_translate import translate_polymorphic_field_path
# chunk-size: maximum number of objects requested per db-request
# by the polymorphic queryset.iterator() implementation; we use the same chunk size as Django
try:
from django.db.models.query import CHUNK_SIZE # this is 100 for Django 1.1/1.2
except ImportError:
# CHUNK_SIZE was removed in Django 1.6
CHUNK_SIZE = 100
Polymorphic_QuerySet_objects_per_request = CHUNK_SIZE
def transmogrify(cls, obj):
"""
Upcast a class to a different type without asking questions.
"""
if not '__init__' in obj.__dict__:
# Just assign __class__ to a different value.
new = obj
new.__class__ = cls
else:
# Run constructor, reassign values
new = cls()
for k,v in obj.__dict__.items():
new.__dict__[k] = v
return new
###################################################################################
### PolymorphicQuerySet
class PolymorphicQuerySet(QuerySet):
"""
QuerySet for PolymorphicModel
Contains the core functionality for PolymorphicModel
Usually not explicitly needed, except if a custom queryset class
is to be used.
"""
def __init__(self, *args, **kwargs):
"init our queryset object member variables"
self.polymorphic_disabled = False
super(PolymorphicQuerySet, self).__init__(*args, **kwargs)
def _clone(self, *args, **kwargs):
"Django's _clone only copies its own variables, so we need to copy ours here"
new = super(PolymorphicQuerySet, self)._clone(*args, **kwargs)
new.polymorphic_disabled = self.polymorphic_disabled
return new
def non_polymorphic(self, *args, **kwargs):
"""switch off polymorphic behaviour for this query.
When the queryset is evaluated, only objects of the type of the
base class used for this query are returned."""
self.polymorphic_disabled = True
return self
def instance_of(self, *args):
"""Filter the queryset to only include the classes in args (and their subclasses).
Implementation in _translate_polymorphic_filter_defnition."""
return self.filter(instance_of=args)
def not_instance_of(self, *args):
"""Filter the queryset to exclude the classes in args (and their subclasses).
Implementation in _translate_polymorphic_filter_defnition."""
return self.filter(not_instance_of=args)
def _filter_or_exclude(self, negate, *args, **kwargs):
"We override this internal Django functon as it is used for all filter member functions."
translate_polymorphic_filter_definitions_in_args(self.model, args) # the Q objects
additional_args = translate_polymorphic_filter_definitions_in_kwargs(self.model, kwargs) # filter_field='data'
return super(PolymorphicQuerySet, self)._filter_or_exclude(negate, *(list(args) + additional_args), **kwargs)
def order_by(self, *args, **kwargs):
"""translate the field paths in the args, then call vanilla order_by."""
new_args = [translate_polymorphic_field_path(self.model, a) for a in args]
return super(PolymorphicQuerySet, self).order_by(*new_args, **kwargs)
def _process_aggregate_args(self, args, kwargs):
"""for aggregate and annotate kwargs: allow ModelX___field syntax for kwargs, forbid it for args.
Modifies kwargs if needed (these are Aggregate objects, we translate the lookup member variable)"""
for a in args:
assert not '___' in a.lookup, 'PolymorphicModel: annotate()/aggregate(): ___ model lookup supported for keyword arguments only'
for a in six.itervalues(kwargs):
a.lookup = translate_polymorphic_field_path(self.model, a.lookup)
def annotate(self, *args, **kwargs):
"""translate the polymorphic field paths in the kwargs, then call vanilla annotate.
_get_real_instances will do the rest of the job after executing the query."""
self._process_aggregate_args(args, kwargs)
return super(PolymorphicQuerySet, self).annotate(*args, **kwargs)
def aggregate(self, *args, **kwargs):
"""translate the polymorphic field paths in the kwargs, then call vanilla aggregate.
We need no polymorphic object retrieval for aggregate => switch it off."""
self._process_aggregate_args(args, kwargs)
self.polymorphic_disabled = True
return super(PolymorphicQuerySet, self).aggregate(*args, **kwargs)
# Since django_polymorphic 'V1.0 beta2', extra() always returns polymorphic results.^
# The resulting objects are required to have a unique primary key within the result set
# (otherwise an error is thrown).
# The "polymorphic" keyword argument is not supported anymore.
#def extra(self, *args, **kwargs):
def _get_real_instances(self, base_result_objects):
"""
Polymorphic object loader
Does the same as:
return [ o.get_real_instance() for o in base_result_objects ]
but more efficiently.
The list base_result_objects contains the objects from the executed
base class query. The class of all of them is self.model (our base model).
Some, many or all of these objects were not created and stored as
class self.model, but as a class derived from self.model. We want to re-fetch
these objects from the db as their original class so we can return them
just as they were created/saved.
We identify these objects by looking at o.polymorphic_ctype, which specifies
the real class of these objects (the class at the time they were saved).
First, we sort the result objects in base_result_objects for their
subclass (from o.polymorphic_ctype), and then we execute one db query per
subclass of objects. Here, we handle any annotations from annotate().
Finally we re-sort the resulting objects into the correct order and
return them as a list.
"""
ordered_id_list = [] # list of ids of result-objects in correct order
results = {} # polymorphic dict of result-objects, keyed with their id (no order)
# dict contains one entry per unique model type occurring in result,
# in the format idlist_per_model[modelclass]=[list-of-object-ids]
idlist_per_model = defaultdict(list)
# - sort base_result_object ids into idlist_per_model lists, depending on their real class;
# - also record the correct result order in "ordered_id_list"
# - store objects that already have the correct class into "results"
base_result_objects_by_id = {}
self_model_class_id = ContentType.objects.get_for_model(self.model, for_concrete_model=False).pk
self_concrete_model_class_id = ContentType.objects.get_for_model(self.model, for_concrete_model=True).pk
for base_object in base_result_objects:
ordered_id_list.append(base_object.pk)
# check if id of the result object occeres more than once - this can happen e.g. with base_objects.extra(tables=...)
if not base_object.pk in base_result_objects_by_id:
base_result_objects_by_id[base_object.pk] = base_object
if base_object.polymorphic_ctype_id == self_model_class_id:
# Real class is exactly the same as base class, go straight to results
results[base_object.pk] = base_object
else:
real_concrete_class = base_object.get_real_instance_class()
real_concrete_class_id = base_object.get_real_concrete_instance_class_id()
if real_concrete_class_id is None:
# Dealing with a stale content type
continue
elif real_concrete_class_id == self_concrete_model_class_id:
# Real and base classes share the same concrete ancestor,
# upcast it and put it in the results
results[base_object.pk] = transmogrify(real_concrete_class, base_object)
else:
real_concrete_class = ContentType.objects.get_for_id(real_concrete_class_id).model_class()
idlist_per_model[real_concrete_class].append(base_object.pk)
# django's automatic ".pk" field does not always work correctly for
# custom fields in derived objects (unclear yet who to put the blame on).
# We get different type(o.pk) in this case.
# We work around this by using the real name of the field directly
# for accessing the primary key of the the derived objects.
# We might assume that self.model._meta.pk.name gives us the name of the primary key field,
# but it doesn't. Therefore we use polymorphic_primary_key_name, which we set up in base.py.
pk_name = self.model.polymorphic_primary_key_name
# For each model in "idlist_per_model" request its objects (the real model)
# from the db and store them in results[].
# Then we copy the annotate fields from the base objects to the real objects.
# Then we copy the extra() select fields from the base objects to the real objects.
# TODO: defer(), only(): support for these would be around here
for real_concrete_class, idlist in idlist_per_model.items():
real_objects = real_concrete_class.base_objects.filter(pk__in=idlist) # use pk__in instead ####
real_objects.query.select_related = self.query.select_related # copy select related configuration to new qs
for real_object in real_objects:
o_pk = getattr(real_object, pk_name)
real_class = real_object.get_real_instance_class()
# If the real class is a proxy, upcast it
if real_class != real_concrete_class:
real_object = transmogrify(real_class, real_object)
if self.query.aggregates:
for anno_field_name in six.iterkeys(self.query.aggregates):
attr = getattr(base_result_objects_by_id[o_pk], anno_field_name)
setattr(real_object, anno_field_name, attr)
if self.query.extra_select:
for select_field_name in six.iterkeys(self.query.extra_select):
attr = getattr(base_result_objects_by_id[o_pk], select_field_name)
setattr(real_object, select_field_name, attr)
results[o_pk] = real_object
# 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 = six.iterkeys(self.query.aggregates) # get annotate field list
for real_object in resultlist:
real_object.polymorphic_annotate_names = annotate_names
# set polymorphic_extra_select_names in all objects (currently just used for debugging/printing)
if self.query.extra_select:
extra_select_names = six.iterkeys(self.query.extra_select) # get extra select field list
for real_object in resultlist:
real_object.polymorphic_extra_select_names = extra_select_names
return resultlist
def iterator(self):
"""
This function is used by Django for all object retrieval.
By overriding it, we modify the objects that this queryset returns
when it is evaluated (or its get method or other object-returning methods are called).
Here we do the same as:
base_result_objects=list(super(PolymorphicQuerySet, self).iterator())
real_results=self._get_real_instances(base_result_objects)
for o in real_results: yield o
but it requests the objects in chunks from the database,
with Polymorphic_QuerySet_objects_per_request per chunk
"""
base_iter = super(PolymorphicQuerySet, self).iterator()
# disabled => work just like a normal queryset
if self.polymorphic_disabled:
for o in base_iter:
yield o
raise StopIteration
while True:
base_result_objects = []
reached_end = False
for i in range(Polymorphic_QuerySet_objects_per_request):
try:
o = next(base_iter)
base_result_objects.append(o)
except StopIteration:
reached_end = True
break
real_results = self._get_real_instances(base_result_objects)
for o in real_results:
yield o
if reached_end:
raise StopIteration
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)
class _p_list_class(list):
def __repr__(self, *args, **kwargs):
result = [repr(o) for o in self]
return '[ ' + ',\n '.join(result) + ' ]'
def get_real_instances(self, base_result_objects=None):
"same as _get_real_instances, but make sure that __repr__ for ShowField... creates correct output"
if not base_result_objects:
base_result_objects = self
olist = self._get_real_instances(base_result_objects)
if not self.model.polymorphic_query_multiline_output:
return olist
clist = PolymorphicQuerySet._p_list_class(olist)
return clist

View File

@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
""" PolymorphicQuerySet support functions
Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/
"""
from __future__ import absolute_import
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, FieldDoesNotExist
from django.db.models.related import RelatedObject
from functools import reduce
###################################################################################
### PolymorphicQuerySet support functions
# These functions implement the additional filter- and Q-object functionality.
# They form a kind of small framework for easily adding more
# functionality to filters and Q objects.
# Probably a more general queryset enhancement class could be made out of them.
def translate_polymorphic_filter_definitions_in_kwargs(queryset_model, kwargs):
"""
Translate the keyword argument list for PolymorphicQuerySet.filter()
Any kwargs with special polymorphic functionality are replaced in the kwargs
dict with their vanilla django equivalents.
For some kwargs a direct replacement is not possible, as a Q object is needed
instead to implement the required functionality. In these cases the kwarg is
deleted from the kwargs dict and a Q object is added to the return list.
Modifies: kwargs dict
Returns: a list of non-keyword-arguments (Q objects) to be added to the filter() query.
"""
additional_args = []
for field_path, val in kwargs.copy().items(): # Python 3 needs copy
new_expr = _translate_polymorphic_filter_definition(queryset_model, field_path, val)
if type(new_expr) == tuple:
# replace kwargs element
del(kwargs[field_path])
kwargs[new_expr[0]] = new_expr[1]
elif isinstance(new_expr, models.Q):
del(kwargs[field_path])
additional_args.append(new_expr)
return additional_args
def translate_polymorphic_Q_object(queryset_model, potential_q_object):
def tree_node_correct_field_specs(my_model, node):
" process all children of this Q node "
for i in range(len(node.children)):
child = node.children[i]
if type(child) == tuple:
# this Q object child is a tuple => a kwarg like Q( instance_of=ModelB )
key, val = child
new_expr = _translate_polymorphic_filter_definition(my_model, key, val)
if new_expr:
node.children[i] = new_expr
else:
# this Q object child is another Q object, recursively process this as well
tree_node_correct_field_specs(my_model, child)
if isinstance(potential_q_object, models.Q):
tree_node_correct_field_specs(queryset_model, potential_q_object)
return potential_q_object
def translate_polymorphic_filter_definitions_in_args(queryset_model, args):
"""
Translate the non-keyword argument list for PolymorphicQuerySet.filter()
In the args list, we replace all kwargs to Q-objects that contain special
polymorphic functionality with their vanilla django equivalents.
We traverse the Q object tree for this (which is simple).
TODO: investigate: we modify the Q-objects ina args in-place. Is this OK?
Modifies: args list
"""
for q in args:
translate_polymorphic_Q_object(queryset_model, q)
def _translate_polymorphic_filter_definition(queryset_model, field_path, field_val):
"""
Translate a keyword argument (field_path=field_val), as used for
PolymorphicQuerySet.filter()-like functions (and Q objects).
A kwarg with special polymorphic functionality is translated into
its vanilla django equivalent, which is returned, either as tuple
(field_path, field_val) or as Q object.
Returns: kwarg tuple or Q object or None (if no change is required)
"""
# handle instance_of expressions or alternatively,
# if this is a normal Django filter expression, return None
if field_path == 'instance_of':
return _create_model_filter_Q(field_val)
elif field_path == 'not_instance_of':
return _create_model_filter_Q(field_val, not_instance_of=True)
elif not '___' in field_path:
return None # no change
# filter expression contains '___' (i.e. filter for polymorphic field)
# => get the model class specified in the filter expression
newpath = translate_polymorphic_field_path(queryset_model, field_path)
return (newpath, field_val)
def translate_polymorphic_field_path(queryset_model, field_path):
"""
Translate a field path from a keyword argument, as used for
PolymorphicQuerySet.filter()-like functions (and Q objects).
Supports leading '-' (for order_by args).
E.g.: if queryset_model is ModelA, then "ModelC___field3" is translated
into modela__modelb__modelc__field3.
Returns: translated path (unchanged, if no translation needed)
"""
classname, sep, pure_field_path = field_path.partition('___')
if not sep:
return field_path
assert classname, 'PolymorphicModel: %s: bad field specification' % field_path
negated = False
if classname[0] == '-':
negated = True
classname = classname.lstrip('-')
if '__' in classname:
# the user has app label prepended to class name via __ => use Django's get_model function
appname, sep, classname = classname.partition('__')
model = models.get_model(appname, classname)
assert model, 'PolymorphicModel: model %s (in app %s) not found!' % (model.__name__, appname)
if not issubclass(model, queryset_model):
e = 'PolymorphicModel: queryset filter error: "' + model.__name__ + '" is not derived from "' + queryset_model.__name__ + '"'
raise AssertionError(e)
else:
# the user has only given us the class name via ___
# => select the model from the sub models of the queryset base model
# Test whether it's actually a regular relation__ _fieldname (the field starting with an _)
# so no tripple ClassName___field was intended.
try:
# rel = (field_object, model, direct, m2m)
field = queryset_model._meta.get_field_by_name(classname)[0]
if isinstance(field, RelatedObject):
# Can also test whether the field exists in the related object to avoid ambiguity between
# class names and field names, but that never happens when your class names are in CamelCase.
return field_path # No exception raised, field does exist.
except FieldDoesNotExist:
pass
# function to collect all sub-models, this should be optimized (cached)
def add_all_sub_models(model, result):
if issubclass(model, models.Model) and model != models.Model:
# model name is occurring twice in submodel inheritance tree => Error
if model.__name__ in result and model != result[model.__name__]:
e = 'PolymorphicModel: model name alone is ambiguous: %s.%s and %s.%s!\n'
e += 'In this case, please use the syntax: applabel__ModelName___field'
assert model, e % (
model._meta.app_label, model.__name__,
result[model.__name__]._meta.app_label, result[model.__name__].__name__)
result[model.__name__] = model
for b in model.__subclasses__():
add_all_sub_models(b, result)
submodels = {}
add_all_sub_models(queryset_model, submodels)
model = submodels.get(classname, None)
assert model, 'PolymorphicModel: model %s not found (not a subclass of %s)!' % (classname, queryset_model.__name__)
# create new field path for expressions, e.g. for baseclass=ModelA, myclass=ModelC
# 'modelb__modelc" is returned
def _create_base_path(baseclass, myclass):
bases = myclass.__bases__
for b in bases:
if b == baseclass:
return myclass.__name__.lower()
path = _create_base_path(baseclass, b)
if path:
return path + '__' + myclass.__name__.lower()
return ''
basepath = _create_base_path(queryset_model, model)
if negated:
newpath = '-'
else:
newpath = ''
newpath += basepath
if basepath:
newpath += '__'
newpath += pure_field_path
return newpath
def _create_model_filter_Q(modellist, not_instance_of=False):
"""
Helper function for instance_of / not_instance_of
Creates and returns a Q object that filters for the models in modellist,
including all subclasses of these models (as we want to do the same
as pythons isinstance() ).
.
We recursively collect all __subclasses__(), create a Q filter for each,
and or-combine these Q objects. This could be done much more
efficiently however (regarding the resulting sql), should an optimization
be needed.
"""
if not modellist:
return None
from .polymorphic_model import PolymorphicModel
if type(modellist) != list and type(modellist) != tuple:
if issubclass(modellist, PolymorphicModel):
modellist = [modellist]
else:
assert False, 'PolymorphicModel: instance_of expects a list of (polymorphic) models or a single (polymorphic) model'
def q_class_with_subclasses(model):
q = Q(polymorphic_ctype=ContentType.objects.get_for_model(model, for_concrete_model=False))
for subclass in model.__subclasses__():
q = q | q_class_with_subclasses(subclass)
return q
qlist = [q_class_with_subclasses(m) for m in modellist]
q_ored = reduce(lambda a, b: a | b, qlist)
if not_instance_of:
q_ored = ~q_ored
return q_ored

View File

@ -0,0 +1,164 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.utils import six
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
# these may be overridden by the user
polymorphic_showfield_max_line_width = None
polymorphic_showfield_max_field_width = 20
polymorphic_showfield_old_format = False
def __repr__(self):
return self.__unicode__()
def _showfields_get_content(self, field_name, field_type=type(None)):
"helper for __unicode__"
content = getattr(self, field_name)
if self.polymorphic_showfield_old_format:
out = ': '
else:
out = ' '
if issubclass(field_type, models.ForeignKey):
if content is None:
out += 'None'
else:
out += content.__class__.__name__
elif issubclass(field_type, models.ManyToManyField):
out += '%d' % content.count()
elif isinstance(content, six.integer_types):
out += str(content)
elif content is None:
out += 'None'
else:
txt = str(content)
if len(txt) > self.polymorphic_showfield_max_field_width:
txt = txt[:self.polymorphic_showfield_max_field_width - 2] + '..'
out += '"' + txt + '"'
return out
def _showfields_add_regular_fields(self, parts):
"helper for __unicode__"
done_fields = set()
for field in self._meta.fields + self._meta.many_to_many:
if field.name in self.polymorphic_internal_model_fields or '_ptr' in field.name:
continue
if field.name in done_fields:
continue # work around django diamond inheritance problem
done_fields.add(field.name)
out = field.name
# if this is the standard primary key named "id", print it as we did with older versions of django_polymorphic
if field.primary_key and field.name == 'id' and type(field) == models.AutoField:
out += ' ' + str(getattr(self, field.name))
# otherwise, display it just like all other fields (with correct type, shortened content etc.)
else:
if self.polymorphic_showfield_type:
out += ' (' + type(field).__name__
if field.primary_key:
out += '/pk'
out += ')'
if self.polymorphic_showfield_content:
out += self._showfields_get_content(field.name, type(field))
parts.append((False, out, ','))
def _showfields_add_dynamic_fields(self, field_list, title, parts):
"helper for __unicode__"
parts.append((True, '- ' + title, ':'))
for field_name in field_list:
out = field_name
content = getattr(self, field_name)
if self.polymorphic_showfield_type:
out += ' (' + type(content).__name__ + ')'
if self.polymorphic_showfield_content:
out += self._showfields_get_content(field_name)
parts.append((False, out, ','))
def __unicode__(self):
# create list ("parts") containing one tuple for each title/field:
# ( bool: new section , item-text , separator to use after item )
# start with model name
parts = [(True, self.__class__.__name__, ':')]
# add all regular fields
self._showfields_add_regular_fields(parts)
# add annotate fields
if hasattr(self, 'polymorphic_annotate_names'):
self._showfields_add_dynamic_fields(self.polymorphic_annotate_names, 'Ann', parts)
# add extra() select fields
if hasattr(self, 'polymorphic_extra_select_names'):
self._showfields_add_dynamic_fields(self.polymorphic_extra_select_names, 'Extra', parts)
# format result
indent = len(self.__class__.__name__) + 5
indentstr = ''.rjust(indent)
out = ''
xpos = 0
possible_line_break_pos = None
for i in range(len(parts)):
new_section, p, separator = parts[i]
final = (i == len(parts) - 1)
if not final:
next_new_section, _, _ = parts[i + 1]
if (self.polymorphic_showfield_max_line_width
and xpos + len(p) > self.polymorphic_showfield_max_line_width
and possible_line_break_pos != None):
rest = out[possible_line_break_pos:]
out = out[:possible_line_break_pos]
out += '\n' + indentstr + rest
xpos = indent + len(rest)
out += p
xpos += len(p)
if not final:
if not next_new_section:
out += separator
xpos += len(separator)
out += ' '
xpos += 1
if not new_section:
possible_line_break_pos = len(out)
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

View File

@ -0,0 +1,11 @@
{% extends "admin/change_form.html" %}
{% if save_on_top %}
{% block submit_buttons_top %}
{% include 'admin/submit_line.html' with show_save=1 %}
{% endblock %}
{% endif %}
{% block submit_buttons_bottom %}
{% include 'admin/submit_line.html' with show_save=1 %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "admin/change_form.html" %}
{% load polymorphic_admin_tags %}
{% block breadcrumbs %}
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "admin/delete_confirmation.html" %}
{% load polymorphic_admin_tags %}
{% block breadcrumbs %}
{% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %}
{% endblock %}

View File

@ -0,0 +1,53 @@
from django.template import Library, Node, TemplateSyntaxError
from django.utils import six
register = Library()
class BreadcrumbScope(Node):
def __init__(self, base_opts, nodelist):
self.base_opts = base_opts
self.nodelist = nodelist # Note, takes advantage of Node.child_nodelists
@classmethod
def parse(cls, parser, token):
bits = token.split_contents()
if len(bits) == 2:
(tagname, base_opts) = bits
base_opts = parser.compile_filter(base_opts)
nodelist = parser.parse(('endbreadcrumb_scope',))
parser.delete_first_token()
return cls(
base_opts=base_opts,
nodelist=nodelist
)
else:
raise TemplateSyntaxError("{0} tag expects 1 argument".format(token.contents[0]))
def render(self, context):
# app_label is really hard to overwrite in the standard Django ModelAdmin.
# To insert it in the template, the entire render_change_form() and delete_view() have to copied and adjusted.
# Instead, have an assignment tag that inserts that in the template.
base_opts = self.base_opts.resolve(context)
new_vars = {}
if base_opts and not isinstance(base_opts, six.string_types):
new_vars = {
'app_label': base_opts.app_label, # What this is all about
'opts': base_opts,
}
new_scope = context.push()
new_scope.update(new_vars)
html = self.nodelist.render(context)
context.pop()
return html
@register.tag
def breadcrumb_scope(parser, token):
"""
Easily allow the breadcrumb to be generated in the admin change templates.
"""
return BreadcrumbScope.parse(parser, token)

View File

@ -0,0 +1,791 @@
# -*- coding: utf-8 -*-
""" Test Cases
Please see README.rst or DOCS.rst or http://chrisglass.github.com/django_polymorphic/
"""
from __future__ import print_function
import uuid
import re
from django.db.models.query import QuerySet
from django.test import TestCase
from django.db.models import Q,Count
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils import six
from polymorphic import PolymorphicModel, PolymorphicManager, PolymorphicQuerySet
from polymorphic import ShowFieldContent, ShowFieldType, ShowFieldTypeAndContent
from polymorphic.tools_for_tests import UUIDField
class PlainA(models.Model):
field1 = models.CharField(max_length=10)
class PlainB(PlainA):
field2 = models.CharField(max_length=10)
class PlainC(PlainB):
field3 = models.CharField(max_length=10)
class Model2A(ShowFieldType, PolymorphicModel):
field1 = models.CharField(max_length=10)
class Model2B(Model2A):
field2 = models.CharField(max_length=10)
class Model2C(Model2B):
field3 = models.CharField(max_length=10)
class Model2D(Model2C):
field4 = models.CharField(max_length=10)
class ModelExtraA(ShowFieldTypeAndContent, PolymorphicModel):
field1 = models.CharField(max_length=10)
class ModelExtraB(ModelExtraA):
field2 = models.CharField(max_length=10)
class ModelExtraC(ModelExtraB):
field3 = models.CharField(max_length=10)
class ModelExtraExternal(models.Model):
topic = models.CharField(max_length=10)
class ModelShow1(ShowFieldType,PolymorphicModel):
field1 = models.CharField(max_length=10)
m2m = models.ManyToManyField('self')
class ModelShow2(ShowFieldContent, PolymorphicModel):
field1 = models.CharField(max_length=10)
m2m = models.ManyToManyField('self')
class ModelShow3(ShowFieldTypeAndContent, PolymorphicModel):
field1 = models.CharField(max_length=10)
m2m = models.ManyToManyField('self')
class ModelShow1_plain(PolymorphicModel):
field1 = models.CharField(max_length=10)
class ModelShow2_plain(ModelShow1_plain):
field2 = models.CharField(max_length=10)
class Base(ShowFieldType, PolymorphicModel):
field_b = models.CharField(max_length=10)
class ModelX(Base):
field_x = models.CharField(max_length=10)
class ModelY(Base):
field_y = models.CharField(max_length=10)
class Enhance_Plain(models.Model):
field_p = models.CharField(max_length=10)
class Enhance_Base(ShowFieldTypeAndContent, PolymorphicModel):
field_b = models.CharField(max_length=10)
class Enhance_Inherit(Enhance_Base, Enhance_Plain):
field_i = models.CharField(max_length=10)
class DiamondBase(models.Model):
field_b = models.CharField(max_length=10)
class DiamondX(DiamondBase):
field_x = models.CharField(max_length=10)
class DiamondY(DiamondBase):
field_y = models.CharField(max_length=10)
class DiamondXY(DiamondX, DiamondY):
pass
class RelationBase(ShowFieldTypeAndContent, PolymorphicModel):
field_base = models.CharField(max_length=10)
fk = models.ForeignKey('self', null=True, related_name='relationbase_set')
m2m = models.ManyToManyField('self')
class RelationA(RelationBase):
field_a = models.CharField(max_length=10)
class RelationB(RelationBase):
field_b = models.CharField(max_length=10)
class RelationBC(RelationB):
field_c = models.CharField(max_length=10)
class RelatingModel(models.Model):
many2many = models.ManyToManyField(Model2A)
class One2OneRelatingModel(PolymorphicModel):
one2one = models.OneToOneField(Model2A)
field1 = models.CharField(max_length=10)
class One2OneRelatingModelDerived(One2OneRelatingModel):
field2 = models.CharField(max_length=10)
class MyManagerQuerySet(PolymorphicQuerySet):
def my_queryset_foo(self):
return self.all() # Just a method to prove the existance of the custom queryset.
class MyManager(PolymorphicManager):
queryset_class = MyManagerQuerySet
def get_query_set(self):
return super(MyManager, self).get_query_set().order_by('-field1')
class ModelWithMyManager(ShowFieldTypeAndContent, Model2A):
objects = MyManager()
field4 = models.CharField(max_length=10)
class MROBase1(ShowFieldType, PolymorphicModel):
objects = MyManager()
field1 = models.CharField(max_length=10) # needed as MyManager uses it
class MROBase2(MROBase1):
pass # Django vanilla inheritance does not inherit MyManager as _default_manager here
class MROBase3(models.Model):
objects = PolymorphicManager()
class MRODerived(MROBase2, MROBase3):
pass
class ParentModelWithManager(PolymorphicModel):
pass
class ChildModelWithManager(PolymorphicModel):
# Also test whether foreign keys receive the manager:
fk = models.ForeignKey(ParentModelWithManager, related_name='childmodel_set')
objects = MyManager()
class PlainMyManagerQuerySet(QuerySet):
def my_queryset_foo(self):
return self.all() # Just a method to prove the existance of the custom queryset.
class PlainMyManager(models.Manager):
def my_queryset_foo(self):
return self.get_query_set().my_queryset_foo()
def get_query_set(self):
return PlainMyManagerQuerySet(self.model, using=self._db)
class PlainParentModelWithManager(models.Model):
pass
class PlainChildModelWithManager(models.Model):
fk = models.ForeignKey(PlainParentModelWithManager, related_name='childmodel_set')
objects = PlainMyManager()
class MgrInheritA(models.Model):
mgrA = models.Manager()
mgrA2 = models.Manager()
field1 = models.CharField(max_length=10)
class MgrInheritB(MgrInheritA):
mgrB = models.Manager()
field2 = models.CharField(max_length=10)
class MgrInheritC(ShowFieldTypeAndContent, MgrInheritB):
pass
class BlogBase(ShowFieldTypeAndContent, PolymorphicModel):
name = models.CharField(max_length=10)
class BlogA(BlogBase):
info = models.CharField(max_length=10)
class BlogB(BlogBase):
pass
class BlogEntry(ShowFieldTypeAndContent, PolymorphicModel):
blog = models.ForeignKey(BlogA)
text = models.CharField(max_length=10)
class BlogEntry_limit_choices_to(ShowFieldTypeAndContent, PolymorphicModel):
blog = models.ForeignKey(BlogBase)
text = models.CharField(max_length=10)
class ModelFieldNameTest(ShowFieldType, PolymorphicModel):
modelfieldnametest = models.CharField(max_length=10)
class InitTestModel(ShowFieldType, PolymorphicModel):
bar = models.CharField(max_length=100)
def __init__(self, *args, **kwargs):
kwargs['bar'] = self.x()
super(InitTestModel, self).__init__(*args, **kwargs)
class InitTestModelSubclass(InitTestModel):
def x(self):
return 'XYZ'
# models from github issue
class Top(PolymorphicModel):
name = models.CharField(max_length=50)
class Meta:
ordering = ('pk',)
class Middle(Top):
description = models.TextField()
class Bottom(Middle):
author = models.CharField(max_length=50)
class UUIDProject(ShowFieldTypeAndContent, PolymorphicModel):
uuid_primary_key = UUIDField(primary_key = True)
topic = models.CharField(max_length = 30)
class UUIDArtProject(UUIDProject):
artist = models.CharField(max_length = 30)
class UUIDResearchProject(UUIDProject):
supervisor = models.CharField(max_length = 30)
class UUIDPlainA(models.Model):
uuid_primary_key = UUIDField(primary_key = True)
field1 = models.CharField(max_length=10)
class UUIDPlainB(UUIDPlainA):
field2 = models.CharField(max_length=10)
class UUIDPlainC(UUIDPlainB):
field3 = models.CharField(max_length=10)
# base -> proxy
class ProxyBase(PolymorphicModel):
some_data = models.CharField(max_length=128)
class ProxyChild(ProxyBase):
class Meta:
proxy = True
# base -> proxy -> real models
class ProxiedBase(ShowFieldTypeAndContent, PolymorphicModel):
name = models.CharField(max_length=10)
class ProxyModelBase(ProxiedBase):
class Meta:
proxy = True
class ProxyModelA(ProxyModelBase):
field1 = models.CharField(max_length=10)
class ProxyModelB(ProxyModelBase):
field2 = models.CharField(max_length=10)
# test bad field name
#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(ShowFieldType, PolymorphicModel):
ctype = models.ForeignKey(ContentType, null=True, editable=False)
class PolymorphicTests(TestCase):
"""
The test suite
"""
def test_diamond_inheritance(self):
# Django diamond problem
# https://code.djangoproject.com/ticket/10808
o1 = DiamondXY.objects.create(field_b='b', field_x='x', field_y='y')
o2 = DiamondXY.objects.get()
if o2.field_b != 'b':
print('')
print('# known django model inheritance diamond problem detected')
print('DiamondXY fields 1: field_b "{0}", field_x "{1}", field_y "{2}"'.format(o1.field_b, o1.field_x, o1.field_y))
print('DiamondXY fields 2: field_b "{0}", field_x "{1}", field_y "{2}"'.format(o2.field_b, o2.field_x, o2.field_y))
def test_annotate_aggregate_order(self):
# create a blog of type BlogA
# create two blog entries in BlogA
# create some blogs of type BlogB to make the BlogBase table data really polymorphic
blog = BlogA.objects.create(name='B1', info='i1')
blog.blogentry_set.create(text='bla')
BlogEntry.objects.create(blog=blog, text='bla2')
BlogB.objects.create(name='Bb1')
BlogB.objects.create(name='Bb2')
BlogB.objects.create(name='Bb3')
qs = BlogBase.objects.annotate(entrycount=Count('BlogA___blogentry'))
self.assertEqual(len(qs), 4)
for o in qs:
if o.name == 'B1':
self.assertEqual(o.entrycount, 2)
else:
self.assertEqual(o.entrycount, 0)
x = BlogBase.objects.aggregate(entrycount=Count('BlogA___blogentry'))
self.assertEqual(x['entrycount'], 2)
# create some more blogs for next test
BlogA.objects.create(name='B2', info='i2')
BlogA.objects.create(name='B3', info='i3')
BlogA.objects.create(name='B4', info='i4')
BlogA.objects.create(name='B5', info='i5')
# test ordering for field in all entries
expected = '''
[ <BlogB: id 4, name (CharField) "Bb3">,
<BlogB: id 3, name (CharField) "Bb2">,
<BlogB: id 2, name (CharField) "Bb1">,
<BlogA: id 8, name (CharField) "B5", info (CharField) "i5">,
<BlogA: id 7, name (CharField) "B4", info (CharField) "i4">,
<BlogA: id 6, name (CharField) "B3", info (CharField) "i3">,
<BlogA: id 5, name (CharField) "B2", info (CharField) "i2">,
<BlogA: id 1, name (CharField) "B1", info (CharField) "i1"> ]'''
x = '\n' + repr(BlogBase.objects.order_by('-name'))
self.assertEqual(x, expected)
# test ordering for field in one subclass only
# MySQL and SQLite return this order
expected1='''
[ <BlogA: id 8, name (CharField) "B5", info (CharField) "i5">,
<BlogA: id 7, name (CharField) "B4", info (CharField) "i4">,
<BlogA: id 6, name (CharField) "B3", info (CharField) "i3">,
<BlogA: id 5, name (CharField) "B2", info (CharField) "i2">,
<BlogA: id 1, name (CharField) "B1", info (CharField) "i1">,
<BlogB: id 2, name (CharField) "Bb1">,
<BlogB: id 3, name (CharField) "Bb2">,
<BlogB: id 4, name (CharField) "Bb3"> ]'''
# PostgreSQL returns this order
expected2='''
[ <BlogB: id 2, name (CharField) "Bb1">,
<BlogB: id 3, name (CharField) "Bb2">,
<BlogB: id 4, name (CharField) "Bb3">,
<BlogA: id 8, name (CharField) "B5", info (CharField) "i5">,
<BlogA: id 7, name (CharField) "B4", info (CharField) "i4">,
<BlogA: id 6, name (CharField) "B3", info (CharField) "i3">,
<BlogA: id 5, name (CharField) "B2", info (CharField) "i2">,
<BlogA: id 1, name (CharField) "B1", info (CharField) "i1"> ]'''
x = '\n' + repr(BlogBase.objects.order_by('-BlogA___info'))
self.assertTrue(x == expected1 or x == expected2)
def test_limit_choices_to(self):
"""
this is not really a testcase, as limit_choices_to only affects the Django admin
"""
# create a blog of type BlogA
blog_a = BlogA.objects.create(name='aa', info='aa')
blog_b = BlogB.objects.create(name='bb')
# create two blog entries
entry1 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text='bla2')
entry2 = BlogEntry_limit_choices_to.objects.create(blog=blog_b, text='bla2')
def test_primary_key_custom_field_problem(self):
"""
object retrieval problem occuring with some custom primary key fields (UUIDField as test case)
"""
UUIDProject.objects.create(topic="John's gathering")
UUIDArtProject.objects.create(topic="Sculpting with Tim", artist="T. Turner")
UUIDResearchProject.objects.create(topic="Swallow Aerodynamics", supervisor="Dr. Winter")
qs = UUIDProject.objects.all()
ol = list(qs)
a = qs[0]
b = qs[1]
c = qs[2]
self.assertEqual(len(qs), 3)
self.assertIsInstance(a.uuid_primary_key, uuid.UUID)
self.assertIsInstance(a.pk, uuid.UUID)
res = re.sub(' "(.*?)..", topic',', topic', repr(qs))
res_exp = """[ <UUIDProject: uuid_primary_key (UUIDField/pk), topic (CharField) "John's gathering">,
<UUIDArtProject: uuid_primary_key (UUIDField/pk), topic (CharField) "Sculpting with Tim", artist (CharField) "T. Turner">,
<UUIDResearchProject: uuid_primary_key (UUIDField/pk), topic (CharField) "Swallow Aerodynamics", supervisor (CharField) "Dr. Winter"> ]"""
self.assertEqual(res, res_exp)
#if (a.pk!= uuid.UUID or c.pk!= uuid.UUID):
# print()
# print('# known inconstency with custom primary key field detected (django problem?)')
a = UUIDPlainA.objects.create(field1='A1')
b = UUIDPlainB.objects.create(field1='B1', field2='B2')
c = UUIDPlainC.objects.create(field1='C1', field2='C2', field3='C3')
qs = UUIDPlainA.objects.all()
if a.pk!= uuid.UUID or c.pk!= uuid.UUID:
print('')
print('# known type inconstency with custom primary key field detected (django problem?)')
def create_model2abcd(self):
"""
Create the chain of objects of Model2,
this is reused in various tests.
"""
Model2A.objects.create(field1='A1')
Model2B.objects.create(field1='B1', field2='B2')
Model2C.objects.create(field1='C1', field2='C2', field3='C3')
Model2D.objects.create(field1='D1', field2='D2', field3='D3', field4='D4')
def test_simple_inheritance(self):
self.create_model2abcd()
objects = list(Model2A.objects.all())
self.assertEqual(repr(objects[0]), '<Model2A: id 1, field1 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[3]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
def test_manual_get_real_instance(self):
self.create_model2abcd()
o = Model2A.objects.non_polymorphic().get(field1='C1')
self.assertEqual(repr(o.get_real_instance()), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
def test_non_polymorphic(self):
self.create_model2abcd()
objects = list(Model2A.objects.all().non_polymorphic())
self.assertEqual(repr(objects[0]), '<Model2A: id 1, field1 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2A: id 2, field1 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2A: id 3, field1 (CharField)>')
self.assertEqual(repr(objects[3]), '<Model2A: id 4, field1 (CharField)>')
def test_get_real_instances(self):
self.create_model2abcd()
qs = Model2A.objects.all().non_polymorphic()
# from queryset
objects = qs.get_real_instances()
self.assertEqual(repr(objects[0]), '<Model2A: id 1, field1 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[3]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
# from a manual list
objects = Model2A.objects.get_real_instances(list(qs))
self.assertEqual(repr(objects[0]), '<Model2A: id 1, field1 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[3]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
def test_translate_polymorphic_q_object(self):
self.create_model2abcd()
q = Model2A.translate_polymorphic_Q_object(Q(instance_of=Model2C))
objects = Model2A.objects.filter(q)
self.assertEqual(repr(objects[0]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
def test_base_manager(self):
def show_base_manager(model):
return "{0} {1}".format(
repr(type(model._base_manager)),
repr(model._base_manager.model)
)
self.assertEqual(show_base_manager(PlainA), "<class 'django.db.models.manager.Manager'> <class 'polymorphic.tests.PlainA'>")
self.assertEqual(show_base_manager(PlainB), "<class 'django.db.models.manager.Manager'> <class 'polymorphic.tests.PlainB'>")
self.assertEqual(show_base_manager(PlainC), "<class 'django.db.models.manager.Manager'> <class 'polymorphic.tests.PlainC'>")
self.assertEqual(show_base_manager(Model2A), "<class 'polymorphic.manager.PolymorphicManager'> <class 'polymorphic.tests.Model2A'>")
self.assertEqual(show_base_manager(Model2B), "<class 'django.db.models.manager.Manager'> <class 'polymorphic.tests.Model2B'>")
self.assertEqual(show_base_manager(Model2C), "<class 'django.db.models.manager.Manager'> <class 'polymorphic.tests.Model2C'>")
self.assertEqual(show_base_manager(One2OneRelatingModel), "<class 'polymorphic.manager.PolymorphicManager'> <class 'polymorphic.tests.One2OneRelatingModel'>")
self.assertEqual(show_base_manager(One2OneRelatingModelDerived), "<class 'django.db.models.manager.Manager'> <class 'polymorphic.tests.One2OneRelatingModelDerived'>")
def test_foreignkey_field(self):
self.create_model2abcd()
object2a = Model2A.base_objects.get(field1='C1')
self.assertEqual(repr(object2a.model2b), '<Model2B: id 3, field1 (CharField), field2 (CharField)>')
object2b = Model2B.base_objects.get(field1='C1')
self.assertEqual(repr(object2b.model2c), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
def test_onetoone_field(self):
self.create_model2abcd()
a = Model2A.base_objects.get(field1='C1')
b = One2OneRelatingModelDerived.objects.create(one2one=a, field1='f1', field2='f2')
# this result is basically wrong, probably due to Django cacheing (we used base_objects), but should not be a problem
self.assertEqual(repr(b.one2one), '<Model2A: id 3, field1 (CharField)>')
c = One2OneRelatingModelDerived.objects.get(field1='f1')
self.assertEqual(repr(c.one2one), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(a.one2onerelatingmodel), '<One2OneRelatingModelDerived: One2OneRelatingModelDerived object>')
def test_manytomany_field(self):
# Model 1
o = ModelShow1.objects.create(field1='abc')
o.m2m.add(o)
o.save()
self.assertEqual(repr(ModelShow1.objects.all()), '[ <ModelShow1: id 1, field1 (CharField), m2m (ManyToManyField)> ]')
# Model 2
o = ModelShow2.objects.create(field1='abc')
o.m2m.add(o)
o.save()
self.assertEqual(repr(ModelShow2.objects.all()), '[ <ModelShow2: id 1, field1 "abc", m2m 1> ]')
# Model 3
o=ModelShow3.objects.create(field1='abc')
o.m2m.add(o)
o.save()
self.assertEqual(repr(ModelShow3.objects.all()), '[ <ModelShow3: id 1, field1 (CharField) "abc", m2m (ManyToManyField) 1> ]')
self.assertEqual(repr(ModelShow1.objects.all().annotate(Count('m2m'))), '[ <ModelShow1: id 1, field1 (CharField), m2m (ManyToManyField) - Ann: m2m__count (int)> ]')
self.assertEqual(repr(ModelShow2.objects.all().annotate(Count('m2m'))), '[ <ModelShow2: id 1, field1 "abc", m2m 1 - Ann: m2m__count 1> ]')
self.assertEqual(repr(ModelShow3.objects.all().annotate(Count('m2m'))), '[ <ModelShow3: id 1, field1 (CharField) "abc", m2m (ManyToManyField) 1 - Ann: m2m__count (int) 1> ]')
# no pretty printing
ModelShow1_plain.objects.create(field1='abc')
ModelShow2_plain.objects.create(field1='abc', field2='def')
self.assertEqual(repr(ModelShow1_plain.objects.all()), '[<ModelShow1_plain: ModelShow1_plain object>, <ModelShow2_plain: ModelShow2_plain object>]')
def test_extra_method(self):
self.create_model2abcd()
objects = list(Model2A.objects.extra(where=['id IN (2, 3)']))
self.assertEqual(repr(objects[0]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
objects = Model2A.objects.extra(select={"select_test": "field1 = 'A1'"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by=['-id'])
self.assertEqual(repr(objects[0]), '<Model2B: id 2, field1 (CharField), field2 (CharField) - Extra: select_test (int)>')
self.assertEqual(repr(objects[1]), '<Model2A: id 1, field1 (CharField) - Extra: select_test (int)>')
self.assertEqual(len(objects), 2) # Placed after the other tests, only verifying whether there are no more additional objects.
ModelExtraA.objects.create(field1='A1')
ModelExtraB.objects.create(field1='B1', field2='B2')
ModelExtraC.objects.create(field1='C1', field2='C2', field3='C3')
ModelExtraExternal.objects.create(topic='extra1')
ModelExtraExternal.objects.create(topic='extra2')
ModelExtraExternal.objects.create(topic='extra3')
objects = ModelExtraA.objects.extra(tables=["polymorphic_modelextraexternal"], select={"topic":"polymorphic_modelextraexternal.topic"}, where=["polymorphic_modelextraa.id = polymorphic_modelextraexternal.id"])
if six.PY3:
self.assertEqual(repr(objects[0]), '<ModelExtraA: id 1, field1 (CharField) "A1" - Extra: topic (str) "extra1">')
self.assertEqual(repr(objects[1]), '<ModelExtraB: id 2, field1 (CharField) "B1", field2 (CharField) "B2" - Extra: topic (str) "extra2">')
self.assertEqual(repr(objects[2]), '<ModelExtraC: id 3, field1 (CharField) "C1", field2 (CharField) "C2", field3 (CharField) "C3" - Extra: topic (str) "extra3">')
else:
self.assertEqual(repr(objects[0]), '<ModelExtraA: id 1, field1 (CharField) "A1" - Extra: topic (unicode) "extra1">')
self.assertEqual(repr(objects[1]), '<ModelExtraB: id 2, field1 (CharField) "B1", field2 (CharField) "B2" - Extra: topic (unicode) "extra2">')
self.assertEqual(repr(objects[2]), '<ModelExtraC: id 3, field1 (CharField) "C1", field2 (CharField) "C2", field3 (CharField) "C3" - Extra: topic (unicode) "extra3">')
self.assertEqual(len(objects), 3)
def test_instance_of_filter(self):
self.create_model2abcd()
objects = Model2A.objects.instance_of(Model2B)
self.assertEqual(repr(objects[0]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
self.assertEqual(len(objects), 3)
objects = Model2A.objects.filter(instance_of=Model2B)
self.assertEqual(repr(objects[0]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
self.assertEqual(len(objects), 3)
objects = Model2A.objects.filter(Q(instance_of=Model2B))
self.assertEqual(repr(objects[0]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
self.assertEqual(len(objects), 3)
objects = Model2A.objects.not_instance_of(Model2B)
self.assertEqual(repr(objects[0]), '<Model2A: id 1, field1 (CharField)>')
self.assertEqual(len(objects), 1)
def test_polymorphic___filter(self):
self.create_model2abcd()
objects = Model2A.objects.filter(Q( Model2B___field2='B2') | Q( Model2C___field3='C3'))
self.assertEqual(len(objects), 2)
self.assertEqual(repr(objects[0]), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
def test_delete(self):
self.create_model2abcd()
oa = Model2A.objects.get(id=2)
self.assertEqual(repr(oa), '<Model2B: id 2, field1 (CharField), field2 (CharField)>')
self.assertEqual(Model2A.objects.count(), 4)
oa.delete()
objects = Model2A.objects.all()
self.assertEqual(repr(objects[0]), '<Model2A: id 1, field1 (CharField)>')
self.assertEqual(repr(objects[1]), '<Model2C: id 3, field1 (CharField), field2 (CharField), field3 (CharField)>')
self.assertEqual(repr(objects[2]), '<Model2D: id 4, field1 (CharField), field2 (CharField), field3 (CharField), field4 (CharField)>')
self.assertEqual(len(objects), 3)
def test_combine_querysets(self):
ModelX.objects.create(field_x='x')
ModelY.objects.create(field_y='y')
qs = Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY)
self.assertEqual(repr(qs[0]), '<ModelX: id 1, field_b (CharField), field_x (CharField)>')
self.assertEqual(repr(qs[1]), '<ModelY: id 2, field_b (CharField), field_y (CharField)>')
self.assertEqual(len(qs), 2)
def test_multiple_inheritance(self):
# multiple inheritance, subclassing third party models (mix PolymorphicModel with models.Model)
Enhance_Base.objects.create(field_b='b-base')
Enhance_Inherit.objects.create(field_b='b-inherit', field_p='p', field_i='i')
qs = Enhance_Base.objects.all()
self.assertEqual(repr(qs[0]), '<Enhance_Base: id 1, field_b (CharField) "b-base">')
self.assertEqual(repr(qs[1]), '<Enhance_Inherit: id 2, field_b (CharField) "b-inherit", field_p (CharField) "p", field_i (CharField) "i">')
self.assertEqual(len(qs), 2)
def test_relation_base(self):
# ForeignKey, ManyToManyField
obase = RelationBase.objects.create(field_base='base')
oa = RelationA.objects.create(field_base='A1', field_a='A2', fk=obase)
ob = RelationB.objects.create(field_base='B1', field_b='B2', fk=oa)
oc = RelationBC.objects.create(field_base='C1', field_b='C2', field_c='C3', fk=oa)
oa.m2m.add(oa)
oa.m2m.add(ob)
objects = RelationBase.objects.all()
self.assertEqual(repr(objects[0]), '<RelationBase: id 1, field_base (CharField) "base", fk (ForeignKey) None, m2m (ManyToManyField) 0>')
self.assertEqual(repr(objects[1]), '<RelationA: id 2, field_base (CharField) "A1", fk (ForeignKey) RelationBase, field_a (CharField) "A2", m2m (ManyToManyField) 2>')
self.assertEqual(repr(objects[2]), '<RelationB: id 3, field_base (CharField) "B1", fk (ForeignKey) RelationA, field_b (CharField) "B2", m2m (ManyToManyField) 1>')
self.assertEqual(repr(objects[3]), '<RelationBC: id 4, field_base (CharField) "C1", fk (ForeignKey) RelationA, field_b (CharField) "C2", field_c (CharField) "C3", m2m (ManyToManyField) 0>')
self.assertEqual(len(objects), 4)
oa = RelationBase.objects.get(id=2)
self.assertEqual(repr(oa.fk), '<RelationBase: id 1, field_base (CharField) "base", fk (ForeignKey) None, m2m (ManyToManyField) 0>')
objects = oa.relationbase_set.all()
self.assertEqual(repr(objects[0]), '<RelationB: id 3, field_base (CharField) "B1", fk (ForeignKey) RelationA, field_b (CharField) "B2", m2m (ManyToManyField) 1>')
self.assertEqual(repr(objects[1]), '<RelationBC: id 4, field_base (CharField) "C1", fk (ForeignKey) RelationA, field_b (CharField) "C2", field_c (CharField) "C3", m2m (ManyToManyField) 0>')
self.assertEqual(len(objects), 2)
ob = RelationBase.objects.get(id=3)
self.assertEqual(repr(ob.fk), '<RelationA: id 2, field_base (CharField) "A1", fk (ForeignKey) RelationBase, field_a (CharField) "A2", m2m (ManyToManyField) 2>')
oa = RelationA.objects.get()
objects = oa.m2m.all()
self.assertEqual(repr(objects[0]), '<RelationA: id 2, field_base (CharField) "A1", fk (ForeignKey) RelationBase, field_a (CharField) "A2", m2m (ManyToManyField) 2>')
self.assertEqual(repr(objects[1]), '<RelationB: id 3, field_base (CharField) "B1", fk (ForeignKey) RelationA, field_b (CharField) "B2", m2m (ManyToManyField) 1>')
self.assertEqual(len(objects), 2)
def test_user_defined_manager(self):
self.create_model2abcd()
ModelWithMyManager.objects.create(field1='D1a', field4='D4a')
ModelWithMyManager.objects.create(field1='D1b', field4='D4b')
objects = ModelWithMyManager.objects.all() # MyManager should reverse the sorting of field1
self.assertEqual(repr(objects[0]), '<ModelWithMyManager: id 6, field1 (CharField) "D1b", field4 (CharField) "D4b">')
self.assertEqual(repr(objects[1]), '<ModelWithMyManager: id 5, field1 (CharField) "D1a", field4 (CharField) "D4a">')
self.assertEqual(len(objects), 2)
self.assertIs(type(ModelWithMyManager.objects), MyManager)
self.assertIs(type(ModelWithMyManager._default_manager), MyManager)
self.assertIs(type(ModelWithMyManager.base_objects), models.Manager)
def test_manager_inheritance(self):
# by choice of MRO, should be MyManager from MROBase1.
self.assertIs(type(MRODerived.objects), MyManager)
# check for correct default manager
self.assertIs(type(MROBase1._default_manager), MyManager)
# Django vanilla inheritance does not inherit MyManager as _default_manager here
self.assertIs(type(MROBase2._default_manager), MyManager)
def test_queryset_assignment(self):
# This is just a consistency check for now, testing standard Django behavior.
parent = PlainParentModelWithManager.objects.create()
child = PlainChildModelWithManager.objects.create(fk=parent)
self.assertIs(type(PlainParentModelWithManager._default_manager), models.Manager)
self.assertIs(type(PlainChildModelWithManager._default_manager), PlainMyManager)
self.assertIs(type(PlainChildModelWithManager.objects), PlainMyManager)
self.assertIs(type(PlainChildModelWithManager.objects.all()), PlainMyManagerQuerySet)
# A related set is created using the model's _default_manager, so does gain extra methods.
self.assertIs(type(parent.childmodel_set.my_queryset_foo()), PlainMyManagerQuerySet)
# For polymorphic models, the same should happen.
parent = ParentModelWithManager.objects.create()
child = ChildModelWithManager.objects.create(fk=parent)
self.assertIs(type(ParentModelWithManager._default_manager), PolymorphicManager)
self.assertIs(type(ChildModelWithManager._default_manager), MyManager)
self.assertIs(type(ChildModelWithManager.objects), MyManager)
self.assertIs(type(ChildModelWithManager.objects.my_queryset_foo()), MyManagerQuerySet)
# A related set is created using the model's _default_manager, so does gain extra methods.
self.assertIs(type(parent.childmodel_set.my_queryset_foo()), MyManagerQuerySet)
def test_proxy_models(self):
# prepare some data
for data in ('bleep bloop', 'I am a', 'computer'):
ProxyChild.objects.create(some_data=data)
# this caches ContentType queries so they don't interfere with our query counts later
list(ProxyBase.objects.all())
# one query per concrete class
with self.assertNumQueries(1):
items = list(ProxyBase.objects.all())
self.assertIsInstance(items[0], ProxyChild)
def test_content_types_for_proxy_models(self):
"""Checks if ContentType is capable of returning proxy models."""
from django.db.models import Model
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(ProxyChild, for_concrete_model=False)
self.assertEqual(ProxyChild, ct.model_class())
def test_proxy_model_inheritance(self):
"""
Polymorphic abilities should also work when the base model is a proxy object.
"""
# The managers should point to the proper objects.
# otherwise, the whole excersise is pointless.
self.assertEqual(ProxiedBase.objects.model, ProxiedBase)
self.assertEqual(ProxyModelBase.objects.model, ProxyModelBase)
self.assertEqual(ProxyModelA.objects.model, ProxyModelA)
self.assertEqual(ProxyModelB.objects.model, ProxyModelB)
# Create objects
ProxyModelA.objects.create(name="object1")
ProxyModelB.objects.create(name="object2", field2="bb")
# Getting single objects
object1 = ProxyModelBase.objects.get(name='object1')
object2 = ProxyModelBase.objects.get(name='object2')
self.assertEqual(repr(object1), '<ProxyModelA: id 1, name (CharField) "object1", field1 (CharField) "">')
self.assertEqual(repr(object2), '<ProxyModelB: id 2, name (CharField) "object2", field2 (CharField) "bb">')
self.assertIsInstance(object1, ProxyModelA)
self.assertIsInstance(object2, ProxyModelB)
# Same for lists
objects = list(ProxyModelBase.objects.all().order_by('name'))
self.assertEqual(repr(objects[0]), '<ProxyModelA: id 1, name (CharField) "object1", field1 (CharField) "">')
self.assertEqual(repr(objects[1]), '<ProxyModelB: id 2, name (CharField) "object2", field2 (CharField) "bb">')
self.assertIsInstance(objects[0], ProxyModelA)
self.assertIsInstance(objects[1], ProxyModelB)
def test_fix_getattribute(self):
### fixed issue in PolymorphicModel.__getattribute__: field name same as model name
o = ModelFieldNameTest.objects.create(modelfieldnametest='1')
self.assertEqual(repr(o), '<ModelFieldNameTest: id 1, modelfieldnametest (CharField)>')
# if subclass defined __init__ and accessed class members,
# __getattribute__ had a problem: "...has no attribute 'sub_and_superclass_dict'"
o = InitTestModelSubclass.objects.create()
self.assertEqual(o.bar, 'XYZ')
class RegressionTests(TestCase):
def test_for_query_result_incomplete_with_inheritance(self):
""" https://github.com/bconstantin/django_polymorphic/issues/15 """
top = Top()
top.save()
middle = Middle()
middle.save()
bottom = Bottom()
bottom.save()
expected_queryset = [top, middle, bottom]
self.assertQuerysetEqual(Top.objects.all(), [repr(r) for r in expected_queryset])
expected_queryset = [middle, bottom]
self.assertQuerysetEqual(Middle.objects.all(), [repr(r) for r in expected_queryset])
expected_queryset = [bottom]
self.assertQuerysetEqual(Bottom.objects.all(), [repr(r) for r in expected_queryset])

View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
####################################################################
import uuid
from django import forms
from django.db import models
from django.utils.encoding import smart_text
from django.utils import six
class UUIDVersionError(Exception):
pass
class UUIDField(six.with_metaclass(models.SubfieldBase, models.CharField)):
"""Encode and stores a Python uuid.UUID in a manner that is appropriate
for the given datatabase that we are using.
For sqlite3 or MySQL we save it as a 36-character string value
For PostgreSQL we save it as a uuid field
This class supports type 1, 2, 4, and 5 UUID's.
"""
_CREATE_COLUMN_TYPES = {
'postgresql_psycopg2': 'uuid',
'postgresql': 'uuid'
}
def __init__(self, verbose_name=None, name=None, auto=True, version=1, node=None, clock_seq=None, namespace=None, **kwargs):
"""Contruct a UUIDField.
@param verbose_name: Optional verbose name to use in place of what
Django would assign.
@param name: Override Django's name assignment
@param auto: If True, create a UUID value if one is not specified.
@param version: By default we create a version 1 UUID.
@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.
@param clock_seq: Used for version 1 UUID's. If not supplied a random 14-bit sequence number is chosen
@param namespace: Required for version 3 and version 5 UUID's.
@param name: Required for version4 and version 5 UUID's.
See Also:
- Python Library Reference, section 18.16 for more information.
- RFC 4122, "A Universally Unique IDentifier (UUID) URN Namespace"
If you want to use one of these as a primary key for a Django
model, do this::
id = UUIDField(primary_key=True)
This will currently I{not} work with Jython because PostgreSQL support
in Jython is not working for uuid column types.
"""
self.max_length = 36
kwargs['max_length'] = self.max_length
if auto:
kwargs['blank'] = True
kwargs.setdefault('editable', False)
self.auto = auto
self.version = version
if version == 1:
self.node, self.clock_seq = node, clock_seq
elif version == 3 or version == 5:
self.namespace, self.name = namespace, name
super(UUIDField, self).__init__(verbose_name=verbose_name,
name=name, **kwargs)
def create_uuid(self):
if not self.version or self.version == 4:
return uuid.uuid4()
elif self.version == 1:
return uuid.uuid1(self.node, self.clock_seq)
elif self.version == 2:
raise UUIDVersionError("UUID version 2 is not supported.")
elif self.version == 3:
return uuid.uuid3(self.namespace, self.name)
elif self.version == 5:
return uuid.uuid5(self.namespace, self.name)
else:
raise UUIDVersionError("UUID version %s is not valid." % self.version)
def db_type(self, connection):
from django.conf import settings
full_database_type = settings.DATABASES['default']['ENGINE']
database_type = full_database_type.split('.')[-1]
return UUIDField._CREATE_COLUMN_TYPES.get(database_type, "char(%s)" % self.max_length)
def to_python(self, value):
"""Return a uuid.UUID instance from the value returned by the database."""
#
# This is the proper way... But this doesn't work correctly when
# working with an inherited model
#
if not value:
return None
if isinstance(value, uuid.UUID):
return value
# attempt to parse a UUID
return uuid.UUID(smart_text(value))
#
# If I do the following (returning a String instead of a UUID
# instance), everything works.
#
#if not value:
# return None
#if isinstance(value, uuid.UUID):
# return smart_text(value)
#else:
# return value
def pre_save(self, model_instance, add):
if self.auto and add:
value = self.create_uuid()
setattr(model_instance, self.attname, value)
else:
value = super(UUIDField, self).pre_save(model_instance, add)
if self.auto and not value:
value = self.create_uuid()
setattr(model_instance, self.attname, value)
return value
def get_db_prep_value(self, value, connection, prepared):
"""Casts uuid.UUID values into the format expected by the back end for use in queries"""
if isinstance(value, uuid.UUID):
return smart_text(value)
return value
def value_to_string(self, obj):
val = self._get_val_from_obj(obj)
if val is None:
data = ''
else:
data = smart_text(val)
return data
def formfield(self, **kwargs):
defaults = {
'form_class': forms.CharField,
'max_length': self.max_length
}
defaults.update(kwargs)
return super(UUIDField, self).formfield(**defaults)

View File

@ -62,6 +62,7 @@ Django-1.5.5.tar.gz
#django-celery-3.1.1.tar.gz
#django-extensions-1.2.5.tar.gz
#django-jsonfield-0.9.12.tar.gz
#django_polymorphic-0.5.3.tar.gz
#django-split-settings-0.1.1.tar.gz
#django-taggit-0.11.2.tar.gz
#djangorestframework-2.3.10.tar.gz
@ -93,3 +94,4 @@ ipython-1.1.0.tar.gz
# - psycopg2 (via "yum install python-psycopg2")
# - python-ldap (via "yum install python-ldap")
# - readline-6.2.4.1.tar.gz (for the ipython shell)
# - python-zmq (for using the job callback receiver)

Binary file not shown.

View File

@ -10,6 +10,7 @@ Django>=1.4
#django-celery
#django-extensions
#django-jsonfield
#django-polymorphic
#django-split-settings
#django-taggit
#djangorestframework>=2.3.0,<2.4.0
@ -25,3 +26,4 @@ Django>=1.4
# - ansible (via yum, pip or source checkout)
# - psycopg2 (via "yum install python-psycopg2")
# - python-ldap (via "yum install python-ldap")
# - python-zmq (for using the job callback receiver)

View File

@ -60,6 +60,7 @@ Django-1.5.5.tar.gz
#django-celery-3.1.1.tar.gz
#django-extensions-1.2.5.tar.gz
#django-jsonfield-0.9.12.tar.gz
#django_polymorphic-0.5.3.tar.gz
#django-split-settings-0.1.1.tar.gz
#django-taggit-0.11.2.tar.gz
#djangorestframework-2.3.10.tar.gz
@ -73,3 +74,4 @@ Django-1.5.5.tar.gz
# - ansible (via yum, pip or source checkout)
# - psycopg2 (via "yum install python-psycopg2")
# - python-ldap (via "yum install python-ldap")
# - python-zmq (for using the job callback receiver)