From 9e11ffaa371e3dcdd3ae39c7293e457cdf91c138 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 22 Feb 2017 10:58:36 +1100 Subject: [PATCH 1/7] Initial implementation of content listing plugin for Events New `EventContentListingPlugin` to list events in page content, based on the new `ContentListingPlugin` feature from ICEkit. --- .../plugins/event_content_listing/__init__.py | 1 + .../event_content_listing/abstract_models.py | 22 ++++++++++++++ .../plugins/event_content_listing/apps.py | 7 +++++ .../event_content_listing/content_plugins.py | 17 +++++++++++ .../plugins/event_content_listing/forms.py | 21 ++++++++++++++ .../migrations/0001_initial.py | 29 +++++++++++++++++++ .../migrations/__init__.py | 0 .../plugins/event_content_listing/models.py | 8 +++++ .../plugins/event_content_listing/_event.html | 4 +++ .../event_content_listing/default.html | 9 ++++++ 10 files changed, 118 insertions(+) create mode 100644 icekit_events/plugins/event_content_listing/__init__.py create mode 100644 icekit_events/plugins/event_content_listing/abstract_models.py create mode 100644 icekit_events/plugins/event_content_listing/apps.py create mode 100644 icekit_events/plugins/event_content_listing/content_plugins.py create mode 100644 icekit_events/plugins/event_content_listing/forms.py create mode 100644 icekit_events/plugins/event_content_listing/migrations/0001_initial.py create mode 100644 icekit_events/plugins/event_content_listing/migrations/__init__.py create mode 100644 icekit_events/plugins/event_content_listing/models.py create mode 100644 icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/_event.html create mode 100644 icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html diff --git a/icekit_events/plugins/event_content_listing/__init__.py b/icekit_events/plugins/event_content_listing/__init__.py new file mode 100644 index 0000000..dd48ee4 --- /dev/null +++ b/icekit_events/plugins/event_content_listing/__init__.py @@ -0,0 +1 @@ +default_app_config = '%s.apps.AppConfig' % __name__ diff --git a/icekit_events/plugins/event_content_listing/abstract_models.py b/icekit_events/plugins/event_content_listing/abstract_models.py new file mode 100644 index 0000000..4c73306 --- /dev/null +++ b/icekit_events/plugins/event_content_listing/abstract_models.py @@ -0,0 +1,22 @@ +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + +from icekit.plugins.content_listing.abstract_models import \ + AbstractContentListingItem + + +@python_2_unicode_compatible +class AbstractEventContentListingItem(AbstractContentListingItem): + """ + An embedded listing of event content items. + """ + + class Meta: + abstract = True + verbose_name = _('Event Content Listing') + + def __str__(self): + return 'Event Content Listing of %s' % self.content_type + + def get_items(self): + return super(AbstractEventContentListingItem, self).get_items() diff --git a/icekit_events/plugins/event_content_listing/apps.py b/icekit_events/plugins/event_content_listing/apps.py new file mode 100644 index 0000000..4853fda --- /dev/null +++ b/icekit_events/plugins/event_content_listing/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + name = '.'.join(__name__.split('.')[:-1]) + label = "ik_event_listing" + verbose_name = "Event Content Listing" diff --git a/icekit_events/plugins/event_content_listing/content_plugins.py b/icekit_events/plugins/event_content_listing/content_plugins.py new file mode 100644 index 0000000..b78d122 --- /dev/null +++ b/icekit_events/plugins/event_content_listing/content_plugins.py @@ -0,0 +1,17 @@ +""" +Definition of the plugin. +""" +from django.utils.translation import ugettext_lazy as _ + +from fluent_contents.extensions import ContentPlugin, plugin_pool + +from . import forms, models + + +@plugin_pool.register +class EventContentListingPlugin(ContentPlugin): + model = models.EventContentListingItem + category = _('Assets') + render_template = 'icekit_events/plugins/event_content_listing/default.html' + form = forms.EventContentListingAdminForm + cache_output = False diff --git a/icekit_events/plugins/event_content_listing/forms.py b/icekit_events/plugins/event_content_listing/forms.py new file mode 100644 index 0000000..628bb4d --- /dev/null +++ b/icekit_events/plugins/event_content_listing/forms.py @@ -0,0 +1,21 @@ +from icekit.plugins.content_listing.forms import ContentListingAdminForm + +from icekit_events.models import EventBase + +from .models import EventContentListingItem + + +class EventContentListingAdminForm(ContentListingAdminForm): + + class Meta: + model = EventContentListingItem + fields = '__all__' + + def filter_content_types(self, content_type_qs): + """ Filter the content types selectable to only event subclasses """ + valid_ct_ids = [] + for ct in content_type_qs: + model = ct.model_class() + if model and issubclass(model, EventBase): + valid_ct_ids.append(ct.id) + return content_type_qs.filter(pk__in=valid_ct_ids) diff --git a/icekit_events/plugins/event_content_listing/migrations/0001_initial.py b/icekit_events/plugins/event_content_listing/migrations/0001_initial.py new file mode 100644 index 0000000..5eca7b0 --- /dev/null +++ b/icekit_events/plugins/event_content_listing/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 = [ + ('fluent_contents', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='EventContentListingItem', + fields=[ + ('contentitem_ptr', models.OneToOneField(serialize=False, primary_key=True, to='fluent_contents.ContentItem', parent_link=True, auto_created=True)), + ('limit', models.IntegerField(null=True, help_text=b'How many items to show? No limit is applied if this field is not set', blank=True)), + ('content_type', models.ForeignKey(help_text=b'Content type of items to show in a listing', to='contenttypes.ContentType')), + ], + options={ + 'db_table': 'contentitem_ik_event_listing_eventcontentlistingitem', + 'abstract': False, + 'verbose_name': 'Event Content Listing', + }, + bases=('fluent_contents.contentitem',), + ), + ] diff --git a/icekit_events/plugins/event_content_listing/migrations/__init__.py b/icekit_events/plugins/event_content_listing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/icekit_events/plugins/event_content_listing/models.py b/icekit_events/plugins/event_content_listing/models.py new file mode 100644 index 0000000..813a35d --- /dev/null +++ b/icekit_events/plugins/event_content_listing/models.py @@ -0,0 +1,8 @@ +from .abstract_models import AbstractEventContentListingItem + + +class EventContentListingItem(AbstractEventContentListingItem): + """ + An embedded listing of event content items. + """ + pass diff --git a/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/_event.html b/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/_event.html new file mode 100644 index 0000000..7e07654 --- /dev/null +++ b/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/_event.html @@ -0,0 +1,4 @@ +
  • + {{ event.title|safe }} +
  • + diff --git a/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html b/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html new file mode 100644 index 0000000..055e5cb --- /dev/null +++ b/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html @@ -0,0 +1,9 @@ +
    + {% for event in instance.get_items %} + {% include "icekit_events/plugins/event_content_listing/_event.html" %} + {% empty %} +
  • There are no items to show on this page
  • + {{ instance.get_default_embed }} + {% endfor %} +
    + From 62de2fb9604f9a43f8458d61a401096a47528a32 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 22 Feb 2017 12:08:40 +1100 Subject: [PATCH 2/7] Add richer filters for Event content listing plugin - filter events shown in listing by primary/secondary `EventType` - filter events shown in listing to those with occurrences within given start/end datetimes - filter events shown in listing to those with occurrences that end after N days ago, and/or start before N days hence. --- .../event_content_listing/abstract_models.py | 59 ++++++++++++++++++- .../migrations/0002_auto_20170222_1136.py | 40 +++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 icekit_events/plugins/event_content_listing/migrations/0002_auto_20170222_1136.py diff --git a/icekit_events/plugins/event_content_listing/abstract_models.py b/icekit_events/plugins/event_content_listing/abstract_models.py index 4c73306..2a0b777 100644 --- a/icekit_events/plugins/event_content_listing/abstract_models.py +++ b/icekit_events/plugins/event_content_listing/abstract_models.py @@ -1,6 +1,13 @@ +from datetime import timedelta + +from django.db import models +from django.db.models import Q from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from timezone import timezone as djtz # django-timezone + +from icekit_events.models import EventType from icekit.plugins.content_listing.abstract_models import \ AbstractContentListingItem @@ -10,6 +17,34 @@ class AbstractEventContentListingItem(AbstractContentListingItem): """ An embedded listing of event content items. """ + limit_to_types = models.ManyToManyField( + EventType, + help_text="Leave empty to show all events.", + blank=True, + db_table="ik_event_listing_types", + ) + from_date = models.DateTimeField( + blank=True, null=True, + help_text="Only show events with occurrences that end after this" + " date and time.", + ) + to_date = models.DateTimeField( + blank=True, null=True, + help_text="Only show events with occurrences that start before this" + " date and time.", + ) + from_days_ago = models.IntegerField( + blank=True, null=True, + help_text="Only show events with occurrences after this number of" + " days into the past. Set this to zero to show only events" + " with future occurrences.", + ) + to_days_ahead = models.IntegerField( + blank=True, null=True, + help_text="Only show events with occurrences before this number of" + " days into the future. Set this to zero to show only events" + " with past occurrences.", + ) class Meta: abstract = True @@ -19,4 +54,26 @@ def __str__(self): return 'Event Content Listing of %s' % self.content_type def get_items(self): - return super(AbstractEventContentListingItem, self).get_items() + qs = super(AbstractEventContentListingItem, self).get_items( + apply_limit=False) + if self.limit_to_types.count(): + types = self.limit_to_types.all() + qs = qs.filter( + Q(primary_type__in=types) | + Q(secondary_types__in=types) + ) + # Apply `from_date` and `to_date` limits + qs = qs.overlapping(self.from_date, self.to_date) + # Apply `from_days_ago` and `to_days_ahead` limits + today = djtz.now().date() + from_date = to_date = None + if self.from_days_ago is not None: + from_date = today - timedelta(days=self.from_days_ago) + if self.to_days_ahead is not None: + to_date = today + timedelta(days=self.to_days_ahead) + qs = qs.overlapping(from_date, to_date) + # Apply a sensible default ordering + qs = qs.order_by_first_occurrence() + if self.limit: + qs = qs[:self.limit] + return qs diff --git a/icekit_events/plugins/event_content_listing/migrations/0002_auto_20170222_1136.py b/icekit_events/plugins/event_content_listing/migrations/0002_auto_20170222_1136.py new file mode 100644 index 0000000..74f8baf --- /dev/null +++ b/icekit_events/plugins/event_content_listing/migrations/0002_auto_20170222_1136.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('icekit_events', '0016_auto_20161208_0030'), + ('ik_event_listing', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='eventcontentlistingitem', + name='from_date', + field=models.DateTimeField(help_text=b'Only show events with occurrences that end after this date and time.', null=True, blank=True), + ), + migrations.AddField( + model_name='eventcontentlistingitem', + name='from_days_ago', + field=models.IntegerField(help_text=b'Only show events with occurrences after this number of days into the past. Set this to zero to show only events with future occurrences.', null=True, blank=True), + ), + migrations.AddField( + model_name='eventcontentlistingitem', + name='limit_to_types', + field=models.ManyToManyField(help_text=b'Leave empty to show all events.', to='icekit_events.EventType', db_table=b'ik_event_listing_types', blank=True), + ), + migrations.AddField( + model_name='eventcontentlistingitem', + name='to_date', + field=models.DateTimeField(help_text=b'Only show events with occurrences that start before this date and time.', null=True, blank=True), + ), + migrations.AddField( + model_name='eventcontentlistingitem', + name='to_days_ahead', + field=models.IntegerField(help_text=b'Only show events with occurrences before this number of days into the future. Set this to zero to show only events with past occurrences.', null=True, blank=True), + ), + ] From 820164afb0bcad756f42bc569be9535664146a19 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 22 Feb 2017 12:19:02 +1100 Subject: [PATCH 3/7] Add configurable "no items" message for events content listing plugin --- ...ventcontentlistingitem_no_items_message.py | 19 +++++++++++++++++++ .../event_content_listing/default.html | 3 +-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 icekit_events/plugins/event_content_listing/migrations/0003_eventcontentlistingitem_no_items_message.py diff --git a/icekit_events/plugins/event_content_listing/migrations/0003_eventcontentlistingitem_no_items_message.py b/icekit_events/plugins/event_content_listing/migrations/0003_eventcontentlistingitem_no_items_message.py new file mode 100644 index 0000000..0e38ddd --- /dev/null +++ b/icekit_events/plugins/event_content_listing/migrations/0003_eventcontentlistingitem_no_items_message.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ik_event_listing', '0002_auto_20170222_1136'), + ] + + operations = [ + migrations.AddField( + model_name='eventcontentlistingitem', + name='no_items_message', + field=models.CharField(blank=True, help_text=b'Message to show if there are not items in listing.', null=True, max_length=255), + ), + ] diff --git a/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html b/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html index 055e5cb..533ae30 100644 --- a/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html +++ b/icekit_events/plugins/event_content_listing/templates/icekit_events/plugins/event_content_listing/default.html @@ -2,8 +2,7 @@ {% for event in instance.get_items %} {% include "icekit_events/plugins/event_content_listing/_event.html" %} {% empty %} -
  • There are no items to show on this page
  • - {{ instance.get_default_embed }} +
  • {{ instance.no_items_message|default:"There are no items to show" }}
  • {% endfor %} From da7d6ab927db1482c90540d723f259cd5ee37579 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 22 Feb 2017 12:46:26 +1100 Subject: [PATCH 4/7] Render occurrences for Event content listing, not events Generate and render Occurrence instances when listing events within the event content listing plugin, since the occurrences are what we really want especially for rendering them in the appropriate order. --- .../plugins/event_content_listing/abstract_models.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/icekit_events/plugins/event_content_listing/abstract_models.py b/icekit_events/plugins/event_content_listing/abstract_models.py index 2a0b777..65587ed 100644 --- a/icekit_events/plugins/event_content_listing/abstract_models.py +++ b/icekit_events/plugins/event_content_listing/abstract_models.py @@ -7,7 +7,7 @@ from timezone import timezone as djtz # django-timezone -from icekit_events.models import EventType +from icekit_events.models import EventType, Occurrence from icekit.plugins.content_listing.abstract_models import \ AbstractContentListingItem @@ -54,13 +54,12 @@ def __str__(self): return 'Event Content Listing of %s' % self.content_type def get_items(self): - qs = super(AbstractEventContentListingItem, self).get_items( - apply_limit=False) + qs = Occurrence.objects.visible().distinct() if self.limit_to_types.count(): types = self.limit_to_types.all() qs = qs.filter( - Q(primary_type__in=types) | - Q(secondary_types__in=types) + Q(event__primary_type__in=types) | + Q(event__secondary_types__in=types) ) # Apply `from_date` and `to_date` limits qs = qs.overlapping(self.from_date, self.to_date) @@ -72,8 +71,6 @@ def get_items(self): if self.to_days_ahead is not None: to_date = today + timedelta(days=self.to_days_ahead) qs = qs.overlapping(from_date, to_date) - # Apply a sensible default ordering - qs = qs.order_by_first_occurrence() if self.limit: qs = qs[:self.limit] return qs From 447dd826f7238c8ea3fdc236b682979e338d2123 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 22 Feb 2017 13:51:59 +1100 Subject: [PATCH 5/7] Pin django-dynamic-fixture to avoid test failures for polymorphic models --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 76fa79c..e506a80 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,10 @@ install_requires=[ 'Django<1.9', 'django-dynamic-fixture', + # TODO Specific version of django-dynamic-fixture is necessary to avoid + # AttributeError: can't set attribute` failures on polymorphic models. + # See https://github.com/paulocheque/django-dynamic-fixture/pull/59 + 'django-dynamic-fixture==1.9.0+0.caeb3427399edd3b0d589516993c7da55e0de560.ixc', 'django-icekit', 'django-polymorphic', 'django-polymorphic-tree', From d588215e0aef35f1343d80a8d74e3ce9342f468e Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 22 Feb 2017 13:57:28 +1100 Subject: [PATCH 6/7] Oops, removed double mention of django-dynamic-fixture --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e506a80..d5c5823 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,6 @@ include_package_data=True, install_requires=[ 'Django<1.9', - 'django-dynamic-fixture', # TODO Specific version of django-dynamic-fixture is necessary to avoid # AttributeError: can't set attribute` failures on polymorphic models. # See https://github.com/paulocheque/django-dynamic-fixture/pull/59 From 2e1be817622ff5f3d127c53e09a8c9fb1cc12dfb Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 28 Feb 2017 16:08:41 +1100 Subject: [PATCH 7/7] Add TODO's for improving admin experience for Event Content Listing --- icekit_events/plugins/event_content_listing/forms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/icekit_events/plugins/event_content_listing/forms.py b/icekit_events/plugins/event_content_listing/forms.py index 628bb4d..c15a71e 100644 --- a/icekit_events/plugins/event_content_listing/forms.py +++ b/icekit_events/plugins/event_content_listing/forms.py @@ -6,6 +6,10 @@ class EventContentListingAdminForm(ContentListingAdminForm): + # TODO Improve admin experience: + # - horizontal filter for `limit_to_types` choice + # - verbose_name for Content Type + # - default (required) value for No Items Message. class Meta: model = EventContentListingItem