issue#1: synchronise automatically station and programs from db. For station generate a set of usual desired pages.

This commit is contained in:
bkfox 2016-10-12 23:37:14 +02:00
parent 8ec3693b39
commit f58f3352f2
13 changed files with 290 additions and 127 deletions

View File

@ -82,7 +82,7 @@ class ProgramAdmin(NameableAdmin):
schedule.short_description = _("Schedule") schedule.short_description = _("Schedule")
list_display = ('id', 'name', 'active', 'schedule') list_display = ('id', 'name', 'active', 'schedule')
fields = NameableAdmin.fields + [ 'active' ] fields = NameableAdmin.fields + [ 'active', 'station' ]
# TODO list_display # TODO list_display
inlines = [ ScheduleInline, StreamInline ] inlines = [ ScheduleInline, StreamInline ]

View File

@ -292,7 +292,7 @@ class Program(Nameable):
active = models.BooleanField( active = models.BooleanField(
_('active'), _('active'),
default = True, 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 @property

View File

@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError
from honeypot.decorators import verify_honeypot_value from honeypot.decorators import verify_honeypot_value
import aircox.cms.models as models import aircox_cms.models as models
class CommentForm(forms.ModelForm): class CommentForm(forms.ModelForm):

View File

@ -41,7 +41,7 @@ class Command (BaseCommand):
# programs # programs
logger.info('Programs...') logger.info('Programs...')
parent = settings.default_program_parent_page parent = settings.default_programs_page
qs = Program.objects.filter( qs = Program.objects.filter(
active = True, active = True,
stream__isnull = True, stream__isnull = True,

View File

@ -33,11 +33,22 @@ import aircox_cms.settings as settings
from aircox_cms.utils import image_url from aircox_cms.utils import image_url
from aircox_cms.sections import * from aircox_cms.sections import *
import aircox_cms.signals
@register_setting @register_setting
class WebsiteSettings(BaseSetting): class WebsiteSettings(BaseSetting):
# TODO: #Station assign a website to a aircox.models.model.station when it will station = models.OneToOneField(
# exist. Update all dependent code such as signal handling 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 # general website information
favicon = models.ImageField( favicon = models.ImageField(
@ -58,11 +69,12 @@ class WebsiteSettings(BaseSetting):
help_text = _('public description of the website; used for referencing'), help_text = _('public description of the website; used for referencing'),
) )
list_page = models.ForeignKey( list_page = models.ForeignKey(
'aircox_cms.GenericPage', 'aircox_cms.DynamicListPage',
verbose_name = _('page for lists'), verbose_name = _('page for lists'),
help_text=_('page used to display the results of a search and other ' help_text=_('page used to display the results of a search and other '
'lists'), 'lists'),
related_name= 'list_page' related_name= 'list_page',
blank = True, null = True,
) )
# comments # comments
accept_comments = models.BooleanField( accept_comments = models.BooleanField(
@ -76,36 +88,50 @@ class WebsiteSettings(BaseSetting):
comment_success_message = models.TextField( comment_success_message = models.TextField(
_('success message'), _('success message'),
default = _('Your comment has been successfully posted!'), 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( comment_wait_message = models.TextField(
_('waiting message'), _('waiting message'),
default = _('Your comment is awaiting for approval.'), 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( comment_error_message = models.TextField(
_('error message'), _('error message'),
default = _('We could not save your message. Please correct the error(s) below.'), 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( sync = models.BooleanField(
_('automatic publications'), _('synchronize with Aircox'),
default = False, default = False,
help_text = _( help_text = _(
'Create automatically new publications for new programs and ' 'create publication for each object added to an Aircox\'s '
'diffusions in the timetable. If set, please complete other ' 'station; for example when there is a new program, or '
'options of this panel' '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, Page,
verbose_name = _('default program parent page'), verbose_name = _('default programs page'),
blank = True, null = True, blank = True, null = True,
help_text = _( help_text = _(
'Default parent page for program\'s pages. It is used to assign ' 'when a new program is saved and a publication is created, '
'a page to the publication of a newly created program and can ' 'put this publication as a child of this page. If no page '
'be changed later' '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: { limit_choices_to = lambda: {
'show_in_menus': True, 'show_in_menus': True,
@ -119,7 +145,7 @@ class WebsiteSettings(BaseSetting):
FieldPanel('tags'), FieldPanel('tags'),
FieldPanel('description'), FieldPanel('description'),
FieldPanel('list_page'), FieldPanel('list_page'),
], heading=_('promotion')), ], heading=_('Promotion')),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('allow_comments'), FieldPanel('allow_comments'),
FieldPanel('accept_comments'), FieldPanel('accept_comments'),
@ -128,8 +154,8 @@ class WebsiteSettings(BaseSetting):
FieldPanel('comment_error_message'), FieldPanel('comment_error_message'),
], heading = _('Comments')), ], heading = _('Comments')),
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('auto_create'), FieldPanel('sync'),
FieldPanel('default_program_parent_page'), FieldPanel('default_programs_page'),
], heading = _('Programs and controls')), ], heading = _('Programs and controls')),
] ]
@ -559,44 +585,36 @@ 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. Displays a list of publications using query passed by the url.
Look at get_queryset for more information. 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 <title></title>
body = RichTextField( body = RichTextField(
_('body'), _('body'),
blank = True, null = True, blank = True, null = True,
help_text = _('add an extra description for this list') 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 = [ content_panels = [
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('title'), FieldPanel('title'),
FieldPanel('body'), FieldPanel('body'),
FieldPanel('list_from_request'),
], heading=_('Content')) ], heading=_('Content'))
] ]
class Meta: class Meta:
verbose_name = _('Generic Page') verbose_name = _('Dynamic List Page')
verbose_name_plural = _('Generic Page') verbose_name_plural = _('Dynamic List Pages')
def get_context(self, request, *args, **kwargs): def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs) context = super().get_context(request, *args, **kwargs)
if self.list_from_request:
qs = ListBase.from_request(request, context=context) qs = ListBase.from_request(request, context=context)
context['object_list'] = qs context['object_list'] = qs
return context return context
@ -647,6 +665,7 @@ class DatedListPage(DatedListBase,Page):
class LogsPage(DatedListPage): class LogsPage(DatedListPage):
template = 'aircox_cms/dated_list_page.html' template = 'aircox_cms/dated_list_page.html'
# TODO: make it a property that automatically select the station
station = models.ForeignKey( station = models.ForeignKey(
aircox.models.Station, aircox.models.Station,
verbose_name = _('station'), verbose_name = _('station'),

View File

@ -176,8 +176,10 @@ class ListBase(models.Model):
siblings = models.BooleanField( siblings = models.BooleanField(
verbose_name = _('select siblings of related'), verbose_name = _('select siblings of related'),
default = False, default = False,
help_text = _('if selected select related publications that are ' help_text = _(
'siblings of this one'), 'if checked, related publications are siblings instead of '
'the children.'
),
) )
asc = models.BooleanField( asc = models.BooleanField(
verbose_name = _('ascending order'), verbose_name = _('ascending order'),
@ -200,7 +202,6 @@ class ListBase(models.Model):
], heading=_('sorting')) ], heading=_('sorting'))
] ]
def get_queryset(self): def get_queryset(self):
""" """
Get queryset based on the arguments. This class is intended to be Get queryset based on the arguments. This class is intended to be
@ -223,8 +224,8 @@ class ListBase(models.Model):
else: else:
qs = qs.descendant_of(related) qs = qs.descendant_of(related)
date = self.related.date if hasattr(related, 'date') else \ date = related.date if hasattr(related, 'date') else \
self.related.first_published_at related.first_published_at
if self.date_filter == self.DateFilter.before_related: if self.date_filter == self.DateFilter.before_related:
qs = qs.filter(date__lt = date) qs = qs.filter(date__lt = date)
elif self.date_filter == self.DateFilter.after_related: 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. 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 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 import aircox_cms.models as models
@ -261,7 +262,7 @@ class ListBase(models.Model):
params.update(kwargs) params.update(kwargs)
page = params.get('related') or list_page or \ page = params.get('related') or list_page or \
models.GenericPage.objects.all().first() models.DynamicListPage.objects.all().first()
if params.get('related'): if params.get('related'):
params['related'] = True params['related'] = True
@ -483,9 +484,6 @@ class Section(ClusterableModel):
InlinePanel('places', label=_('Section Items')), InlinePanel('places', label=_('Section Items')),
] ]
def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.name or self.pk)
@classmethod @classmethod
def get_sections_at (cl, position, page = None): def get_sections_at (cl, position, page = None):
""" """
@ -503,22 +501,40 @@ class Section(ClusterableModel):
) )
return qs 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): def render(self, request, page = None, context = None, *args, **kwargs):
return ''.join([ return ''.join([
place.item.specific.render(request, page, context, *args, **kwargs) place.item.specific.render(request, page, context, *args, **kwargs)
for place in self.places.all() for place in self.places.all()
]) ])
def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.name or self.pk)
class SectionPlace(Orderable): class SectionPlace(Orderable):
section = ParentalKey(Section, related_name='places') section = ParentalKey(Section, related_name='places')
item = models.ForeignKey( item = models.ForeignKey(
'SectionItem', 'SectionItem',
verbose_name=_('item') verbose_name=_('item'),
) )
panels = [ SnippetChooserPanel('item'), ] panels = [ SnippetChooserPanel('item'), ]
class Meta:
unique_together = ('section', 'item')
def __str__(self): def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.title or self.pk) return '{}: {}'.format(self.__class__.__name__, self.title or self.pk)
@ -954,7 +970,8 @@ class SectionSearchField(SectionItem):
] ]
def get_context(self, request, page): 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) context = super().get_context(request, page)
return context return context

View File

@ -1,13 +1,134 @@
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver 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 <meta> 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) @receiver(post_save, sender=aircox.Program)
def on_new_program(sender, instance, created, *args): def program_post_saved(sender, instance, created, *args, **kwargs):
import aircox_cms.models as models import aircox_cms.models as models
if not created or instance.page.count(): if not created or instance.page.count():
return 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)

View File

@ -12,8 +12,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="application-name" content="aircox-cms"> <meta name="application-name" content="aircox-cms">
<meta name="description" content="{{ settings.cms.WebsiteSettings.description }}"> <meta name="description" content="{{ settings.aircox_cms.WebsiteSettings.description }}">
<meta name="keywords" content="{{ page.tags.all|default:settings.cms.WebsiteSettings.tags }}"> <meta name="keywords" content="{{ page.tags.all|default:settings.aircox_cms.WebsiteSettings.tags }}">
{% with favicon=settings.cms.WebsiteSettings.favicon %} {% with favicon=settings.cms.WebsiteSettings.favicon %}
<link rel="icon" href="{{ favicon.url }}" /> <link rel="icon" href="{{ favicon.url }}" />

View File

@ -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 %}
<h1>
{# 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 <i>{{ terms }}</i>{% 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 <a href="{{ url }}">{{ title }}</a>{% endblocktrans %}
{% endwith %}
{% else %}
{% blocktrans %}All the publications{% endblocktrans %}
{% endif %}
{% endwith %}
{% endif %}
</h1>
{% endblock %}
{% block content %}
{% with related=list_selector.filter_related %}
{% if related %}
<div class="body summary">
{% image related.cover fill-128x128 class="cover item_cover" %}
{{ related.summary }}
<a href="{{ related.url }}">{% trans "More about it" %}</a>
</div>
{% elif page.body %}
<div class="body">
{{ page.body|richtext }}
</div>
{% endif %}
{% endwith %}
{% with list_paginator=paginator %}
{% include "aircox_cms/snippets/list.html" %}
{% endwith %}
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -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 %}
<h1>
{# 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 <i>{{ terms }}</i>{% 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 <a href="{{ url }}">{{ title }}</a>{% endblocktrans %}
{% endwith %}
{% else %}
{% blocktrans %}All the publications{% endblocktrans %}
{% endif %}
{% endwith %}
{% else %}
{{ page.title }}
{% endif %}
</h1>
{% endblock %}
{% block content %}
{% if page.list_from_request %}
{% with related=list_selector.filter_related %}
{% if related %}
<div class="body summary">
{% image related.cover fill-128x128 class="cover item_cover" %}
{{ related.summary }}
<a href="{{ related.url }}">{% trans "More about it" %}</a>
</div>
{% elif page.body %}
<div class="body">
{{ page.body|richtext }}
</div>
{% endif %}
{% endwith %}
{% with list_paginator=paginator %}
{% include "aircox_cms/snippets/list.html" %}
{% endwith %}
{% else %}
<div class="body">
{{ page.body|richtext }}
</div>
{% endif %}
{% endblock %}

View File

@ -57,4 +57,3 @@
{% endblock %} {% endblock %}

View File

@ -2,9 +2,20 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from wagtail.wagtailimages.utils import generate_signature from wagtail.wagtailimages.utils import generate_signature
def image_url(image, filter_spec): def image_url(image, filter_spec):
signature = generate_signature(image.id, filter_spec) signature = generate_signature(image.id, filter_spec)
url = reverse('wagtailimages_serve', args=(signature, image.id, filter_spec)) url = reverse('wagtailimages_serve', args=(signature, image.id, filter_spec))
url += image.file.name[len('original_images/'):] url += image.file.name[len('original_images/'):]
return url 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

View File

@ -18,13 +18,16 @@ This file is used as a reminder, can be used as crappy documentation too.
# Long term TODO # To discuss / To think
- debug/prod configuration - 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: programs:
- sounds monitor: max_size of path, take in account - sounds monitor: max_size of path, take in account
controllers:
- archives can be set afterwards for rerun, so check must be done - archives can be set afterwards for rerun, so check must be done
at the same time we monitor at the same time we monitor
- logs: archive functionnality - logs: archive functionnality