From 039ae87bab202c76b63d779c2f4f74f4f45fcbd3 Mon Sep 17 00:00:00 2001 From: Ryan Stuart Date: Mon, 8 Aug 2016 15:00:11 +1000 Subject: [PATCH 1/3] First attempt at abstracting assets. RE #39 --- icekit/assets/__init__.py | 1 + icekit/assets/apps.py | 8 ++++ icekit/assets/migrations/0001_initial.py | 29 +++++++++++++ icekit/assets/migrations/__init__.py | 0 icekit/assets/models.py | 53 ++++++++++++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 icekit/assets/__init__.py create mode 100644 icekit/assets/apps.py create mode 100644 icekit/assets/migrations/0001_initial.py create mode 100644 icekit/assets/migrations/__init__.py create mode 100644 icekit/assets/models.py diff --git a/icekit/assets/__init__.py b/icekit/assets/__init__.py new file mode 100644 index 00000000..dd48ee45 --- /dev/null +++ b/icekit/assets/__init__.py @@ -0,0 +1 @@ +default_app_config = '%s.apps.AppConfig' % __name__ diff --git a/icekit/assets/apps.py b/icekit/assets/apps.py new file mode 100644 index 00000000..129ec942 --- /dev/null +++ b/icekit/assets/apps.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class AppConfig(AppConfig): + name = '.'.join(__name__.split('.')[:-1]) # Name of package where `apps` module is located + diff --git a/icekit/assets/migrations/0001_initial.py b/icekit/assets/migrations/0001_initial.py new file mode 100644 index 00000000..23e45ff1 --- /dev/null +++ b/icekit/assets/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('icekit', '0006_auto_20150911_0744'), + ] + + operations = [ + migrations.CreateModel( + name='Asset', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(help_text='The title is shown in the "title" attribute', max_length=255, blank=True)), + ('caption', models.TextField(blank=True)), + ('admin_notes', models.TextField(help_text='Internal notes for administrators only.', blank=True)), + ('categories', models.ManyToManyField(related_name='assets_asset_related', to='icekit.MediaCategory', blank=True)), + ('polymorphic_ctype', models.ForeignKey(related_name='polymorphic_assets.asset_set+', editable=False, to='contenttypes.ContentType', null=True)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/icekit/assets/migrations/__init__.py b/icekit/assets/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/icekit/assets/models.py b/icekit/assets/models.py new file mode 100644 index 00000000..56d768e3 --- /dev/null +++ b/icekit/assets/models.py @@ -0,0 +1,53 @@ +from __future__ import unicode_literals + +import six +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from fluent_contents.models import ContentItem +from polymorphic import PolymorphicModel + + +class Asset(PolymorphicModel): + """ + A static asset available for use on a CMS page. + """ + title = models.CharField( + max_length=255, + blank=True, + help_text=_('The title is shown in the "title" attribute'), + ) + caption = models.TextField( + blank=True, + ) + categories = models.ManyToManyField( + 'icekit.MediaCategory', + blank=True, + related_name='%(app_label)s_%(class)s_related', + ) + admin_notes = models.TextField( + blank=True, + help_text=_('Internal notes for administrators only.'), + ) + + def get_uses(self): + return [item.parent.get_absolute_url() for item in self.assetitem_set().all()] + + def __str__(self): + return self.title + + +class AssetItem(ContentItem): + """ + Concrete uses of an Asset. + """ + asset = models.ForeignKey( + 'icekit.assets.models.Asset', + ) + + class Meta: + abstract = True + verbose_name = _('Asset Item') + verbose_name_plural = _('Asset Items') + + def __str__(self): + return six.text_type(self.asset) From 54730d5cb103f274ff0b35b79edfeff7b2854e65 Mon Sep 17 00:00:00 2001 From: Ryan Stuart Date: Thu, 20 Oct 2016 13:42:56 +1000 Subject: [PATCH 2/3] Refinement of Assets and conversion of Image. Writing the migrations when converting over an old plugin to use asset is unfortunately a manual task. But it's not as difficult as it sounds. --- icekit/admin.py | 15 ++++ icekit/assets/__init__.py | 1 - icekit/assets/apps.py | 8 --- icekit/assets/migrations/__init__.py | 0 icekit/assets/models.py | 53 -------------- .../0007_asset.py} | 4 +- icekit/models.py | 43 +++++++++++ icekit/plugins/image/abstract_models.py | 42 ++++++----- icekit/plugins/image/admin.py | 16 +---- .../migrations/0009_assets_20161013_1124.py | 72 +++++++++++++++++++ icekit/project/settings/_base.py | 2 + 11 files changed, 157 insertions(+), 99 deletions(-) delete mode 100644 icekit/assets/__init__.py delete mode 100644 icekit/assets/apps.py delete mode 100644 icekit/assets/migrations/__init__.py delete mode 100644 icekit/assets/models.py rename icekit/{assets/migrations/0001_initial.py => migrations/0007_asset.py} (89%) create mode 100644 icekit/plugins/image/migrations/0009_assets_20161013_1124.py diff --git a/icekit/admin.py b/icekit/admin.py index 7d6cc7a4..61265cd4 100644 --- a/icekit/admin.py +++ b/icekit/admin.py @@ -9,7 +9,9 @@ from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.http import JsonResponse +from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ +from polymorphic.admin import PolymorphicChildModelFilter from polymorphic.admin import PolymorphicParentModelAdmin from icekit import models @@ -166,5 +168,18 @@ class MediaCategoryAdmin(admin.ModelAdmin): pass +class AssetParentAdmin(PolymorphicParentModelAdmin): + base_model = models.Asset + list_display = ['title', 'get_admin_thumbnail', 'get_child_type', 'caption', ] + list_display_links = ['title'] + filter_horizontal = ['categories', ] + list_filter = [PolymorphicChildModelFilter, 'categories'] + search_fields = ['title', 'caption', 'admin_notes'] + polymorphic_list = True + + def get_child_models(self): + return [import_string(kls) for kls in settings.ASSET_CLASSES] + +admin.site.register(models.Asset, AssetParentAdmin) admin.site.register(models.Layout, LayoutAdmin) admin.site.register(models.MediaCategory, MediaCategoryAdmin) diff --git a/icekit/assets/__init__.py b/icekit/assets/__init__.py deleted file mode 100644 index dd48ee45..00000000 --- a/icekit/assets/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = '%s.apps.AppConfig' % __name__ diff --git a/icekit/assets/apps.py b/icekit/assets/apps.py deleted file mode 100644 index 129ec942..00000000 --- a/icekit/assets/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import unicode_literals - -from django.apps import AppConfig - - -class AppConfig(AppConfig): - name = '.'.join(__name__.split('.')[:-1]) # Name of package where `apps` module is located - diff --git a/icekit/assets/migrations/__init__.py b/icekit/assets/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/icekit/assets/models.py b/icekit/assets/models.py deleted file mode 100644 index 56d768e3..00000000 --- a/icekit/assets/models.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import unicode_literals - -import six -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from fluent_contents.models import ContentItem -from polymorphic import PolymorphicModel - - -class Asset(PolymorphicModel): - """ - A static asset available for use on a CMS page. - """ - title = models.CharField( - max_length=255, - blank=True, - help_text=_('The title is shown in the "title" attribute'), - ) - caption = models.TextField( - blank=True, - ) - categories = models.ManyToManyField( - 'icekit.MediaCategory', - blank=True, - related_name='%(app_label)s_%(class)s_related', - ) - admin_notes = models.TextField( - blank=True, - help_text=_('Internal notes for administrators only.'), - ) - - def get_uses(self): - return [item.parent.get_absolute_url() for item in self.assetitem_set().all()] - - def __str__(self): - return self.title - - -class AssetItem(ContentItem): - """ - Concrete uses of an Asset. - """ - asset = models.ForeignKey( - 'icekit.assets.models.Asset', - ) - - class Meta: - abstract = True - verbose_name = _('Asset Item') - verbose_name_plural = _('Asset Items') - - def __str__(self): - return six.text_type(self.asset) diff --git a/icekit/assets/migrations/0001_initial.py b/icekit/migrations/0007_asset.py similarity index 89% rename from icekit/assets/migrations/0001_initial.py rename to icekit/migrations/0007_asset.py index 23e45ff1..8c5ec8d7 100644 --- a/icekit/assets/migrations/0001_initial.py +++ b/icekit/migrations/0007_asset.py @@ -19,8 +19,8 @@ class Migration(migrations.Migration): ('title', models.CharField(help_text='The title is shown in the "title" attribute', max_length=255, blank=True)), ('caption', models.TextField(blank=True)), ('admin_notes', models.TextField(help_text='Internal notes for administrators only.', blank=True)), - ('categories', models.ManyToManyField(related_name='assets_asset_related', to='icekit.MediaCategory', blank=True)), - ('polymorphic_ctype', models.ForeignKey(related_name='polymorphic_assets.asset_set+', editable=False, to='contenttypes.ContentType', null=True)), + ('categories', models.ManyToManyField(related_name='icekit_asset_related', to='icekit.MediaCategory', blank=True)), + ('polymorphic_ctype', models.ForeignKey(related_name='polymorphic_icekit.asset_set+', editable=False, to='contenttypes.ContentType', null=True)), ], options={ 'abstract': False, diff --git a/icekit/models.py b/icekit/models.py index bc2443df..1287ddf5 100644 --- a/icekit/models.py +++ b/icekit/models.py @@ -1,3 +1,9 @@ +import six +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from fluent_contents.models import ContentItem +from polymorphic.models import PolymorphicModel + from . import abstract_models, managers @@ -15,3 +21,40 @@ class MediaCategory(abstract_models.AbstractMediaCategory): A categorisation model for Media assets. """ pass + + +class Asset(PolymorphicModel): + """ + A static asset available for use on a CMS page. + """ + title = models.CharField( + max_length=255, + blank=True, + help_text=_('The title is shown in the "title" attribute'), + ) + caption = models.TextField( + blank=True, + ) + categories = models.ManyToManyField( + 'icekit.MediaCategory', + blank=True, + related_name='%(app_label)s_%(class)s_related', + ) + admin_notes = models.TextField( + blank=True, + help_text=_('Internal notes for administrators only.'), + ) + + def get_admin_thumbnail(self, width=150, height=150): + raise NotImplementedError + get_admin_thumbnail.short_description = "thumbnail" + + def get_child_type(self): + return self.polymorphic_ctype + get_child_type.short_description = 'type' + + def get_uses(self): + return [item.parent.get_absolute_url() for item in self.assetitem_set().all()] + + def __str__(self): + return self.title diff --git a/icekit/plugins/image/abstract_models.py b/icekit/plugins/image/abstract_models.py index 893209da..bff7a5fd 100644 --- a/icekit/plugins/image/abstract_models.py +++ b/icekit/plugins/image/abstract_models.py @@ -1,16 +1,19 @@ from django.core.exceptions import ValidationError from django.template import Context from django.template.loader import render_to_string +from django.utils import six +from django.utils.html import format_html from django.utils.safestring import mark_safe from django.db import models from django.utils import six from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from fluent_contents.models import ContentItem +from icekit.models import Asset @python_2_unicode_compatible -class AbstractImage(models.Model): +class AbstractImage(Asset): """ A reusable image. """ @@ -18,38 +21,33 @@ class AbstractImage(models.Model): upload_to='uploads/images/', verbose_name=_('Image field'), ) - title = models.CharField( - max_length=255, - blank=True, - help_text=_('Can be included in captions'), - ) alt_text = models.CharField( max_length=255, help_text=_("A description of the image for users who don't see images. Leave blank if the image has no informational value."), blank=True, ) - caption = models.TextField( - blank=True, - ) - categories = models.ManyToManyField( - 'icekit.MediaCategory', - blank=True, - related_name='%(app_label)s_%(class)s_related', - ) - admin_notes = models.TextField( - blank=True, - help_text=_('Internal notes for administrators only.'), - ) is_active = models.BooleanField( default=True, ) + class Meta: + abstract = True + def clean(self): if not (self.title or self.alt_text): raise ValidationError("You must specify either title or alt text") - class Meta: - abstract = True + def get_admin_thumbnail(self, width=150, height=150): + try: + from easy_thumbnails.files import get_thumbnailer + except ImportError: + return 'Thumbnail error: easy_thumbnails not installed' + try: + thumbnailer = get_thumbnailer(self.image) + thumbnail = thumbnailer.get_thumbnail({'size': (width, height)}) + return format_html(''.format(thumbnail.url)) + except Exception as ex: + return 'Thumbnail exception: {0}'.format(ex) def __str__(self): return self.title or self.alt_text @@ -79,7 +77,7 @@ class Meta: verbose_name_plural = _('Images') def __str__(self): - return six.text_type(self.image) + return six.text_type(self.asset) @property def caption(self): @@ -95,7 +93,7 @@ def caption(self): @caption.setter def caption(self, value): """ - If the caption property is assigned, make it use the + If the caption property is assigned to make it use the `caption_override` field. :param value: The caption value to be saved. diff --git a/icekit/plugins/image/admin.py b/icekit/plugins/image/admin.py index 12664102..991a9689 100644 --- a/icekit/plugins/image/admin.py +++ b/icekit/plugins/image/admin.py @@ -1,23 +1,13 @@ from django.contrib import admin from icekit.utils.admin.mixins import ThumbnailAdminMixin +from polymorphic.admin import PolymorphicChildModelAdmin from . import models -class ImageAdmin(ThumbnailAdminMixin, admin.ModelAdmin): - list_display = ['thumbnail', 'title', 'alt_text',] - list_display_links = ['alt_text', 'thumbnail'] - filter_horizontal = ['categories', ] - list_filter = ['categories',] - search_fields = ['title', 'alt_text', 'caption', 'admin_notes', 'image'] - +class ImageAdmin(PolymorphicChildModelAdmin): + base_model = models.Image change_form_template = 'image/admin/change_form.html' - # ThumbnailAdminMixin attributes - thumbnail_field = 'image' - thumbnail_options = { - 'size': (150, 150), - } - admin.site.register(models.Image, ImageAdmin) diff --git a/icekit/plugins/image/migrations/0009_assets_20161013_1124.py b/icekit/plugins/image/migrations/0009_assets_20161013_1124.py new file mode 100644 index 00000000..1a6d9946 --- /dev/null +++ b/icekit/plugins/image/migrations/0009_assets_20161013_1124.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def create_assets(apps, schema_editor): + # We need to create a new asset for each image + Image = apps.get_model('icekit_plugins_image', 'Image') + Asset = apps.get_model('icekit', 'Asset') + for image in Image.objects.all(): + # Create a new asset + asset = Asset() + asset.admin_notes = image.admin_notes + asset.caption = image.caption + asset.title = image.title + asset.save() + asset.categories.add(*image.categories.all()) + + # Set the asset_ptr on the image + image.asset_ptr = asset + image.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('icekit', '0007_asset'), + ('icekit_plugins_image', '0008_auto_20160920_2114'), + ('icekit', '0006_auto_20150911_0744'), + ] + + operations = [ + # Add the ForeignKey first as not the primary key so we can fake it + migrations.AddField( + model_name='image', + name='asset_ptr', + field=models.OneToOneField(parent_link=True, auto_created=True, default=1, serialize=False, to='icekit.Asset'), + preserve_default=False, + ), + # Create asset links for existing images + migrations.RunPython( + create_assets + ), + migrations.RemoveField( + model_name='image', + name='admin_notes', + ), + migrations.RemoveField( + model_name='image', + name='caption', + ), + migrations.RemoveField( + model_name='image', + name='categories', + ), + migrations.RemoveField( + model_name='image', + name='id', + ), + migrations.RemoveField( + model_name='image', + name='title', + ), + # Make asset_ptr the primary key + migrations.AlterField( + model_name='image', + name='asset_ptr', + field=models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='icekit.Asset'), + preserve_default=False, + ), + ] diff --git a/icekit/project/settings/_base.py b/icekit/project/settings/_base.py index 0bd47cc4..db0afe94 100644 --- a/icekit/project/settings/_base.py +++ b/icekit/project/settings/_base.py @@ -610,6 +610,8 @@ MIDDLEWARE_CLASSES += ('icekit.publishing.middleware.PublishingMiddleware', ) +ASSET_CLASSES = ('icekit.plugins.image.models.Image',) + # MASTER PASSWORD ############################################################# AUTHENTICATION_BACKENDS += ('master_password.auth.ModelBackend', ) From 1b1c025da116bff3b583daf1e9b359cde446c463 Mon Sep 17 00:00:00 2001 From: Ryan Stuart Date: Thu, 20 Oct 2016 14:15:47 +1000 Subject: [PATCH 3/3] Fix bug causing test error. --- icekit/plugins/image/abstract_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icekit/plugins/image/abstract_models.py b/icekit/plugins/image/abstract_models.py index bff7a5fd..db36d374 100644 --- a/icekit/plugins/image/abstract_models.py +++ b/icekit/plugins/image/abstract_models.py @@ -77,7 +77,7 @@ class Meta: verbose_name_plural = _('Images') def __str__(self): - return six.text_type(self.asset) + return six.text_type(self.image) @property def caption(self):