From e957de2016e594de45ac5b39a2d3f2431c64be82 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 27 Feb 2014 17:12:32 -0500 Subject: [PATCH] AC-1040 Add django-polymorphic as prerequisite for moving toward unified jobs. --- awx/lib/site-packages/README | 1 + awx/lib/site-packages/polymorphic/__init__.py | 46 + .../site-packages/polymorphic/__version__.py | 14 + awx/lib/site-packages/polymorphic/admin.py | 507 +++++++++++ awx/lib/site-packages/polymorphic/base.py | 243 ++++++ awx/lib/site-packages/polymorphic/manager.py | 46 + awx/lib/site-packages/polymorphic/models.py | 10 + .../polymorphic/polymorphic_model.py | 201 +++++ awx/lib/site-packages/polymorphic/query.py | 307 +++++++ .../polymorphic/query_translate.py | 248 ++++++ .../site-packages/polymorphic/showfields.py | 164 ++++ .../admin/polymorphic/add_type_form.html | 11 + .../admin/polymorphic/change_form.html | 6 + .../polymorphic/delete_confirmation.html | 6 + .../polymorphic/templatetags/__init__.py | 0 .../templatetags/polymorphic_admin_tags.py | 53 ++ awx/lib/site-packages/polymorphic/tests.py | 791 ++++++++++++++++++ .../polymorphic/tools_for_tests.py | 146 ++++ requirements/dev_local.txt | 2 + requirements/django_polymorphic-0.5.3.tar.gz | Bin 0 -> 33698 bytes requirements/prod.txt | 2 + requirements/prod_local.txt | 2 + 22 files changed, 2806 insertions(+) create mode 100644 awx/lib/site-packages/polymorphic/__init__.py create mode 100644 awx/lib/site-packages/polymorphic/__version__.py create mode 100644 awx/lib/site-packages/polymorphic/admin.py create mode 100644 awx/lib/site-packages/polymorphic/base.py create mode 100644 awx/lib/site-packages/polymorphic/manager.py create mode 100644 awx/lib/site-packages/polymorphic/models.py create mode 100644 awx/lib/site-packages/polymorphic/polymorphic_model.py create mode 100644 awx/lib/site-packages/polymorphic/query.py create mode 100644 awx/lib/site-packages/polymorphic/query_translate.py create mode 100644 awx/lib/site-packages/polymorphic/showfields.py create mode 100644 awx/lib/site-packages/polymorphic/templates/admin/polymorphic/add_type_form.html create mode 100644 awx/lib/site-packages/polymorphic/templates/admin/polymorphic/change_form.html create mode 100644 awx/lib/site-packages/polymorphic/templates/admin/polymorphic/delete_confirmation.html create mode 100644 awx/lib/site-packages/polymorphic/templatetags/__init__.py create mode 100644 awx/lib/site-packages/polymorphic/templatetags/polymorphic_admin_tags.py create mode 100644 awx/lib/site-packages/polymorphic/tests.py create mode 100644 awx/lib/site-packages/polymorphic/tools_for_tests.py create mode 100644 requirements/django_polymorphic-0.5.3.tar.gz diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index 31524309de..254e363882 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -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/*) diff --git a/awx/lib/site-packages/polymorphic/__init__.py b/awx/lib/site-packages/polymorphic/__init__.py new file mode 100644 index 0000000000..c598b84ef2 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/__init__.py @@ -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 + diff --git a/awx/lib/site-packages/polymorphic/__version__.py b/awx/lib/site-packages/polymorphic/__version__.py new file mode 100644 index 0000000000..202b7e2573 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/__version__.py @@ -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 + 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" diff --git a/awx/lib/site-packages/polymorphic/admin.py b/awx/lib/site-packages/polymorphic/admin.py new file mode 100644 index 0000000000..db1c8b29e1 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/admin.py @@ -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.+)$', 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 diff --git a/awx/lib/site-packages/polymorphic/base.py b/awx/lib/site-packages/polymorphic/base.py new file mode 100644 index 0000000000..95a124e6b3 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/base.py @@ -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) diff --git a/awx/lib/site-packages/polymorphic/manager.py b/awx/lib/site-packages/polymorphic/manager.py new file mode 100644 index 0000000000..3e0e6d8143 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/manager.py @@ -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__) diff --git a/awx/lib/site-packages/polymorphic/models.py b/awx/lib/site-packages/polymorphic/models.py new file mode 100644 index 0000000000..69652f9eed --- /dev/null +++ b/awx/lib/site-packages/polymorphic/models.py @@ -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, ... + +""" diff --git a/awx/lib/site-packages/polymorphic/polymorphic_model.py b/awx/lib/site-packages/polymorphic/polymorphic_model.py new file mode 100644 index 0000000000..5b20751b1e --- /dev/null +++ b/awx/lib/site-packages/polymorphic/polymorphic_model.py @@ -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 diff --git a/awx/lib/site-packages/polymorphic/query.py b/awx/lib/site-packages/polymorphic/query.py new file mode 100644 index 0000000000..7f6d70dbbf --- /dev/null +++ b/awx/lib/site-packages/polymorphic/query.py @@ -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 diff --git a/awx/lib/site-packages/polymorphic/query_translate.py b/awx/lib/site-packages/polymorphic/query_translate.py new file mode 100644 index 0000000000..b6ee8cc609 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/query_translate.py @@ -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 diff --git a/awx/lib/site-packages/polymorphic/showfields.py b/awx/lib/site-packages/polymorphic/showfields.py new file mode 100644 index 0000000000..1a279d6392 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/showfields.py @@ -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 diff --git a/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/add_type_form.html b/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/add_type_form.html new file mode 100644 index 0000000000..20cc8ab1ef --- /dev/null +++ b/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/add_type_form.html @@ -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 %} diff --git a/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/change_form.html b/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/change_form.html new file mode 100644 index 0000000000..4224c12631 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/change_form.html @@ -0,0 +1,6 @@ +{% extends "admin/change_form.html" %} +{% load polymorphic_admin_tags %} + +{% block breadcrumbs %} + {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} +{% endblock %} diff --git a/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/delete_confirmation.html b/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/delete_confirmation.html new file mode 100644 index 0000000000..ca0073dac4 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/templates/admin/polymorphic/delete_confirmation.html @@ -0,0 +1,6 @@ +{% extends "admin/delete_confirmation.html" %} +{% load polymorphic_admin_tags %} + +{% block breadcrumbs %} + {% breadcrumb_scope base_opts %}{{ block.super }}{% endbreadcrumb_scope %} +{% endblock %} diff --git a/awx/lib/site-packages/polymorphic/templatetags/__init__.py b/awx/lib/site-packages/polymorphic/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx/lib/site-packages/polymorphic/templatetags/polymorphic_admin_tags.py b/awx/lib/site-packages/polymorphic/templatetags/polymorphic_admin_tags.py new file mode 100644 index 0000000000..8fc5dca394 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/templatetags/polymorphic_admin_tags.py @@ -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) diff --git a/awx/lib/site-packages/polymorphic/tests.py b/awx/lib/site-packages/polymorphic/tests.py new file mode 100644 index 0000000000..376f0f3a04 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/tests.py @@ -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 = ''' +[ , + , + , + , + , + , + , + ]''' + 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=''' +[ , + , + , + , + , + , + , + ]''' + + # PostgreSQL returns this order + expected2=''' +[ , + , + , + , + , + , + , + ]''' + + 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 = """[ , + , + ]""" + 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]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(repr(objects[3]), '') + + + def test_manual_get_real_instance(self): + self.create_model2abcd() + + o = Model2A.objects.non_polymorphic().get(field1='C1') + self.assertEqual(repr(o.get_real_instance()), '') + + + def test_non_polymorphic(self): + self.create_model2abcd() + + objects = list(Model2A.objects.all().non_polymorphic()) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(repr(objects[3]), '') + + + def test_get_real_instances(self): + self.create_model2abcd() + qs = Model2A.objects.all().non_polymorphic() + + # from queryset + objects = qs.get_real_instances() + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(repr(objects[3]), '') + + # from a manual list + objects = Model2A.objects.get_real_instances(list(qs)) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(repr(objects[3]), '') + + + 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]), '') + self.assertEqual(repr(objects[1]), '') + + + 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), " ") + self.assertEqual(show_base_manager(PlainB), " ") + self.assertEqual(show_base_manager(PlainC), " ") + + self.assertEqual(show_base_manager(Model2A), " ") + self.assertEqual(show_base_manager(Model2B), " ") + self.assertEqual(show_base_manager(Model2C), " ") + + self.assertEqual(show_base_manager(One2OneRelatingModel), " ") + self.assertEqual(show_base_manager(One2OneRelatingModelDerived), " ") + + + def test_foreignkey_field(self): + self.create_model2abcd() + + object2a = Model2A.base_objects.get(field1='C1') + self.assertEqual(repr(object2a.model2b), '') + + object2b = Model2B.base_objects.get(field1='C1') + self.assertEqual(repr(object2b.model2c), '') + + + 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), '') + + c = One2OneRelatingModelDerived.objects.get(field1='f1') + self.assertEqual(repr(c.one2one), '') + self.assertEqual(repr(a.one2onerelatingmodel), '') + + + 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()), '[ ]') + + # Model 2 + o = ModelShow2.objects.create(field1='abc') + o.m2m.add(o) + o.save() + self.assertEqual(repr(ModelShow2.objects.all()), '[ ]') + + # Model 3 + o=ModelShow3.objects.create(field1='abc') + o.m2m.add(o) + o.save() + self.assertEqual(repr(ModelShow3.objects.all()), '[ ]') + self.assertEqual(repr(ModelShow1.objects.all().annotate(Count('m2m'))), '[ ]') + self.assertEqual(repr(ModelShow2.objects.all().annotate(Count('m2m'))), '[ ]') + self.assertEqual(repr(ModelShow3.objects.all().annotate(Count('m2m'))), '[ ]') + + # no pretty printing + ModelShow1_plain.objects.create(field1='abc') + ModelShow2_plain.objects.create(field1='abc', field2='def') + self.assertEqual(repr(ModelShow1_plain.objects.all()), '[, ]') + + + def test_extra_method(self): + self.create_model2abcd() + + objects = list(Model2A.objects.extra(where=['id IN (2, 3)'])) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + + objects = Model2A.objects.extra(select={"select_test": "field1 = 'A1'"}, where=["field1 = 'A1' OR field1 = 'B1'"], order_by=['-id']) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + 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]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + else: + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + 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]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(len(objects), 3) + + objects = Model2A.objects.filter(instance_of=Model2B) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(len(objects), 3) + + objects = Model2A.objects.filter(Q(instance_of=Model2B)) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(len(objects), 3) + + objects = Model2A.objects.not_instance_of(Model2B) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(len(objects), 1) + + + def test_polymorphic___filter(self): + self.create_model2abcd() + + objects = Model2A.objects.filter(Q( Model2B___field2='B2') | Q( Model2C___field3='C3')) + self.assertEqual(len(objects), 2) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + + + def test_delete(self): + self.create_model2abcd() + + oa = Model2A.objects.get(id=2) + self.assertEqual(repr(oa), '') + self.assertEqual(Model2A.objects.count(), 4) + + oa.delete() + objects = Model2A.objects.all() + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(len(objects), 3) + + + def test_combine_querysets(self): + ModelX.objects.create(field_x='x') + ModelY.objects.create(field_y='y') + + qs = Base.objects.instance_of(ModelX) | Base.objects.instance_of(ModelY) + self.assertEqual(repr(qs[0]), '') + self.assertEqual(repr(qs[1]), '') + self.assertEqual(len(qs), 2) + + + def test_multiple_inheritance(self): + # multiple inheritance, subclassing third party models (mix PolymorphicModel with models.Model) + + 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]), '') + self.assertEqual(repr(qs[1]), '') + 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]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(repr(objects[2]), '') + self.assertEqual(repr(objects[3]), '') + self.assertEqual(len(objects), 4) + + oa = RelationBase.objects.get(id=2) + self.assertEqual(repr(oa.fk), '') + + objects = oa.relationbase_set.all() + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(len(objects), 2) + + ob = RelationBase.objects.get(id=3) + self.assertEqual(repr(ob.fk), '') + + oa = RelationA.objects.get() + objects = oa.m2m.all() + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + self.assertEqual(len(objects), 2) + + + def test_user_defined_manager(self): + self.create_model2abcd() + ModelWithMyManager.objects.create(field1='D1a', field4='D4a') + ModelWithMyManager.objects.create(field1='D1b', field4='D4b') + + objects = ModelWithMyManager.objects.all() # MyManager should reverse the sorting of field1 + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + 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), '') + self.assertEqual(repr(object2), '') + self.assertIsInstance(object1, ProxyModelA) + self.assertIsInstance(object2, ProxyModelB) + + # Same for lists + objects = list(ProxyModelBase.objects.all().order_by('name')) + self.assertEqual(repr(objects[0]), '') + self.assertEqual(repr(objects[1]), '') + 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), '') + + # 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]) + diff --git a/awx/lib/site-packages/polymorphic/tools_for_tests.py b/awx/lib/site-packages/polymorphic/tools_for_tests.py new file mode 100644 index 0000000000..b7c017dcb2 --- /dev/null +++ b/awx/lib/site-packages/polymorphic/tools_for_tests.py @@ -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) diff --git a/requirements/dev_local.txt b/requirements/dev_local.txt index 95621ec79c..f40ce27076 100644 --- a/requirements/dev_local.txt +++ b/requirements/dev_local.txt @@ -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) diff --git a/requirements/django_polymorphic-0.5.3.tar.gz b/requirements/django_polymorphic-0.5.3.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..0050d4f75595707117ee93243eeb3f7fd64f3dc9 GIT binary patch literal 33698 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

>(X#)9<>N#a0|>B^UfsOB)4+*Kj?QL z#q4o1l|agNZI^-@;}Xxp`Bqa(lc)liieyT~fmDfX)HmgvXpSmA<4Fs;CW*RAsY#9n zR)0;Ha03PawW0>MH>7XH_GBv`G-1Q$KsJ&<{~^E>LjsL9Yo5_P^*(SyO`(Dng8)S% z@+PKO>v%iv0d#u7ivDhoNZ&@8u~3|!RubcmF-?F(4snnOf}TkGAcin8OOhWvUl;|? z*V*z?a#cH`+R~jjwgw9e&yh&T?1%vdoe-NyY>G-{3e*@YovFA|lb2`I%nRd8^~%g3 zBBJxC#T}|HLz%YR&alY~n_%SAOuCxTD=M^efG#tvVT__8i-P#t;$5K&2)L9RkGD*X zYYc5JOALm2Q2411nR64F4B<%Wu0``3I}{b`iks*|!NES&JO(F3#w;A-j2L~<4Sqk6 zl8IH}ysoSB!?d>e0nA0A$!>r2XnR*PFRM{)RlDaM;`iTW@I1Mj)_EzlP|)yb{=uYO z(lsxb6?u>IlHjLmaAE}Yng?toB{_g)h8!6)>|+=#%6IA9SfAvK&D6ifZ5GTX0Co$Z zE1iIpFBV)8jQrrY+M5NCNn9Hq(YEmPk#C(5>0z-3M!c!{4W2bKP?eJkl=VY0eaaA^ zH5{VxrG}19nz|9Odjbugd$Q?7t=e05TS5Lwe!i5HptoZvnV5s|Dpsi4L!$TgLBz-jVVe2rdkJuiWG7MY?z3A zU&Tr#{3nzk5;-IkN43mLcQanHF?cHh?aC^x7H;kz!{G)~r%@UR;mW9v6w~#h8KPp3 zzB1?B2^htWPJ1P1{7WlltE${TgYUeTEIFG-tye zrEAVX4B7WXa8Jv}QCK|HS_Rfi2G%f5^T}uAcB`eJW9O|{bG`DIgxf7#Jz^+iN;L{` z>Cv<4o4n36uQjc*v%5kiJgB)gT$f#TkcL+rNn3i6j$=ySrk;3m+y!AHby7At;sqvE zHKRPOhq#9z?0QkmhhFvT*4?+*d!&teV?G_7-^{y&=U0>a7G5hKYhZ#(G-;8tp7msQ zmk&Y6gkn}`gNeP}^FriAH~bCM+e-@l0&YP*o8HI-Iab9Bq4(#IVoCyQxz1qJzD(3- zk{%Ym(RR>d1--4VuSq_l76s?Y#vB%9Eu%Jy@uf@&C>|i|=5nGze0$_O%TB6OBWIh# z%7Lr{C=i;T66gohcs82E0kS6hD0n2ZH~sU7(pHps5kZjcbbEpLqMilR98q}85>Ll~ zbbP%Uh5gyM>}gTJa-h)}H40(2Q95ZA)WLz&Xgxs*Y@)I_n^NKwSPa8`AJs3SvVxOV zg%A?3-;u7CGI90=h0eXY!P7(jKTthk_;2K+$@PbY2KL-~G$Rv30SzdzcfHLLe%dZ! zE#RWAE3S@Ikm%<;&KDVF1Cc6gimROg$+w(h`@C5WwWLViMm*gz51ad7>+Jc z;koSBeY1UTD|$+xAY}!VqKHt+O2VM?x9r{$J)2Io6SRM#e}8W8V;B7MvcLtv2Lb;6 zGkq-kaj|jwg1h}K`&-s=oR;B7z69K1;Lueu#lYF{WiilPhL~VrGc4V}t8jOrEI!uN zOuYAZiJp9ITMS5}IE*ynNLrg;un|B?w3*7QEp*CAutn8!zKd$a12Sg<-yH}m4W@0L zrNYptN;-5(ZDez@0-h1NmI>Txne_~4RhATRGaH7|4)ijOlQEvQ4!aTyis#xw9Wh-a zGg2QP5cc^9wt};lPhaj6LC1V`bCuYI=QoX0Nl+{@scTA0WiL&wq}9><*LS2nUJRyTyxA}Uc^PpF;=G4K*KFzcZ!}_Ux5MYp*)@>Kv2AJ3+)J`%ei{lMBNtd3;Z#es8Cc8~nP3g(3m3PIkG^{hJy%gLY%9m^l<|wfGx&;Kms3}JUzmC&V`1obF);dRu z38D-N^L5aP=pzF@_+i@wMb7#r?rbfjt^?+|zlO=%T(C_>Oxp!O>g(^cR%1&oiJl^H z^2|H(82A<|QYWG`%-SY`+p#cs5Ew$Y_Iv%i# zT&)ejR=60dXIgmfOuCz$fWr@p(4MP!{B9qopK%Q*$|;*G7i?V(W`q@=3sq$*YMEN$ zPASGE*DmyBCuta-7YqlBJ7_7!>Sn-ADBZf@DtAxjO~C3S7hZdI#H<_8R`#2sJgDs8 zr7YE6$>JWfA1<3|ufjEl>Z*koI2pbOTsw(rh1wV}TOBb=xN^wSQDVAYgMt!wC1^=G zfcfyR6QTc>aqWCeX03A#{YXH+jN~F)+U$yFEc{?$Byq@?PgNHyxFrL%t^Ow4AtjNpIJ49 zTUIG&L0?EspcoS84M9>QnYEg}ruIbhW7o8@`qO+tp((=a8Wk(!cVQP7jYm+RQbkqb zDyT+Jzz-gNw`+YTE#qG6Dy&R+Y~4o$xUUi5zC?igCYDlk1x>%S)~=2(5m;K+ZSwN) zC6@+Zxmf@{^V1>Dw0*8$8?vtF3G*Q&Mi8A80>+M!CrV9|UIew7w3pgSr0?vvRt8CW z_*e&jINj72cR&9=KWoMR$VcgxAwbLHf42Af{XHlCXLon+!Cm~%Zz=xAU@<;>*v}$M z(F8r5V#Fdmt}@o=Ixg^0D%XhhF*Qc3Lv1`^7*dn(GkLm^7(U$(ljqmvf;m~B@K&FEcec8P{H0?p7eHSn@f@xE5cvG_G_Bba~qsvi=r-T zCgw##>exS!yH^2Rvvn>*$GJ!rC9Snk#@mH;u$b;cpjVZJ$3HekiL|8Q5&{qV^g>omr>H2QTa=lTlYn zw6BW%;|)nVi;SvENW%lNlgf8pjw{MU0DhoOXqL=IB8o?hrgY*gW~La99@hiXR7U%+ zTG~QQmAWfTa4S(=o159+0jS;$TuZr3oLuq!rJ0RTE2}6fdPEUqAH~hZNF6D>{g9|^ z04N;G3KV+Ka5(Hr7f3X($`leb;EhTgW{FcPBq`f`JnVGWOQcc58t-f_R!9w!@+sG7 z)RkR~9muD0*nni6FH66AhslZzCt!`I8#K`K;iwT(aK}ymtMdQM_1rcCzzY6ZV66 zAs)y~kn9jdj!S|q7mq^5<(K36ycEg{ms9rz>CrMNx~Ecz1z6$NjH4oNq=7mf!!K z?MJ&0SpI*|fADYz@Bf3nhY#=W|3CWvpC<8an%IJHaBMi8X~zxr{U}?5jelA#zdivi z7)qf)Hs(b^Ntn5O1t$S)wO$r7wbT#EXuq|w+Cg+MTIs%?4z1Qn;C$8SFs<*<19EFn%sxWi4 zlcY80B9o={nku7XOS)}Pa0wS9mvSH4`!n^IE8M5jsi=b&no=#7avYmk`;YX#F-Si-xZ_8a@$z(sxw zZxAS#C@i&ObGK=YQkB7DVf_)22|)#Sicn#h1LcAl96PPG9tD9flU1lFW0i#jJ|+f9 zr)fb+9(}aOvGknAuSMt{IuXVkxMMgTaw0|P)!3uxoqboa@gppfays=`Fr}TbDnn?lEt(_t)p-|7g1$}l}m+y#|>nPxNx!Apbef64`Y()+qnYNOs-+Xc`XQW+?tm#0Sd3uKa{(SpJCxc{O)@GA5#5nyz*wT3e#X=oupxqXh=`6^qw7~Nz-*1lk8bYQNLV2CVrINQpqwjsf5XD_dn#T8AyVuJE7!zexQ$`DaO}=}MN{bi<W&05Gq7{}wQ#57ClXU>|JcsnDihLa#w@iYm9m`Ez#?Tzq$ns0?g|~ z*zA&zX$dEUV9IjJN|&IG^+HD1XWRm}By!!PD zopKNpTd)x69{2Ug_7F z<0mb0X>y(_={SFtU==W+->7$?kEE-#l)ap=KH=4WEt9}`+;OfwD!xDKRr5ZyBSQg_e2_;!PH%CO5*ISmg79{I4u|wl=%PRxjn1 z>kdAF#V}j0^U7;t-PCM8s$%o|n~!NJlvI#2p|@&1l;pvE#d-?HZaA78sWycBUYTiB zr6$4hv#-&^1_yl{z(;j!D`?d!UL@rZBNw#(6F*SMBWb&#GodGebXEiudb4NlD4n2CiH?6ysl-kw3QWWDFOyrnUt-!U4L2!yuPXp zK*aJjQb^YmGuolaW}YW&oGvcv%_rEjCvrn1ntA+Kr#ZkB$LdFr4H&f}S8c5g8$XsA zl0B7tmTg}a$_v-OMjR(EN~Uu&P{vFwxnCD*MR>2s6euY4`s@OoEG@!{ z{G{tYnK<;Km8;S0P@SlG%nU!IxHwOLo&IGWsz$&hR7SKC!c%q&1z{Q-SW2XJLMz1D~_lRAe8|_ zBB1E#15|CTtkLheN3$g_<55Q(f zNlVm*!@O;nZ)&t9K7av)Dbth+r8ha1ql3%gE9l|Pu;ZA=l&>j-ci=rAF43@Zu< zS*LyY-Rl>B9-bcm-y=^tRxYv$#K`n->qq_j{b;ALpwK;YOiWqM2H}a$tpr0nq^3s0 z0Z!9;Wm-K29!f!Ji0_2}Aey{X-&U)oEU&ZtvPdt0HJz4Rk@=NL{>XuZ-L50KP71Sz`BeHiGeqmZFtG-w@C($7S``#@~hCv5F*cUIbmlc1-cfeSt&HXMdH{jKV^)g z9k16CMs#01W24rz*=CFFNHe5n5h^Vq^~mh?rE6q0F6AUGvB|_r?%>rMX~TPSY?xze z1>^?Fdi6$>%9lHEE@;lkO5Oydvz43a38SEl>clb;2!)c;gS4I7uzDN3#@Z&#JJA5J zMqqkUTWO$VAQQW89V<-p7?3r-f`{nB@AW9OnyE;IHDFl!fRcNgMyS%M$d$j4V!O?DX=#QSV21zb(D2M zhlJcwa1hof!xw2`=$X2P-N~+f2Hl3MxDsYc98-<9dW!;v#;yj=eQ|0BZoFw3FZi3< zud^w4&vLxH7>eb~n85@y)-1glKO|-pE~SkXUbU(w7ZCP56NT?m1AEzaM^JQiL{Zx? zz;DKfT2ZaETIpD!idmW>u*kblU9l3^#+pMVb+Zq}i9nuAx~kCeNzl=&=$lz`NYOaMhdy1%F&;w+s`V>O9| zlbBdgK={onQNTXHE~ARPL+2aAP;L4vxd{I|IWKZlSPtXM%YtC6v8g9E>w^=mPIh6F zbOzTA|KR5P!iGjX%wLB?-f2+Yz%~EKyrGb|xJD}85*idf)1(1AP?QDF5 zOs2teo0QaDyH9dTjSC(}NDgG5xjjy1B zS~Hufk>fK$R?cbpAP`TR<<_u*rx&gzm;u1L^@GQOU=L9kxVZSGaNU??TX_Hqy8aWm zhi1fa64cdYLFX_1XdCkt#ycGpuK@NKm@>VNZ%Q%09S6k9;WGMfBX@(YlqMNnW>-1& zBC<{7XAxsA9>7eiLKx}}Qt5$7s&APY`CbrGo!Dzk)u^HrC>=xr;);>_FA3Vl65BQM zB14Te5mNESGz4NOZ@mdSZ6ln0IYlQjQ1cd)rpRq8!h8Xie&2|0V-j7$CMt}OhzK4EG@JTj95X=={hqb z{igQ&{yXc$*z1LMCU$k7LIw=Tz^Z^@&#Ls|##zPahF)Y!!Vbi&?!zkmePz0%(sK53 zef-U#LtWHQ6z0IF1!J|eO7j4}#n=W8YY@feB@IvoMEp1`T#VA9=C=`q1ZU62x?F>V z-id34p|(fhJn@XEmD5r1T_RGdilRYWG@G73wteH|nklmO%H_YvJ!#9?uSq&}eMC`L zk2qXhdoA(>ZwnzQ6{Yaf8tR-(hUr9C^bi%0L?ysYB_T0n^01PpRuRMoUE`vr;M1~yFvWkyLJ|j#ZSeY`$g6g8c4NuxLYj0%A zs}GYGO>pl+WlMd-UUGQY=Yu!+-Bx}f*1QGa(L#ya6QE$2ib!O$HnGKsT}NSpI-PWF zNc^+x+4XlAR{HY&!J0#qC?|h-6LI8SBK!dmv>jw<+bgBWB6{g$2M%yXfEcd7m+e^h< zcPi^_WQiGL814D%DY2cZkX?&{;>AcXGHh-9e(U=^J}kw3Qg2cng79Jv`AF(kt7+f~G2tCq{@}iSg+a zDs8Z323yk$F7p5*GcmgZvjcQ58WIT8cbo3eHe1;PnjsJ}yuloPwg38x@%XlQhg=eN z;wji>`pVa*@CH{nVKUGY38P3KUk@K@?AutNPgPKC+W2588CkYkLLynFQt6D#D-Oh$ zHfb?#KoSL-AeW4|V2f@>f8z!xmjUem@P$A3mlu2 zEfgQU$i~woE-2gK^;J$u>_v?2Xi72bSvRta9bvIo+bAFAxCurJqV#3r^r=-S8r>NA zL5bO@MaZjEA5MvKk?>LUHRXG0@}dkH)<&J>%!qKpfh=Ysa?p^oxG>E89hZQ)Ofgt~ zQLZjMZZd%)h*IQYHEJYC50Pc1yQNr!vI@X@F;^9g=Y}zG{!XTOH`cyvSQE)GF-j84 zgcroHx%k_b?lEk$H3cs^ubJ&tde0U*RTXv?vol2u&h0XbEFh85mn;EN;Vn?sE(xv} z#$2O~=YVUYN>vj83i*^@G&a>>m|(akrnN*>GU}j(wnqw!r4ef@upSY>V%of9j}HOi z@8^K$AQKbeBz>w%Y>+EQ(`4+W6fs0)vSo>j-tUkc z;em)lkRQWpDYl>OlvPfK{`Y0Qq_;6U%)fL;kwOlwg|y7OUV0qbv)ejo;!c@%Ut@f!0FAU&{o2QD9MmfxEV46 z^VfroIpimVPHhbv!l%~15kiu()5P}8X;~QqwpfK&S2o$s;G~ssbU;#{q~=l*>^Wpl!aUp6Lhc zT}=sM`p{w}el;+A7hC_Z9o8gF^ZjI$jB=u{coh{!?9r+fwDK!g@^Q` ztZ<*9!@-SZyb<-ZQ(f&(8UWQ$l!_y@UEG2Yt#Z&G4?<>-gWFH(HiZ(E$D#BaGF6?b z5@ssNQN7L;yp|ZhJY;r2&N3_Qq}oZmTW6vK{Tb-UPRGpWGw{_gELVJx7;itgA$-hj zvs+q8R^GOtq23++(@lF(T*WxImSsrsv9{kgl@G=-z4%4DLcpx!$*BQotLmls`Y{WY zL?S5q9rH?H<_U#THR9m2gYWyns+L;6trEud8h@u%K0l^qTXDW1%}o=sw(qY(F1rf4 zz?bnAJX}dOvD5UKOHK(B;EpEYx<}<c;xjS!oS)A zS|fvNz`$}_u%?H0?^=zw&5ER6=f16R;Tn;nRcGzx7WNw|x_YpO9mqKe=cRW{qXPX5yXQX7fS~gx!1A1(-rw+#=SH1cTr0gr%n8oESbKo{@07Z$)6P4a_w_a%*5TXVY2Y-$A8@%y(Ow1w801>cwJe zl5!z)u&QLDxQWxN{Q6r&)hLQoHW9^_6MEE=;H@+(`RSRJb|x2${Ouk-GaKJ+v7AmR zw>Y-3Sd=$YpoLL6TV0*esQ+Dp|KCFXefi~ zaQiO)_cv7kPeqI=m8=W|_$|c&|H?V-RWPXv8tw%|?$OupMP40YhQfwmV<#z$mGeZb zp5ribJ$#X1uE@_DP3Sq`);*DQN+$?W0lI8nP@xSXrecfKPu~j%96NF_YKer$l%OjP z-lZ8iK5+7{3#`|sYV2V$O{_&WnQ-36B7x3!93myL#W_o!Q(G6PG#V!ui`jYNGRd%h zq!x~o>Tx>aETOedlUy(S<^2uhFDwVnZ*oR3vXETZi=py_fo?-}qQ67MD zcZ+4j!sI9?G5vYxITxAhu@{^?KKh!XCpa2Z*iO*d%ECP5plqX9aGVKy5#?&uI|@V+ zWH^YVlFp!dzr0rp5wVt{(%JEx#GbIjUds&q*f)ovZ%$bIvM(I`5ScJbqd4C!9Xt^# z#O;c}S8Gmr#AfKCM4ftwl#q3)Xo?D)=wTo<9xtykLaR~ORrW6`CCK%ucM0?**_B#W zfKc*rj!K5%nzx{a`p#V&buNl`WxaVBr{jOVtf zEWVfXz1coLCvjZ-**BEf5mQlMA+Z<#UWPUF@xTBOpZ zyTG64_^1IX7`~VGs(mAYdy)(@tQ;uB6=9-4>QRQX8#xFzF~y}ogiVkFb9|LfCq<%5 zoqkX413PfZ(5e8{2(d*Zdv+a2(&ud1Ad*Pdl`e3h`F;&}%zFN+TihNEbvb<3XGfDm`;|N{-b$q4lAB!8#G@Bm59Rn&TQUKKaS`hn?Y)xACtBm0Sa-4heQR^p zz{?n{WDAU^<5sM}v5pI+Zap*8CSG7cQ{mD1e#^2c2rq8QOt^J49EM^|W+j72O6Wj@J6%D*FZXu_Si&QOMWgn=G_xG-rLbgLlMkZOxa zq)}?uV!-7Q9SC~5CNqk`VRGRU@=xcB4=31&OVYC|EmeYrUXY|jpfK15Njn#N^23c{ zk2IAN3s<}wub&@?T$4Qfof6S`=&iSnk7LJ}zf&v5G8FkOWkno9#qUE6t(M}6 zoMD|D2AdDvb8|beW4@lyifY!g*CwK40l=wKQDvfBaOwCI9yd%QAPk}z-u7DTYF7*e ztOA-#bzvV7|D|fo%ZL?s%9h{<7;dWro}2G}*L;^kF{FvuD_*8`rf8-qI+9n!Ox38& zCnhHpFepo_OhFz*r9fv34o&&03`aNojEXya6kSI9R<(Oogo(3wi>Tu^XXBQ!|GRCQ z4hbO?-bInEdnht=^S@)+EwTxRLC6S9=oU=q2;_-rhHQbD$ntL2f;Zt5y9t%a9GDYS zf|=LOII3tK7ZpDjgv;HQrQDy_Zi$iG&Bz>JOzFuM@Fi?(6w5+n0@=Ad7gi28)5S5G zUkVRKoi_!dneyR?O_ zyrpId)u3t&BTFcso4C53GviUQ@H;Xtu;nkyE+NGQy3qcfD!$v91bHkD<{QQw^{elYSbMnhAg>iy;bscLfR^AZItAs1xVy%sM zTycK2I1ssL1C>R37%*|5rG?t%A59#Uj<+{tpQath`i!Gw*<1DPyxc9J2S`ShUOU5j*`L!D2#-L8?m>2s4oD}4p$3c$c7v{ zVC&htJ^}YI&N%$CGC{3jCkhj(;2fssQcB51vBr2dN-r14z>bpYIx%WX7-1s{ZCF>$ z^q_1RmEh-~w^``2-u=E)`T2@#{BZ=pVPQp6`pBuUmlb#v>LR*d8 zf=4wAhYiI!rtCB06{MpZWu2qEtR!0k>TfaX_n4a%MIT1MRKtp9>HnZ38dag1bbm>H zXTpP}IlG&4bo&s58?wJ~X@n_P*XcMB8Te(qhf_xbXaNx#pW4QuSjv~tUYtm7ZZ|&n zt9lxUt+D=?5(lCfLFk$okmx>DP~qV5FWb^Q&^RQpTixs@aLJ z>TpO)Fo}tOZ2JjI@Ih*Ht2W7lDk9o*z}Z_7J?8?nK3-Eh@mr(Aw%engk^a^>$elaj zONTB|v24mdu6nyHzv@}%M$}hSw8P*J?TQfV^C61&z{CNVm)1JFMa#QRAvO@vVGP0Y7A4n-+c4Pc~D2DCWTz=bBFc|4OS*Rr!8>&HLa@_;S343X#Sn2I*6%gy6=v{X?|tND-|wcnPG_|k@j_2 z6-xR*2R+}E?_!pS#cE#ycHvjP0TX;hXA6q+6yb+P`la9s za2$EK5W`4{i4Mn=d1A`#QXVY$!qb2Upp zrpUTaWHv?e4O9=}YDaaUMkXmCMnN?LsU7H&Y*sT#2tWP$`05f2MfrKH()Kr#`E0EC zqI;4P;##=5t|u>QqoCe#$)L0Ns z%}KgTBiMYfaPoW+R(1A+WdS&%kIRROA(e9h~h5&ZYf z{`$iNTfxpbirrDv@otZ-B{;Ng8IgfIi^_jjQE?=!45CD(xJ_umDmS^7C^Ly6C{U?# z%I~n=Mux?)I!-kI+WZW2#SW`{i2v4Z33Z56Y6us6vhabDv}t&loU2MIr-H~f8ZJ#e zR-4t>X!8*G4w%WCgiFgLjc0i_u@uQzC3|Y5kad6=!4ZXe1&&^lsgVBKO+ILNmXHJv zQ%*o+wX!f_zX>KXYGV;ptiPI8?9>PA6`4SRRAR1rQ863^tU6!m2b+;jNgG8NnB=Mg zG((A=?0&GeAM2JzZ@UIT_$EhD2!S&de!az2HZ8g^_%L}RQzS&0M& zg#-f`=f{*0E>Qtwf>ZGj`AMp=lY7xmnU_CGIC?`kvD|S;cU=RlPSRJMM036rIm!L` zyUx;8bUUpLcP8MpbaPAy8kh-vrag1}&iNAMlJ3@_&>z6?y`a4fOTU@Y z;wo6jQKc0IGHgs5?Gu|!#5<%*buuWsOyVA*wQvoN!l~8IR`H=5l*mUe( z5C)?>pYD@YqvYzAK@Ss-%(kioUUKpQ}XlNz(fonn3P&y(oyq&@d%h?IOEww@IgmoqleHE7mvUZyD=X4xC(YqSY zrCySf2j)A-vW~pt?y9{B@zz{>J^#!nS$}<4nD+=j^zK54exiJ^UV1iRN`X9X1A1T_ z{sX5+Ny)W3ov6UqGvp67RS`0hzWX2NIU?e=MokeO#v(6eq8M@;t?6!vE8)%6Yc~$X zU4reKH)z1nz}OPoJ0+v2jxa^GLyrp#ci)>xnL2ptm%~8}t=Zu|4fU)5yna2ZC0ro^ zXhndxU^s#EYhLvlV#$ipn$F_)d>CDyNsyGpaLjKSI`5x*i8%M!Z|HR1_{{t*!)He0 z-q*B*T62MB0;?H7*wDi~qaCkR>-)7pg>GvD6cFFKn6QH15%}qCWUfWE>@H!2TTLVM zy&GA&%I2$y%Yiw&wFFw7Z}50%IYH+!Fo>?E5bYtMr8hJbhK52=B$tJTX6mI*DQj1t z2!(&LP)SMyWhY&#(j$-4+OmYT=9jOmXDrL~0Tay0szqv!&A(0r-$Ay?a(gV%xH}ca zQ_w38XTK#JH`oX!F`dohDjlUdf+$DGXs@@HwNFGe>jV9@^_AF>@hw2eJAj5<>(2gT z+kb$eoTJ}kdH+^(kiF-@g9iH#n@jpTJN<_{j~?yr!RI@>yAK|I5j?oF|NPd@Kc39e z?EbHQ{(F0Scjy1xJ%97NU*-HCZTENY&i|j{{80*}+AJtLMpxBrx}p7ldwXwh7w!KK zA3W%9?>+<&?(RI;-n+B^e~zD@zC^V}k^$G$=ELO>HGQ1z;LD#|Eog^Yx%fje%(G#Y z&+*eBfR9JheEg1_gwtvWd}D<%?y3PbktN1EJcDS9f&zL0=6H3*+(O z1O5|MA=cxac$8OIn?v@$)y&Skcl_V7`|qO5#{NIsK<*z>{+}q=F-;JWlh>r30((|L(SX|92m4Ke)U9|CsyVbf9;1;NSlK z;~C#V1#}DffBV7Co&5LPJb$ca1w8a{eOK^R^qcl7_a zc>aFUzYH5zod51#f7kZ^-|hDw-tqsR<;V2om&Yuf`JbhuA}($)(07vbf-~{;DTR4+ zVEghwfFij}OD;?z#zLoXVYYgrp7m3F)x#G$wIsXQFqtuP_amRcIVhbX1Z3IPti(V! zx0}eWBBdn#%9#-=bQ+7_VJPI}Lkt_dq>_{`RQklB>>#oo{3q>#i{Yq6&*P%R92I4c ze=T6p{9Tfn0l+COkUoye`7~9&NoE;T;ZKZ>$>DLwHo%6kK6Z3d8|~t*Q>SjM2@+lq zCflB;(?rCXhPr>tO&ciyCHnE!!j6X_>-4&ATrqVKoFuT3Wi_M?yS4pPs{bnP zb86-ux5r#6Wtm=PoS`|sWP3O-tdxIPYl~VwWVJ7JR3v3S{UDqNKN>DX=%Vleu+7eQ zpz4U@V7PKlU8G_Q3}S-O1(-uByYvPZnB&)wucMB!v#%#Kvq)fFTvd}R# zV_{cw7#r7rwi7Y495j%)70nmrRoJaJi5Bw-z&MoK&`nUHlW>-IClYvDgR+_?pVwpe zaU{U~|CGc&f@7z+c9%?qK9VTi364an_vtjh=JK(sw@8T1+1o3fS&x_xnRZdBDD33e zVvWvQBZ!&3o_|jva!vjZp1oUzey-;K`u*((kEs5~gZ?&1e!Cd|xx4*v_m2Pj96!tI ze^Bx<%rEs1D)K?mCPS5BPkg#qq?4AakkL?XgMvX-nGTU^(hh2+d|#pk?i4q=s@%sR z2CaRTS|fF4n!YvDkv61qu#+vs8E~>i&zE_#am2binB^$dYxJ9P2aKqk31u}P5+Wyf z{rdPR^;g2ct7)8O2ceawiysK`80+h#h=;%@QQwe!R+H?qx*GJiyXHVoLLSc?VrR1< z4tat?EZSlw)T>0=QNxn*$F=(wiDRJu@evSSV- z4$aAX_TH=+()9uKlp~dCJB{F3f5l?BzlO%Rw>*6@!jFwZ-E6!nHP&ByYIrduXFuiU zCn62+FF9jmu-79m7w>dKojF1fmR^u8rqcn(^NF0b6sI-l$YjB!Xeg7avoQgc`#cEE z9b5Z|*M_kgqIs~<=7SyT(X631cshB2?DDxz$7SORBjh=-i>v+n^wL{-`8$T0*5Z?&#LrNUJO2mfBe|9reIUX9v#b@lW3_pNutykYjb8JCD7ZKK7ySHQ9 zPhR3}?XSrSteAKW1jd>klo$~cP^dvL_8DZ$%3jq@D3@o@Y8XIh(PcFmGuH-XnIc^= ze`rjBC86PPW2~S&6Q4nw#GZ54M=12R>*y7B^#Jk)%JuiusGi5>Rb0T)Y9#O=u(1T{ z$sG^Tn6+-dd%;01HTnfVHkca(Z5QJt757bmWq;Z8&UxIUL$xLv!O5v2wg>cZeXxM}bv3^)XQGU{4;Yd}fQjj#}zv@Kqw z^f8tVXw^LbhPdUNl18xSmxbV`y=to2WpQ9J0tR)%?{=EL+*G7f%XktROVX=&vUH-I zTTFDg^$VS#O~@%RwmT5+kJ9MZVDpPuvjJpaz)sANA^MG6yuis$NoY z>VP#TJ@H z$^NNH+~IxuJe3LgtWS-H4?bKmbpm zMN-9jA;i{VHk?5acINLoJwtc;niZReA@v6wFV&kS)34hQoH8j(i4YZvjv_IEn?bg)k!neDMT;Zq(MJ3m2tlL-( z7W`Rtg=(XoBxMp8S?!<*o3wDQ~vcpE;QY6VY1y_nl+{#E}&J=Er= zqI;YKck3~-lOJy+LxjK6v)O=bSX+HKiK~@LmYx6Tnqv}^6{_LIXukVN%Td`GoMqxK z=&~dYo_dBfnf&_CJ*}S$F%yjRyz6er9p@ml$GoNCQID}?y!Djq_gLc6h4v_4*F8a& z;j1U{gu1T#;N4@MNN2=f3p+o=({#ec31x*g)3_`6YFrzx>}o$a7-QMzyf89)c52D* zJ9I)GbEm_F(G2i(nih&Mp$O6x2mplZkM3nOhV->8k*uGNNr1N&?l&znZ@r|HzNg^ zfbL;HZ>I3J^(Cei0ujyVA~Y)xp7a5OQMMd((!PNf*fYZaBqW-l+DG(5=?QMZG&Wi* z8P1&qM;PFLdV%V<<8*w|u(DBq-G*zAxZ5!<-}_R41_7#iNJI!}iNlPGvtw8V=vfQ zCl&9CF}97Re)K{|-i}`ko{w)jdWg>a@y`%BlMU$p3 zySmHnnqBsEm%TN+JkVVpbgaG86R9LhE0;LBTMJTRRJEKFQK!t4oz|P+8#=cAAe{tz zJ?2G2#T9jfHr#{uWAw&08t(cV?$jFY_#5_(h6mK}KfDbew0YO|$LWJw_eb9Dd;acw zweAnS-FN-ncWd1rc)RcTyYJMx_r2Zw{_X&};Jjld4GmQ0N32f~)^4F;M8j#8-?N@V zkadv^jkLOeVK%)nx;?)+{r(w6Ai%#E(jinZ6c0ed)$p=EFwpW2H#WtM&F|laH@cZd zFj^X?|0*vlcmNOuWNp1A2fs6@(WB3~`a62`3+d6V2$JLiaNq{Qgu-O_5g8ptBGA#^ z`ediZq1UVf4reYUNm4+H=p?j!3-&msQMk%)Tn6rFTogUNxFD+;vf4t%q?jD?Sn&dJ>@eZvEVP8F9gRBO)pCcvo5*wZ#gKlpOCt}D@|2g0r$Ki<(V)(; z8>-SF8=_?iQkH3#p@FFR38V^O@FAWmIhbl*tMc*0WC|mziI$I6!E zPH8BZ-7S+=e9-={{3`2|0Wi^5^!B#9R%fjTZ|HwIUQFkeTKri$gKNyn@j?463eK>a zZ{dvU=)s$YzUFAPgQUns9`>@`Gt}dQ_R}H?exR~5ZC8}?ZJqoL^m$-_9>(RNV5AlM zSh^7-PBq^1Cw=|Nm_OM$Uqjy7^%9-q@>m8-#*wR>?~%Gb!X2#cJb!03H;hpwRe+3u zcM3Z}JB+^iV>gQ0J+_B*dL8u}lTlc$8GBd`f%w4)fKKpc8qnOY8xu56-k-ngig9f^ zUc)nf6l@&3-Ox)=&~B*FQBUrm;w@LO3RCUo2G+vcX5QBt8WmVjKpciaSTT1!9{ukJ z`t0%18atm^sv@P@>iRxZ>bo=>=NNRHjYXd5CR(KQcR?sreuAL=v1>|@vAm3A5ArH9 zfhRw()IDaGCrw>QS;Sg@lB~Pd{nRF;rv8U!|HB>m$1eO~3fh{Q*c-5k37p6}2XYH_ zD!t;L>QK~cX4b^siAUqfvX|>nJmctnbrqvbcA;E);8%8k?A7?4B3YCiPW&M*(tN?s zRT+8b5I&S^;fIK++8i-N9Mvzx2oaUlG|E$Rl&9t>Pj}@Xd+^6zoxLd2*?gKTvo;Wg zs2`qj-?Xn4y=^XB32nQ}C=R>@qA})O=mni`#2Au!7nVDNrakVgJ7V7)QAuI-5hVm~ zH_MJW%b^tE)|*8h*X?HBHD`V*Wy0IfEb+x%je|I|ID@57B_@5s?(io5Jz~tmQ%eIQ=Ms?+oyUP}YGo-QsgH;2JZ^V4Uc{46yND;ZTEyX} zUBn?RVv|+KC_`fz=Gbxt^&q&VixH!43KE&t!jCZ?b9Dd?uY<@Q0sNPR|P`)WP z1=o_me|Zk*-M@N(QlRu#kX-gibdUTJp|UddhM zNUie8r&y&pp`G!sE==cHQswZU+n&IY0?D49Wb4+quI_ZiYJpWH$TL>NGml3p1D|&3jlc& ze51c^mJfTBgCulvXmruL!nzl{)gAvV~a|Kml#5m64llJ+yaIoGt66-&&xo8=#{z zvc4MMxNY`pyO2b^XC8j7@s8d2y#53vm>F=$2`E0fp@0StuQ8v=;7tdx;P^!l?x1f- z=lphhz+X*k*Z&ptJ8P(+7(%c#_-pV}n}4CCrtSTptwIIS4BY;?7YI<=Uj#2tbWkB0 zf!pW3K>7j#+_%yRfcrB*8}2J5n%obJeF)O`OKdrQ+HuRtsEWK|T6enaQ9F&tnEdAg zi%L*^bxKJ?f?bcD5mS{s)*`_Tsv+qx}3!-W}_|z_EVsJ=nXG{U<)zEVoGfFCxOX^@p+7-LJ>_ z7%b`VS9pXJc1vRr&7uXE;C}=4-J$$Wh$I^bb^rRR1t(n#l~}vQLB)RGtuUPoRzorV zm7rI1i6`}Gw8z{b=1o{|q-9 zo!)yvQeE5OVp^puf;R32vXz++si)E!pf;_#N{b0r_N;DX;>d88eypX-;_x3U#h*S+ zvl2yf%BU^0SR31Q>RBx5C|0l1Ow9+Kx%wj=bkZ7Xc~`U!75e|%yY}X`aU-6;^(k0+ zZYX(VS}!{{np0ZGc5ZxgaqjG-osOH*ltfEBQzVZMJD&Ewd-nm507*UU%cW_@nn^5? zz+wR`fW>05zd~YE5^LnfXmZ7xkCBy9)z=6Ok{8v_Lp?=$f+&CCl`gS2pDJAvO@iEb zs$x|{N$VJ;b0cLjV9*N{|6xTJ!yD*jT{viI%pob(Wjx9askQ^Z$dY!pdt;NvEokc8cTP6^|#iie7CiKVj8X>6C;XyHgiT zlzOa!(JBO@ZJdXJyXs7_`{+eG7lG0ksc>`DtKmhhPA(*+eEpRtp#4WL^m%!jl2OC$ z>vh~7t(Xa3_t%`nKh{mX5MWI$sp0kbDZCbTJbPL(}pjOSgtvtLLPMS|MY*Y(BSXH>Q zvT&#Q#0gLf6T=sYR`&_y&7ZX&NurF_^m}NPJg7_1JvH9C_%F=Q~Bzsy{VpH!JDZ^vBZTJc}UOijC%$HFGc^_7AS3``GC}ysO6Gd)NC*MyL?awLITMKjz1mrrx zTg&ryqPIzkKLeFoG5B8=;X2;SQ4zyW53Hvq)ww+aRSCUADX<~Nh}YteQ_y(eyk$;& z*NrevWQaCp^UQFT2CA;+G+ahm>c(d2Qgwm4QWn}A!exyARj+-~C9cF!kR>R%2ZqE) zIL3wh0BstWxNt(bH|XC=X7@0QISPnDXK{&vy;iK}4`!r>~7y-A{UN^|r zslIBXDRZsiOg!I9Ny2$KaS(a*K8$bCNjIQLhIY;ZwU2vr?R3E1wv6V1vBcK z3SP9K7jiVAIvNVr&^Ba_R?|4zj*m4=X!D2&%qLPew286AiW;W3$q#$VUZ;7TMPQ764!(xFSmKg=)KZHp-k$6>CG}&$`JfGgrju z-iR4cXXg2m9hRGSzHFZGTirP@H>rr^Ou z+n#yT)NE>PSc_D;ddF7-v@>yF??6zc1#;SjJCC7KUKmt>(<(tb1llzRP_QvREru?I zdZOC8K|#p@(HIq#K+q->;MT!AtNX0HsZN;tB#Oy??|{?~726YG2g9hP;eD2`WiJW1 z6lISAP|I)1=_iDDik?vnd|G}Z(^o(y^4$~Uy#hu$qJWObSQ|0?XOV}y1}dY^l^x@X zkM7P$4F$ij=S;!_pny{>g&4fa*xPQ%dW%@l{w zi5?zRMi)MZ!y)~Bd3d1x?RIWu*08$Qam!U^k(l{e9~y_e1=b>A@;BsU%b&! zHokImi(%%HhQ4{0?*zWA`Z7yB*mt`D^D7f3ODD#A#h=n@;yjJ_Nkt<5EAHXK;1pp^ zTc@z4BZz4Y!(K_v0I7JblNJwHoE_c;3qt@baI|jo!EzZ{wO!zPo+9Tit^TAm6sH#4lg_<6*c+n}DRpRfOd0O^x-(hl z#LZ?j;wHbs4%;1I+;;}1x<_5;4m79~)gi|i9PUklYiEkrtCN&4Kkghqb8`|3q)|CD zR)DpvhJY_cBMP-4=nB{g3CD^DRR=WFVTYRGfEw5o8|ZxPw2v=NkKUi!$M1gp@b=t( z|M#CxFJWr_S3W135px1{vZLf-8bT>SiJYl?UjrGBRw%RsWm7y`$^}ljlm3?o zdS$7xHZtBt;tsob%*t&VV$>%Tq6ja`qxtF{WI{8?i6g0dwzJT&msKuHlmWD3Zzl15 z%!CUmj>^i}uzg0x*^R+%lw0?(NlxKA(O-a2&WI_laYt*Of(ijIYtsKSjV$qN+`%=74&_e zh!9AV6fN3bY6($y0zAA;{P4cQOD+_TVF;=i!w`W9tvB48z%YaSnTzM9L59jav9o}V z59<1kWNrj1^RJl~mx>(F(38LPAy^TCIts%T1ab`=Tl|+FW5akKukisiVqjd4+*ckTuFpo5kj1Pf_tWa1%#3@7ErK|9P&yHwYT+g>&utov1u*A`@ldGgYzLc# zWWOJktlzU$aQ5Q@%s!to?>*l)rdf=~A5fvQe}PQ=CJdoyasPoEapF!LWLBAx#WY)l zA_!z+G1icX#4n`jdK&uKk|8`+z+RBhF#~jxwD2mw59*4L651(#V-dw;GFKFJ!d-ZQ0K~c4R=%WZi<$aE z57bMq>2a|>Rj^o4V#=FkPfS^A)1s!KHTBpS)rh_P_(o$-5un+G&f|oFH4Q$!TQ^{yzf$`mi9Utw=nbCBb`-ld875h@)goXEVK5 zS;WVhFEXuBsD8*GIqUQu@zGPh$=q0n)UsJ zJ_ImjqhE|ouhh9noxeNMcV<@F%{mEe_KWF>GA8J(BF6+O^#v5Wn4Koh^ww|#-f4(U ztZ;i#=c}g-{nfHt-vsp2hRK+%Aff@n$uA0dgh&-#L(o6)+s649J%#bGON$AHiDB}@ zF{m?0{zZsIzee^%8od&gDv;|^UIT37B3N*(-9fC7iDpPK2pvE5xssoi+-{Q120f6!`9-kw@8*4zKP z!^3V**#C#agZ|4G-G29=J9xJLe~AxhcHYvNyQ7hjpL(XTbLS;{Q_l+H`Tlefd&wN< zf|_$rD+=a+p2RQ{FUCul^~u?A1T6{^;)n=7jvnTAe8@V zTqA@LX1wmxXmF}M)rFaGEYUoa2p86pdWvh6w30@QybC(l-_Snj2**g6%$E+@^3#%Q z8as=X2(lp@?fr3~m0JxaiI#Lc4UlEC(O+DN5U^g?*8&0f`kIr0PTpv|diBa!4^n-C z>vM{hT6DusA7H17nKt5P|KxCZ=1s8KNbC=S%Z3`FdL%7#0+$?G-1o`RRd$TEp5 zMHi|~{ipi2Dy|@28QcLxQv z%}~xI_~u6AL&E1ceE)`}x`RRoW3-T6KB*B6gMm@K>4HFVp&4m=$LC7e8!jl~k7&+q zn^DcMuKDV7Y-l2C3EJJ}ex*s_SX#N>Kxb~!gXYe28pX}bq!P*^L6g_Jz#Nb8t_2_daKoDgy}^KR%sAXPZ}H5m|mWzYCG{ND#|kiezVFal{B zZ>{c0Pblf|YhjyzzmM5k~bGzre#_p`L{OkgMx9I3xnX+6bc~DO9#8_?60)fW@c=DaaP&Jtd z7+ng$ZP-tuEE%T`RG>_nPZeT5$`LCf**)AC^=Y)wTwkA^pPybBKfF6Thd$KWf@$2a zu;(_W+?ph+nmV^4QV^}5(A<2eCZcbXnGkqzk}ycb9*_KS%gtcHKy^|x;Yo+c`Qpe3 z?N2VAJ@R=JOzUrI&WdoWz_)d;XW=x%*@m9uVts+;7JPLQP7~uhsq<8O$pTJikPs*& zq`17gwxL&l^rmi*xQ%t{MJ6SVJPm!$fhS3#;ajgSPxgH8)`g+db9YyJ5nmAGkS( z?_K(l+y!>*f{MYXXWhp0mizyD|Ig$9k)D35hsMi;gH_{y0K;DF{|7Jy3~B#A>^+bF zFXI3Ar~NF6_dy-ocY`~F&z{(gjwFCwKb6C{EW(Qe!OV`F=`AQC!apy}(?m|XkZynI z>^4p0uQ2}xDu|@`fSz_}uf)~1wTqhrX`;LVox(15V?3OjNG7=6YCh#eQ<1&yQA5rl^(sNi4m%uIK$K#5)Y|N;7R3Jdk;#; zbU_I$-paM&=09yo)3(va*!)NHW9#al_-D6GznUGbfDUGV%Y+L*0;WBxm`c;QrKPau zahOG|p18Nd2f7!a^?FNJ9_9vgy%F_1PjTj9gSSOMnAG2l0W2I5%<6n0yzL6d3atT6 zEu~>9%>a{&DFB0|iiEg0mnT3`n6Y^u!(`1%*ID9SJMG%wnG#N-FbN4Gkzmaa16a&7 zV$^qLVdIpHWHH+G%^LszwPq{HG3ek6U;?Y*^3_doWk7Jk?`?PKcvQ{=Qdz~tg1^j{ zpRAcHsV#Bq3;$fAc#I6d78fCHz9-Xsr=ZD;yUR1<2wiv)<`I5_ zkQ?H)n&TaiAXrG_@*#mX<*Xp%GdIFggH%lo)tCdbOHBVG@R{Mh5OT1W-S#Q*Ti-f- zY=)PQ%P_D8YKHe@tAXd2;Z1sv+e5X#;5j=9;^w>{1EV^F89>na*myqndHy_qo=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) diff --git a/requirements/prod_local.txt b/requirements/prod_local.txt index 6c22478521..7b0b1630ca 100644 --- a/requirements/prod_local.txt +++ b/requirements/prod_local.txt @@ -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)