From f58f3352f2d2c4c11ee5a5364290b5395e20d7cd Mon Sep 17 00:00:00 2001 From: bkfox Date: Wed, 12 Oct 2016 23:37:14 +0200 Subject: [PATCH] issue#1: synchronise automatically station and programs from db. For station generate a set of usual desired pages. --- aircox/admin.py | 2 +- aircox/models.py | 2 +- aircox_cms/forms.py | 2 +- .../management/commands/programs_to_cms.py | 2 +- aircox_cms/models.py | 99 ++++++++------ aircox_cms/sections.py | 41 ++++-- aircox_cms/signals.py | 127 +++++++++++++++++- .../templates/aircox_cms/base_site.html | 4 +- .../aircox_cms/dynamic_list_page.html | 54 ++++++++ .../templates/aircox_cms/generic_page.html | 61 --------- .../templates/aircox_cms/publication.html | 1 - aircox_cms/utils.py | 11 ++ notes.md | 11 +- 13 files changed, 290 insertions(+), 127 deletions(-) create mode 100644 aircox_cms/templates/aircox_cms/dynamic_list_page.html delete mode 100644 aircox_cms/templates/aircox_cms/generic_page.html diff --git a/aircox/admin.py b/aircox/admin.py index 09b71bd..e763b7a 100755 --- a/aircox/admin.py +++ b/aircox/admin.py @@ -82,7 +82,7 @@ class ProgramAdmin(NameableAdmin): schedule.short_description = _("Schedule") list_display = ('id', 'name', 'active', 'schedule') - fields = NameableAdmin.fields + [ 'active' ] + fields = NameableAdmin.fields + [ 'active', 'station' ] # TODO list_display inlines = [ ScheduleInline, StreamInline ] diff --git a/aircox/models.py b/aircox/models.py index e981fed..66cfc49 100755 --- a/aircox/models.py +++ b/aircox/models.py @@ -292,7 +292,7 @@ class Program(Nameable): active = models.BooleanField( _('active'), default = True, - help_text = _('if not set this program is no longer active') + help_text = _('if not checked this program is no longer active') ) @property diff --git a/aircox_cms/forms.py b/aircox_cms/forms.py index 69c6ae0..e5cbdbf 100644 --- a/aircox_cms/forms.py +++ b/aircox_cms/forms.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from honeypot.decorators import verify_honeypot_value -import aircox.cms.models as models +import aircox_cms.models as models class CommentForm(forms.ModelForm): diff --git a/aircox_cms/management/commands/programs_to_cms.py b/aircox_cms/management/commands/programs_to_cms.py index d576793..3ef1a58 100644 --- a/aircox_cms/management/commands/programs_to_cms.py +++ b/aircox_cms/management/commands/programs_to_cms.py @@ -41,7 +41,7 @@ class Command (BaseCommand): # programs logger.info('Programs...') - parent = settings.default_program_parent_page + parent = settings.default_programs_page qs = Program.objects.filter( active = True, stream__isnull = True, diff --git a/aircox_cms/models.py b/aircox_cms/models.py index a4fe605..c8dd9a4 100644 --- a/aircox_cms/models.py +++ b/aircox_cms/models.py @@ -33,11 +33,22 @@ import aircox_cms.settings as settings from aircox_cms.utils import image_url from aircox_cms.sections import * +import aircox_cms.signals + @register_setting class WebsiteSettings(BaseSetting): - # TODO: #Station assign a website to a aircox.models.model.station when it will - # exist. Update all dependent code such as signal handling + station = models.OneToOneField( + aircox.models.Station, + verbose_name = _('aircox station'), + related_name = 'website_settings', + unique = True, + blank = True, null = True, + help_text = _( + 'refers to an Aircox\'s station; it is used to make the link ' + 'between the website and Aircox' + ), + ) # general website information favicon = models.ImageField( @@ -58,11 +69,12 @@ class WebsiteSettings(BaseSetting): help_text = _('public description of the website; used for referencing'), ) list_page = models.ForeignKey( - 'aircox_cms.GenericPage', + 'aircox_cms.DynamicListPage', verbose_name = _('page for lists'), help_text=_('page used to display the results of a search and other ' 'lists'), - related_name= 'list_page' + related_name= 'list_page', + blank = True, null = True, ) # comments accept_comments = models.BooleanField( @@ -76,36 +88,50 @@ class WebsiteSettings(BaseSetting): comment_success_message = models.TextField( _('success message'), default = _('Your comment has been successfully posted!'), - help_text = _('message to display when a post has been posted'), + help_text = _( + 'message displayed when a comment has been successfully posted' + ), ) comment_wait_message = models.TextField( _('waiting message'), default = _('Your comment is awaiting for approval.'), - help_text = _('message to display when a post waits to be reviewed'), + help_text = _( + 'message displayed when a comment has been sent, but waits for ' + ' website administrators\' approval.' + ), ) comment_error_message = models.TextField( _('error message'), default = _('We could not save your message. Please correct the error(s) below.'), - help_text = _('message to display there is an error an incomplete form.'), + help_text = _( + 'message displayed when the form of the comment has been ' + ' submitted but there is an error, such as an incomplete field' + ), ) - auto_create = models.BooleanField( - _('automatic publications'), + sync = models.BooleanField( + _('synchronize with Aircox'), default = False, help_text = _( - 'Create automatically new publications for new programs and ' - 'diffusions in the timetable. If set, please complete other ' - 'options of this panel' + 'create publication for each object added to an Aircox\'s ' + 'station; for example when there is a new program, or ' + 'when a diffusion has been added to the timetable. Note: ' + 'it does not concern the Station themselves.' + # /doc/ the page is saved but not pubished -- this must be + # done manually, when the user edit it. ) ) - default_program_parent_page = ParentalKey( + default_programs_page = ParentalKey( Page, - verbose_name = _('default program parent page'), + verbose_name = _('default programs page'), blank = True, null = True, help_text = _( - 'Default parent page for program\'s pages. It is used to assign ' - 'a page to the publication of a newly created program and can ' - 'be changed later' + 'when a new program is saved and a publication is created, ' + 'put this publication as a child of this page. If no page ' + 'has been specified, try to put it as the child of the ' + 'website\'s root page (otherwise, do not create the page).' + # /doc/ (technicians, admin): if the page has not been created, + # it still can be created using the `programs_to_cms` command. ), limit_choices_to = lambda: { 'show_in_menus': True, @@ -119,7 +145,7 @@ class WebsiteSettings(BaseSetting): FieldPanel('tags'), FieldPanel('description'), FieldPanel('list_page'), - ], heading=_('promotion')), + ], heading=_('Promotion')), MultiFieldPanel([ FieldPanel('allow_comments'), FieldPanel('accept_comments'), @@ -128,8 +154,8 @@ class WebsiteSettings(BaseSetting): FieldPanel('comment_error_message'), ], heading = _('Comments')), MultiFieldPanel([ - FieldPanel('auto_create'), - FieldPanel('default_program_parent_page'), + FieldPanel('sync'), + FieldPanel('default_programs_page'), ], heading = _('Programs and controls')), ] @@ -559,46 +585,38 @@ class DiffusionPage(Publication): # -# Other type of pages +# Others types of pages # -class GenericPage(Page): +class DynamicListPage(Page): """ - Page for simple lists, query is done though request' GET fields. - Look at get_queryset for more information. + Displays a list of publications using query passed by the url. + This can be used for search/tags page, and generally only one + page is used per website. + + If a title is given, use it instead of the generated one. """ + # FIXME/TODO: title in template body = RichTextField( _('body'), blank = True, null = True, help_text = _('add an extra description for this list') ) - list_from_request = models.BooleanField( - _('list from the request'), - default = False, - help_text = _( - 'if set, the page print a list based on the request made by ' - 'the website visitor, and its title will be adapted to this ' - 'request. Can be usefull for search pages, etc. and should ' - 'only be set on one page.' - ) - ) content_panels = [ MultiFieldPanel([ FieldPanel('title'), FieldPanel('body'), - FieldPanel('list_from_request'), ], heading=_('Content')) ] class Meta: - verbose_name = _('Generic Page') - verbose_name_plural = _('Generic Page') + verbose_name = _('Dynamic List Page') + verbose_name_plural = _('Dynamic List Pages') def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) - if self.list_from_request: - qs = ListBase.from_request(request, context=context) - context['object_list'] = qs + qs = ListBase.from_request(request, context=context) + context['object_list'] = qs return context @@ -647,6 +665,7 @@ class DatedListPage(DatedListBase,Page): class LogsPage(DatedListPage): template = 'aircox_cms/dated_list_page.html' + # TODO: make it a property that automatically select the station station = models.ForeignKey( aircox.models.Station, verbose_name = _('station'), diff --git a/aircox_cms/sections.py b/aircox_cms/sections.py index 46faa99..ce04ef4 100644 --- a/aircox_cms/sections.py +++ b/aircox_cms/sections.py @@ -176,8 +176,10 @@ class ListBase(models.Model): siblings = models.BooleanField( verbose_name = _('select siblings of related'), default = False, - help_text = _('if selected select related publications that are ' - 'siblings of this one'), + help_text = _( + 'if checked, related publications are siblings instead of ' + 'the children.' + ), ) asc = models.BooleanField( verbose_name = _('ascending order'), @@ -200,7 +202,6 @@ class ListBase(models.Model): ], heading=_('sorting')) ] - def get_queryset(self): """ Get queryset based on the arguments. This class is intended to be @@ -223,8 +224,8 @@ class ListBase(models.Model): else: qs = qs.descendant_of(related) - date = self.related.date if hasattr(related, 'date') else \ - self.related.first_published_at + date = related.date if hasattr(related, 'date') else \ + related.first_published_at if self.date_filter == self.DateFilter.before_related: qs = qs.filter(date__lt = date) elif self.date_filter == self.DateFilter.after_related: @@ -247,7 +248,7 @@ class ListBase(models.Model): to override values of self or add some to the parameters. If there is related field use it to get the page, otherwise use the - given list_page or the first GenericPage it finds. + given list_page or the first DynamicListPage it finds. """ import aircox_cms.models as models @@ -261,7 +262,7 @@ class ListBase(models.Model): params.update(kwargs) page = params.get('related') or list_page or \ - models.GenericPage.objects.all().first() + models.DynamicListPage.objects.all().first() if params.get('related'): params['related'] = True @@ -483,9 +484,6 @@ class Section(ClusterableModel): InlinePanel('places', label=_('Section Items')), ] - def __str__(self): - return '{}: {}'.format(self.__class__.__name__, self.name or self.pk) - @classmethod def get_sections_at (cl, position, page = None): """ @@ -503,22 +501,40 @@ class Section(ClusterableModel): ) return qs + def add_item(self, item): + """ + Add an item to the section. Automatically save the item and + create the corresponding SectionPlace. + """ + # TODO: arbitrary position; Orderable.sort_order? + item.save() + place, created = SectionPlace.objects.get_or_create( + section = self, + item = item, + ) + def render(self, request, page = None, context = None, *args, **kwargs): return ''.join([ place.item.specific.render(request, page, context, *args, **kwargs) for place in self.places.all() ]) + def __str__(self): + return '{}: {}'.format(self.__class__.__name__, self.name or self.pk) + class SectionPlace(Orderable): section = ParentalKey(Section, related_name='places') item = models.ForeignKey( 'SectionItem', - verbose_name=_('item') + verbose_name=_('item'), ) panels = [ SnippetChooserPanel('item'), ] + class Meta: + unique_together = ('section', 'item') + def __str__(self): return '{}: {}'.format(self.__class__.__name__, self.title or self.pk) @@ -954,7 +970,8 @@ class SectionSearchField(SectionItem): ] def get_context(self, request, page): - from aircox_cms.models import GenericPage + # FIXME ????? + from aircox_cms.models import DynamicListPage context = super().get_context(request, page) return context diff --git a/aircox_cms/signals.py b/aircox_cms/signals.py index fcbf3b9..49bc113 100644 --- a/aircox_cms/signals.py +++ b/aircox_cms/signals.py @@ -1,13 +1,134 @@ from django.db.models.signals import post_save from django.dispatch import receiver +from django.utils.translation import ugettext as _, ugettext_lazy +from django.contrib.contenttypes.models import ContentType -import aircox.models +from wagtail.wagtailcore.models import Page, Site + +import aircox.models as aircox +import aircox_cms.utils as utils + +# on a new diffusion + +@receiver(post_save, sender=aircox.Station) +def station_post_saved(sender, instance, created, *args, **kwargs): + """ + Create the basis for the website: set up settings and pages + that are common. + """ + from aircox_cms.models import \ + WebsiteSettings, Publication, ProgramPage, \ + TimetablePage, DynamicListPage, LogsPage + import aircox_cms.sections as sections + if not created: + return + + root_page = Page.objects.get(id=1) + + homepage = Publication( + title = instance.name, + slug = instance.slug, + body = _( + 'If you see this page, then Aircox is running for the station ' + '{station.name}. You might want to change it to a better one. ' + ), + ) + root_page.add_child(instance=homepage) + + site = Site( + # /doc/ when a Station is created, a wagtail Site is generated with + # default options. User must set the correct localhost afterwards + hostname = instance.slug + ".local", + port = 80, + site_name = instance.name.capitalize(), + root_page = homepage, + ) + site.save() + + # settings + website_settings = WebsiteSettings( + site = site, + station = instance, + description = _("The website of the {name} radio").format( + name = instance.name + ), + # Translators: tags set by default in description of the website + tags = _('radio,{station.name}').format(station = instance) + ) + + # timetable + timetable = TimetablePage( + title = _('Timetable'), + ) + homepage.add_child(instance = timetable) + + # list page (search, terms) + list_page = DynamicListPage( + # title is dynamic: no need to specify + title = _('Search'), + ) + homepage.add_child(instance = list_page) + website_settings.list_page = list_page + + # programs' page: list of programs in a section + programs = Publication( + title = _('Programs'), + ) + homepage.add_child(instance = programs) + + section = sections.Section( + name = _('Programs List'), + position = 'post_content', + page = programs, + ) + section.save(); + section.add_item(sections.SectionList( + count = 15, + url_text = _('All programs'), + model = ContentType.objects.get_for_model(ProgramPage), + related = programs, + )) + + website_settings.default_programs_page = programs + website_settings.sync = True + + # logs (because it is a cool feature) + logs = LogsPage( + title = _('Previously on air'), + station = instance, + ) + homepage.add_child(instance = logs) + + # save + site.save() + website_settings.save() -@receiver(post_save, sender=programs.Program) -def on_new_program(sender, instance, created, *args): +@receiver(post_save, sender=aircox.Program) +def program_post_saved(sender, instance, created, *args, **kwargs): import aircox_cms.models as models if not created or instance.page.count(): return + settings = utils.get_station_settings(instance.station) + if not settings or not settings.sync: + return + + parent = settings.default_programs_page or \ + settings.site.root_page + if not parent: + return + + page = models.ProgramPage( + program = instance, + title = instance.name, + live = False, + # Translators: default content of a page for program + body = _('{program.name} is a program on {station.name}.').format( + program = instance, + station = instance.station + ) + ) + parent.add_child(instance = page) + diff --git a/aircox_cms/templates/aircox_cms/base_site.html b/aircox_cms/templates/aircox_cms/base_site.html index 5299db2..5a5f8f6 100644 --- a/aircox_cms/templates/aircox_cms/base_site.html +++ b/aircox_cms/templates/aircox_cms/base_site.html @@ -12,8 +12,8 @@ - - + + {% with favicon=settings.cms.WebsiteSettings.favicon %} diff --git a/aircox_cms/templates/aircox_cms/dynamic_list_page.html b/aircox_cms/templates/aircox_cms/dynamic_list_page.html new file mode 100644 index 0000000..17c28dd --- /dev/null +++ b/aircox_cms/templates/aircox_cms/dynamic_list_page.html @@ -0,0 +1,54 @@ +{% extends "aircox_cms/base_site.html" %} +{# generic page to display list of articles #} + +{% load i18n %} +{% load wagtailcore_tags %} +{% load wagtailimages_tags %} + + +{% block title %} +

+{# Translators: titles for the page that shows a list of elements. #} +{# Translators: terms are search terms, or tag tarms. url: url to the page #} +{% with terms=list_selector.terms %} +{% if terms %} +{% blocktrans %}Search in publications for {{ terms }}{% endblocktrans %} +{% elif list_selector.filter_related %} +{# should never happen #} +{% with title=list_selector.filter_related.title url=list_selector.filter_related.url %} +{% blocktrans %} + Related to {{ title }}{% endblocktrans %} +{% endwith %} +{% else %} +{% blocktrans %}All the publications{% endblocktrans %} +{% endif %} +{% endwith %} +{% endif %} +

+{% endblock %} + + +{% block content %} +{% with related=list_selector.filter_related %} + {% if related %} +
+ {% image related.cover fill-128x128 class="cover item_cover" %} + {{ related.summary }} + {% trans "More about it" %} +
+ {% elif page.body %} +
+ {{ page.body|richtext }} +
+ {% endif %} + {% endwith %} + + {% with list_paginator=paginator %} + {% include "aircox_cms/snippets/list.html" %} + {% endwith %} + {% endif %} +{% endwith %} +{% endblock %} + + + diff --git a/aircox_cms/templates/aircox_cms/generic_page.html b/aircox_cms/templates/aircox_cms/generic_page.html deleted file mode 100644 index e78f50e..0000000 --- a/aircox_cms/templates/aircox_cms/generic_page.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "aircox_cms/base_site.html" %} -{# generic page to display list of articles #} - -{% load i18n %} -{% load wagtailcore_tags %} -{% load wagtailimages_tags %} - - -{% block title %} -

-{# Translators: titles for the page that shows a list of elements. #} -{# Translators: terms are search terms, or tag tarms. url: url to the page #} -{% if page.list_from_request %} - {% with terms=list_selector.terms %} - {% if terms %} - {% blocktrans %}Search in publications for {{ terms }}{% endblocktrans %} - {% elif list_selector.filter_related %} - {# should never happen #} - {% with title=list_selector.filter_related.title url=list_selector.filter_related.url %} - {% blocktrans %} - Related to {{ title }}{% endblocktrans %} - {% endwith %} - {% else %} - {% blocktrans %}All the publications{% endblocktrans %} - {% endif %} - {% endwith %} -{% else %} -{{ page.title }} -{% endif %} -

-{% endblock %} - - -{% block content %} -{% if page.list_from_request %} - {% with related=list_selector.filter_related %} - {% if related %} -
- {% image related.cover fill-128x128 class="cover item_cover" %} - {{ related.summary }} - {% trans "More about it" %} -
- {% elif page.body %} -
- {{ page.body|richtext }} -
- {% endif %} - {% endwith %} - - {% with list_paginator=paginator %} - {% include "aircox_cms/snippets/list.html" %} - {% endwith %} -{% else %} -
- {{ page.body|richtext }} -
-{% endif %} -{% endblock %} - - - diff --git a/aircox_cms/templates/aircox_cms/publication.html b/aircox_cms/templates/aircox_cms/publication.html index ef2bd10..14e7d94 100644 --- a/aircox_cms/templates/aircox_cms/publication.html +++ b/aircox_cms/templates/aircox_cms/publication.html @@ -57,4 +57,3 @@ {% endblock %} - diff --git a/aircox_cms/utils.py b/aircox_cms/utils.py index 361289d..4c9a474 100644 --- a/aircox_cms/utils.py +++ b/aircox_cms/utils.py @@ -2,9 +2,20 @@ from django.core.urlresolvers import reverse from wagtail.wagtailimages.utils import generate_signature + def image_url(image, filter_spec): signature = generate_signature(image.id, filter_spec) url = reverse('wagtailimages_serve', args=(signature, image.id, filter_spec)) url += image.file.name[len('original_images/'):] return url +def get_station_settings(station): + import aircox_cms.models as models + return models.WebsiteSettings.objects \ + .filter(station = station).first() + +def get_station_site(station): + settings = get_station_settings(station) + return settings and settings.site + + diff --git a/notes.md b/notes.md index 07a4766..ecee3c6 100644 --- a/notes.md +++ b/notes.md @@ -18,13 +18,16 @@ This file is used as a reminder, can be used as crappy documentation too. -# Long term TODO -- debug/prod configuration +# To discuss / To think +- aircox_cms.signals: handle renaming of the program if the article's title has + not been changed -> need to cache of the title at __init__ +- ensure per station website for all generated publications +- aircox_cms: remove "station" fields when it is possible in the pages & sections + +# Long term TODO programs: - sounds monitor: max_size of path, take in account - -controllers: - archives can be set afterwards for rerun, so check must be done at the same time we monitor - logs: archive functionnality