diff --git a/.gitignore b/.gitignore index eef3882..c31d5c7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ pushreg pbackup mcmd.py dbconfig_local.py +diffmanagement pip-log.txt build diff --git a/pexp/dumpdata_test_correct_output.txt b/pexp/dumpdata_test_correct_output.txt new file mode 100644 index 0000000..5a4e6ac --- /dev/null +++ b/pexp/dumpdata_test_correct_output.txt @@ -0,0 +1,40 @@ +[ + { + "pk": 1, + "model": "pexp.project", + "fields": { + "topic": "John's gathering", + "polymorphic_ctype": 2 + } + }, + { + "pk": 2, + "model": "pexp.project", + "fields": { + "topic": "Sculpting with Tim", + "polymorphic_ctype": 3 + } + }, + { + "pk": 3, + "model": "pexp.project", + "fields": { + "topic": "Swallow Aerodynamics", + "polymorphic_ctype": 4 + } + }, + { + "pk": 2, + "model": "pexp.artproject", + "fields": { + "artist": "T. Turner" + } + }, + { + "pk": 3, + "model": "pexp.researchproject", + "fields": { + "supervisor": "Dr. Winter" + } + } +] diff --git a/pexp/management/commands/pcmd.py b/pexp/management/commands/pcmd.py index 58bdfa8..5a53db4 100644 --- a/pexp/management/commands/pcmd.py +++ b/pexp/management/commands/pcmd.py @@ -20,7 +20,7 @@ class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): - print 'polycmd - sqlite test db is stored in:',settings.DATABASE_NAME + print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH print """ diff --git a/pexp/management/commands/polymorphic_create_test_data.py b/pexp/management/commands/polymorphic_create_test_data.py index ff2fbd0..e45811c 100644 --- a/pexp/management/commands/polymorphic_create_test_data.py +++ b/pexp/management/commands/polymorphic_create_test_data.py @@ -20,7 +20,7 @@ class Command(NoArgsCommand): help = "" def handle_noargs(self, **options): - #print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH + print 'polycmd - sqlite test db is stored in:',settings.SQLITE_DB_PATH print Project.objects.all().delete() diff --git a/polymorphic/base.py b/polymorphic/base.py index 241b894..558bd29 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -3,6 +3,9 @@ Please see README.rst or DOCS.rst or http://bserve.webhop.org/wiki/django_polymorphic """ +import sys +import inspect + from django.db import models from django.db.models.base import ModelBase @@ -75,7 +78,7 @@ class PolymorphicModelBase(ModelBase): 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, + 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) """ add_managers = []; add_managers_keys = set() @@ -150,3 +153,26 @@ class PolymorphicModelBase(ModelBase): 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/polymorphic/management/commands/polymorphic_dumpdata.py b/polymorphic/management/commands/polymorphic_dumpdata.py index ca7e452..09cf477 100644 --- a/polymorphic/management/commands/polymorphic_dumpdata.py +++ b/polymorphic/management/commands/polymorphic_dumpdata.py @@ -1,19 +1,9 @@ """ -polymorphic_dumpdata is just a slightly modified version -of Django's dumpdata. In the long term, patching Django's -dumpdata definitely is a better solution. - -Use the Django 1.1 or 1.2 variant of dumpdata, depending of the -Django version used. +polymorphic_dumpdata has been disabled since it's no longer needed +(this is now handled by polymorphic.base.PolymorphicModelBase). """ -import django - -if django.VERSION[:2]==(1,1): - from polymorphic_dumpdata_11 import Command - -elif django.VERSION[:2]==(1,2): - from polymorphic_dumpdata_12 import Command - -else: - assert False, 'Django version not supported' +assert False, """ +ERROR: The management command polymorphic_dumpdata is no longer supported or needed. + Please use the standard Django dumpdata management command instead! +""" diff --git a/polymorphic/management/commands/polymorphic_dumpdata_11.py b/polymorphic/management/commands/polymorphic_dumpdata_11.py deleted file mode 100644 index bc8f47d..0000000 --- a/polymorphic/management/commands/polymorphic_dumpdata_11.py +++ /dev/null @@ -1,94 +0,0 @@ - -from django.core.exceptions import ImproperlyConfigured -from django.core.management.base import BaseCommand, CommandError -from django.core import serializers -from django.utils.datastructures import SortedDict - -from optparse import make_option - -class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option('--format', default='json', dest='format', - help='Specifies the output serialization format for fixtures.'), - make_option('--indent', default=None, dest='indent', type='int', - help='Specifies the indent level to use when pretty-printing output'), - make_option('-e', '--exclude', dest='exclude',action='append', default=[], - help='App to exclude (use multiple --exclude to exclude multiple apps).'), - ) - help = 'Output the contents of the database as a fixture of the given format.' - args = '[appname ...]' - - def handle(self, *app_labels, **options): - from django.db.models import get_app, get_apps, get_models, get_model - - format = options.get('format','json') - indent = options.get('indent',None) - exclude = options.get('exclude',[]) - show_traceback = options.get('traceback', False) - - excluded_apps = [get_app(app_label) for app_label in exclude] - - if len(app_labels) == 0: - app_list = SortedDict([(app, None) for app in get_apps() if app not in excluded_apps]) - else: - app_list = SortedDict() - for label in app_labels: - try: - app_label, model_label = label.split('.') - try: - app = get_app(app_label) - except ImproperlyConfigured: - raise CommandError("Unknown application: %s" % app_label) - - model = get_model(app_label, model_label) - if model is None: - raise CommandError("Unknown model: %s.%s" % (app_label, model_label)) - - if app in app_list.keys(): - if app_list[app] and model not in app_list[app]: - app_list[app].append(model) - else: - app_list[app] = [model] - except ValueError: - # This is just an app - no model qualifier - app_label = label - try: - app = get_app(app_label) - except ImproperlyConfigured: - raise CommandError("Unknown application: %s" % app_label) - app_list[app] = None - - # Check that the serialization format exists; this is a shortcut to - # avoid collating all the objects and _then_ failing. - if format not in serializers.get_public_serializer_formats(): - raise CommandError("Unknown serialization format: %s" % format) - - try: - serializers.get_serializer(format) - except KeyError: - raise CommandError("Unknown serialization format: %s" % format) - - objects = [] - for app, model_list in app_list.items(): - if model_list is None: - model_list = get_models(app) - - for model in model_list: - if not model._meta.proxy: - -#### patch for django_polymorphic ###################################################### - # modified for django_polymorphic compatibility: - # do not use polymorphic queryset for serialisation - # (as the dumpdata/serializer implementation depends - # on non-polymorphic behavious) - base_manager=model._default_manager - if getattr(model,'polymorphic_model_marker',None) != None: - base_manager=getattr(model,'base_objects',None) - objects.extend(base_manager.all()) - - try: - return serializers.serialize(format, objects, indent=indent) - except Exception, e: - if show_traceback: - raise - raise CommandError("Unable to serialize database: %s" % e) diff --git a/polymorphic/management/commands/polymorphic_dumpdata_12.py b/polymorphic/management/commands/polymorphic_dumpdata_12.py deleted file mode 100644 index 03ab2fd..0000000 --- a/polymorphic/management/commands/polymorphic_dumpdata_12.py +++ /dev/null @@ -1,175 +0,0 @@ -from django.core.exceptions import ImproperlyConfigured -from django.core.management.base import BaseCommand, CommandError -from django.core import serializers -from django.db import connections, DEFAULT_DB_ALIAS -from django.utils.datastructures import SortedDict - -from optparse import make_option - -class Command(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option('--format', default='json', dest='format', - help='Specifies the output serialization format for fixtures.'), - make_option('--indent', default=None, dest='indent', type='int', - help='Specifies the indent level to use when pretty-printing output'), - make_option('--database', action='store', dest='database', - default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load ' - 'fixtures into. Defaults to the "default" database.'), - make_option('-e', '--exclude', dest='exclude',action='append', default=[], - help='App to exclude (use multiple --exclude to exclude multiple apps).'), - make_option('-n', '--natural', action='store_true', dest='use_natural_keys', default=False, - help='Use natural keys if they are available.'), - ) - help = 'Output the contents of the database as a fixture of the given format.' - args = '[appname ...]' - - def handle(self, *app_labels, **options): - from django.db.models import get_app, get_apps, get_models, get_model - - format = options.get('format','json') - indent = options.get('indent',None) - using = options.get('database', DEFAULT_DB_ALIAS) - connection = connections[using] - exclude = options.get('exclude',[]) - show_traceback = options.get('traceback', False) - use_natural_keys = options.get('use_natural_keys', False) - - excluded_apps = set(get_app(app_label) for app_label in exclude) - - if len(app_labels) == 0: - app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps) - else: - app_list = SortedDict() - for label in app_labels: - try: - app_label, model_label = label.split('.') - try: - app = get_app(app_label) - except ImproperlyConfigured: - raise CommandError("Unknown application: %s" % app_label) - - model = get_model(app_label, model_label) - if model is None: - raise CommandError("Unknown model: %s.%s" % (app_label, model_label)) - - if app in app_list.keys(): - if app_list[app] and model not in app_list[app]: - app_list[app].append(model) - else: - app_list[app] = [model] - except ValueError: - # This is just an app - no model qualifier - app_label = label - try: - app = get_app(app_label) - except ImproperlyConfigured: - raise CommandError("Unknown application: %s" % app_label) - app_list[app] = None - - # Check that the serialization format exists; this is a shortcut to - # avoid collating all the objects and _then_ failing. - if format not in serializers.get_public_serializer_formats(): - raise CommandError("Unknown serialization format: %s" % format) - - try: - serializers.get_serializer(format) - except KeyError: - raise CommandError("Unknown serialization format: %s" % format) - - # Now collate the objects to be serialized. - objects = [] - for model in sort_dependencies(app_list.items()): - if not model._meta.proxy: - -#### patch for django_polymorphic ###################################################### - # modified for django_polymorphic compatibility: - # do not use polymorphic queryset for serialisation - # (as the dumpdata/serializer implementation depends - # on non-polymorphic behavious) - base_manager=model._default_manager - if getattr(model,'polymorphic_model_marker',None) != None: - base_manager=getattr(model,'base_objects',None) - objects.extend(base_manager.using(using).all()) - - try: - return serializers.serialize(format, objects, indent=indent, - use_natural_keys=use_natural_keys) - except Exception, e: - if show_traceback: - raise - raise CommandError("Unable to serialize database: %s" % e) - -def sort_dependencies(app_list): - """Sort a list of app,modellist pairs into a single list of models. - - The single list of models is sorted so that any model with a natural key - is serialized before a normal model, and any model with a natural key - dependency has it's dependencies serialized first. - """ - from django.db.models import get_model, get_models - # Process the list of models, and get the list of dependencies - model_dependencies = [] - models = set() - for app, model_list in app_list: - if model_list is None: - model_list = get_models(app) - - for model in model_list: - models.add(model) - # Add any explicitly defined dependencies - if hasattr(model, 'natural_key'): - deps = getattr(model.natural_key, 'dependencies', []) - if deps: - deps = [get_model(*d.split('.')) for d in deps] - else: - deps = [] - - # Now add a dependency for any FK or M2M relation with - # a model that defines a natural key - for field in model._meta.fields: - if hasattr(field.rel, 'to'): - rel_model = field.rel.to - if hasattr(rel_model, 'natural_key'): - deps.append(rel_model) - for field in model._meta.many_to_many: - rel_model = field.rel.to - if hasattr(rel_model, 'natural_key'): - deps.append(rel_model) - model_dependencies.append((model, deps)) - - model_dependencies.reverse() - # Now sort the models to ensure that dependencies are met. This - # is done by repeatedly iterating over the input list of models. - # If all the dependencies of a given model are in the final list, - # that model is promoted to the end of the final list. This process - # continues until the input list is empty, or we do a full iteration - # over the input models without promoting a model to the final list. - # If we do a full iteration without a promotion, that means there are - # circular dependencies in the list. - model_list = [] - while model_dependencies: - skipped = [] - changed = False - while model_dependencies: - model, deps = model_dependencies.pop() - - # If all of the models in the dependency list are either already - # on the final model list, or not on the original serialization list, - # then we've found another model with all it's dependencies satisfied. - found = True - for candidate in ((d not in models or d in model_list) for d in deps): - if not candidate: - found = False - if found: - model_list.append(model) - changed = True - else: - skipped.append((model, deps)) - if not changed: - raise CommandError("Can't resolve dependencies for %s in serialized app list." % - ', '.join('%s.%s' % (model._meta.app_label, model._meta.object_name) - for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__)) - ) - model_dependencies = skipped - - return model_list \ No newline at end of file diff --git a/polymorphic/showfields.py b/polymorphic/showfields.py index 464f9eb..f297c10 100644 --- a/polymorphic/showfields.py +++ b/polymorphic/showfields.py @@ -27,7 +27,12 @@ class ShowFieldBase(object): o = getattr(self, f.name) if isinstance(f, (models.ForeignKey)): - out += ': ' + ( '"None"' if o is None else '"' + o.__class__.__name__ + '"' ) + #out += ': ' + ( '"None"' if o is None else '"' + o.__class__.__name__ + '"' ) + out += ': ' + if o is None: + out += '"None"' + else: + out += '"' + o.__class__.__name__ + '"' elif isinstance(f, (models.ManyToManyField)): out += ': %d' % o.count() diff --git a/settings.py b/settings.py index 2eafaa8..01e4ac4 100644 --- a/settings.py +++ b/settings.py @@ -21,12 +21,12 @@ if django.VERSION[:2][0]>=1 and django.VERSION[:2][1]>=3: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': '/var/tmp/django-polymorphic-test-db.sqlite3' + 'NAME': SQLITE_DB_PATH } } else: DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - DATABASE_NAME = '/var/tmp/django-polymorphic-test-db.sqlite3' # Or path to database file if using sqlite3. + DATABASE_NAME = SQLITE_DB_PATH # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. diff --git a/test_all_versions b/test_all_versions index 4b09640..41d2ba9 100755 --- a/test_all_versions +++ b/test_all_versions @@ -21,8 +21,19 @@ function restore_django { } function test_python_version { + echo ; echo ; echo + echo "#########################################################################" + echo "### Testing Python $1, Django $2" + echo "#########################################################################" + echo + if which python$1 ; then - if ! python$1 manage.py test ; then + if ! python$1 manage.py test polymorphic; then + echo ERROR + restore_django + exit 10 + fi + if ! ./test_dumpdata $1 ; then echo ERROR restore_django exit 10 @@ -35,9 +46,9 @@ function test_python_version { } function test_all_python_versions { - test_python_version 2.4 - test_python_version 2.5 - test_python_version 2.6 + test_python_version 2.4 $1 + test_python_version 2.5 $1 + test_python_version 2.6 $1 } function test_django_version { @@ -51,7 +62,7 @@ function test_django_version { rm -f django ln -s django-versions/django$1 django cd .. - test_all_python_versions + test_all_python_versions $1 } test_django_version 1.1 diff --git a/test_dumpdata b/test_dumpdata index 77da0e2..28b31a3 100755 --- a/test_dumpdata +++ b/test_dumpdata @@ -2,6 +2,7 @@ rm -f /var/tmp/django-polymorphic-test-db.sqlite3 rm -f /ram/django-polymorphic-test-db.sqlite3 + TMPFILE=/tmp/django-polymorphic-test.dump PYCMD="python$1"