merge aircox and aircox_instance

This commit is contained in:
bkfox
2016-10-10 15:04:15 +02:00
parent 72fd7bd490
commit 191d337c3f
100 changed files with 4686 additions and 360 deletions

0
aircox_cms/__init__.py Normal file
View File

3
aircox_cms/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
aircox_cms/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CmsConfig(AppConfig):
name = 'cms'

44
aircox_cms/forms.py Normal file
View File

@ -0,0 +1,44 @@
import django.forms as forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.core.exceptions import ValidationError
from honeypot.decorators import verify_honeypot_value
import aircox.cms.models as models
class CommentForm(forms.ModelForm):
class Meta:
model = models.Comment
fields = ['author', 'email', 'url', 'content']
localized_fields = '__all__'
widgets = {
'author': forms.TextInput(attrs={
'placeholder': _('your name'),
}),
'email': forms.TextInput(attrs={
'placeholder': _('your email (optional)'),
}),
'url': forms.URLInput(attrs={
'placeholder': _('your website (optional)'),
}),
'comment': forms.TextInput(attrs={
'placeholder': _('your comment'),
})
}
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
self.page = kwargs.pop('object', None)
super().__init__(*args, **kwargs)
def clean(self):
super().clean()
if self.request:
if verify_honeypot_value(self.request, 'hp_website'):
raise ValidationError(_('You are a bot, that is not cool'))
if not self.object:
raise ValidationError(_('No publication found for this comment'))

View File

@ -0,0 +1,821 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-10 17:54+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: aircox_cms/forms.py:17
msgid "your name"
msgstr ""
#: aircox_cms/forms.py:20
msgid "your email (optional)"
msgstr ""
#: aircox_cms/forms.py:23
msgid "your website (optional)"
msgstr ""
#: aircox_cms/forms.py:26
msgid "your comment"
msgstr ""
#: aircox_cms/forms.py:39
msgid "You are a bot, that is not cool"
msgstr ""
#: aircox_cms/forms.py:42
msgid "No publication found for this comment"
msgstr ""
#: aircox_cms/models.py:44
msgid "favicon"
msgstr ""
#: aircox_cms/models.py:46
msgid "small logo for the website displayed in the browser"
msgstr ""
#: aircox_cms/models.py:49 aircox_cms/models.py:249
msgid "tags"
msgstr ""
#: aircox_cms/models.py:52
msgid "tags describing the website; used for referencing"
msgstr ""
#: aircox_cms/models.py:55
msgid "public description"
msgstr ""
#: aircox_cms/models.py:58
msgid "public description of the website; used for referencing"
msgstr ""
#: aircox_cms/models.py:62
msgid "page for lists"
msgstr ""
#: aircox_cms/models.py:63
msgid "page used to display the results of a search and other lists"
msgstr ""
#: aircox_cms/models.py:70 aircox_cms/models.py:74
msgid "publish comments automatically without verifying"
msgstr ""
#: aircox_cms/models.py:77
msgid "success message"
msgstr ""
#: aircox_cms/models.py:78
msgid "Your comment has been successfully posted!"
msgstr ""
#: aircox_cms/models.py:79
msgid "message to display when a post has been posted"
msgstr ""
#: aircox_cms/models.py:82
msgid "waiting message"
msgstr ""
#: aircox_cms/models.py:83
msgid "Your comment is awaiting for approval."
msgstr ""
#: aircox_cms/models.py:84
msgid "message to display when a post waits to be reviewed"
msgstr ""
#: aircox_cms/models.py:87
msgid "error message"
msgstr ""
#: aircox_cms/models.py:88
msgid "We could not save your message. Please correct the error(s) below."
msgstr ""
#: aircox_cms/models.py:89
msgid "message to display there is an error an incomplete form."
msgstr ""
#: aircox_cms/models.py:93
msgid "automatic publications"
msgstr ""
#: aircox_cms/models.py:96
msgid ""
"Create automatically new publications for new programs and diffusions in the "
"timetable. If set, please complete other options of this panel"
msgstr ""
#: aircox_cms/models.py:103
msgid "default program parent page"
msgstr ""
#: aircox_cms/models.py:106
msgid ""
"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"
msgstr ""
#: aircox_cms/models.py:122
msgid "promotion"
msgstr ""
#: aircox_cms/models.py:129 aircox_cms/templates/aircox_cms/snippets/comments.html:6
msgid "Comments"
msgstr ""
#: aircox_cms/models.py:133
msgid "Programs and controls"
msgstr ""
#: aircox_cms/models.py:137
msgid "website settings"
msgstr ""
#: aircox_cms/models.py:149
msgid "public"
msgstr ""
#: aircox_cms/models.py:153
msgid "author"
msgstr ""
#: aircox_cms/models.py:157 aircox_cms/models.py:365
msgid "email"
msgstr ""
#: aircox_cms/models.py:161
msgid "website"
msgstr ""
#: aircox_cms/models.py:165 aircox_cms/models.py:212
msgid "date"
msgstr ""
#: aircox_cms/models.py:169
msgid "comment"
msgstr ""
#. Translators: text shown in the comments list (in admin)
#: aircox_cms/models.py:175
#, python-brace-format
msgid "{date}, {author}: {content}..."
msgstr ""
#: aircox_cms/models.py:218
msgid "publish as program"
msgstr ""
#: aircox_cms/models.py:221
msgid "use this program as the author of the publication"
msgstr ""
#: aircox_cms/models.py:224
msgid "focus"
msgstr ""
#: aircox_cms/models.py:226
msgid "the publication is highlighted;"
msgstr ""
#: aircox_cms/models.py:229 aircox_cms/models.py:231
msgid "allow comments"
msgstr ""
#: aircox_cms/models.py:237
msgid "cover"
msgstr ""
#: aircox_cms/models.py:241
msgid "image to use as cover of the publication"
msgstr ""
#: aircox_cms/models.py:244
msgid "summary"
msgstr ""
#: aircox_cms/models.py:246
msgid "summary of the publication"
msgstr ""
#: aircox_cms/models.py:255 aircox_cms/models.py:256
msgid "Publication"
msgstr ""
#: aircox_cms/models.py:263 aircox_cms/models.py:270 aircox_cms/models.py:590 aircox_cms/models.py:619
msgid "Content"
msgstr ""
#: aircox_cms/models.py:271
msgid "Links"
msgstr ""
#: aircox_cms/models.py:358
msgid "program"
msgstr ""
#: aircox_cms/models.py:368
msgid "email is public"
msgstr ""
#: aircox_cms/models.py:370
msgid "the email addess is accessible to the public"
msgstr ""
#: aircox_cms/models.py:374
msgid "Program"
msgstr ""
#: aircox_cms/models.py:375 aircox_cms/wagtail_hooks.py:20 aircox_cms/wagtail_hooks.py:177
msgid "Programs"
msgstr ""
#: aircox_cms/models.py:444
msgid "diffusion"
msgstr ""
#: aircox_cms/models.py:453
msgid "publish archive"
msgstr ""
#: aircox_cms/models.py:455
msgid "publish the podcast of the complete diffusion"
msgstr ""
#: aircox_cms/models.py:459
msgid "Diffusion"
msgstr ""
#: aircox_cms/models.py:460 aircox_cms/wagtail_hooks.py:28
msgid "Diffusions"
msgstr ""
#: aircox_cms/models.py:463
msgid "Tracks"
msgstr ""
#: aircox_cms/models.py:503
#, python-format
msgid "Rerun of %(date)s"
msgstr ""
#: aircox_cms/models.py:507
msgid "Cancelled"
msgstr ""
#: aircox_cms/models.py:570 aircox_cms/models.py:607
msgid "body"
msgstr ""
#: aircox_cms/models.py:572 aircox_cms/models.py:609
msgid "add an extra description for this list"
msgstr ""
#: aircox_cms/models.py:575
msgid "list from the request"
msgstr ""
#: aircox_cms/models.py:578
msgid ""
"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."
msgstr ""
#: aircox_cms/models.py:594 aircox_cms/models.py:595
msgid "Generic Page"
msgstr ""
#: aircox_cms/models.py:652 aircox_cms/sections.py:859
msgid "station"
msgstr ""
#: aircox_cms/models.py:655 aircox_cms/sections.py:862
msgid "(required) the station on which the logs happened"
msgstr ""
#: aircox_cms/models.py:658
msgid "maximum age"
msgstr ""
#: aircox_cms/models.py:660
msgid "maximum days in the past allowed to be shown. 0 means no limit"
msgstr ""
#: aircox_cms/models.py:665 aircox_cms/models.py:666
msgid "Logs"
msgstr ""
#: aircox_cms/models.py:672
msgid "Configuration"
msgstr ""
#: aircox_cms/models.py:707 aircox_cms/models.py:708
msgid "Timetable"
msgstr ""
#: aircox_cms/sections.py:84
msgid "url"
msgstr ""
#: aircox_cms/sections.py:86
msgid "URL of the link"
msgstr ""
#: aircox_cms/sections.py:94
msgid "Use a page instead of a URL"
msgstr ""
#: aircox_cms/sections.py:98
msgid "icon"
msgstr ""
#: aircox_cms/sections.py:102
msgid "icon to display before the url"
msgstr ""
#: aircox_cms/sections.py:110
msgid "text"
msgstr ""
#: aircox_cms/sections.py:113
msgid "text to display of the link"
msgstr ""
#: aircox_cms/sections.py:125
msgid "link"
msgstr ""
#: aircox_cms/sections.py:156
msgid "filter by date"
msgstr ""
#: aircox_cms/sections.py:163
msgid "filter by type"
msgstr ""
#: aircox_cms/sections.py:166
msgid "if set, select only elements that are of this type"
msgstr ""
#: aircox_cms/sections.py:171
msgid "filter by a related page"
msgstr ""
#: aircox_cms/sections.py:174
msgid "if set, select children or siblings related to this page"
msgstr ""
#: aircox_cms/sections.py:177
msgid "select siblings of related"
msgstr ""
#: aircox_cms/sections.py:179
msgid "if selected select related publications that are siblings of this one"
msgstr ""
#: aircox_cms/sections.py:183
msgid "ascending order"
msgstr ""
#: aircox_cms/sections.py:185
msgid "if selected sort list in the ascending order by date"
msgstr ""
#: aircox_cms/sections.py:196
msgid "filters"
msgstr ""
#: aircox_cms/sections.py:200
msgid "sorting"
msgstr ""
#: aircox_cms/sections.py:360
msgid "navigation days count"
msgstr ""
#: aircox_cms/sections.py:362
msgid "number of days to display in the navigation header when we use dates"
msgstr ""
#: aircox_cms/sections.py:366
msgid "navigation per week"
msgstr ""
#: aircox_cms/sections.py:368
msgid ""
"if selected, show dates navigation per weeks instead of show days equally "
"around the current date"
msgstr ""
#: aircox_cms/sections.py:379
msgid "Navigation"
msgstr ""
#: aircox_cms/sections.py:450
msgid "name"
msgstr ""
#: aircox_cms/sections.py:453
msgid "name of this section (not displayed)"
msgstr ""
#: aircox_cms/sections.py:456
msgid "position"
msgstr ""
#: aircox_cms/sections.py:459
msgid "name of the template block in which the section must be set"
msgstr ""
#: aircox_cms/sections.py:464
msgid "model"
msgstr ""
#: aircox_cms/sections.py:466
msgid ""
"this section is displayed only when the current page or publication is of "
"this type"
msgstr ""
#: aircox_cms/sections.py:472
msgid "page"
msgstr ""
#: aircox_cms/sections.py:474
msgid "this section is displayed only on this page"
msgstr ""
#: aircox_cms/sections.py:482 aircox_cms/sections.py:581
msgid "General"
msgstr ""
#: aircox_cms/sections.py:483
msgid "Section Items"
msgstr ""
#: aircox_cms/sections.py:517
msgid "item"
msgstr ""
#: aircox_cms/sections.py:561
msgid "title"
msgstr ""
#: aircox_cms/sections.py:566
msgid "show title"
msgstr ""
#: aircox_cms/sections.py:568
msgid "if set show a title at the head of the section"
msgstr ""
#: aircox_cms/sections.py:571
msgid "CSS class"
msgstr ""
#: aircox_cms/sections.py:574
msgid "section container's \"class\" attribute"
msgstr ""
#: aircox_cms/sections.py:643
msgid "is related"
msgstr ""
#: aircox_cms/sections.py:646
msgid ""
"if set, section is related to the page being processed e.g rendering a list "
"of links will use thoses of the publication instead of an assigned one."
msgstr ""
#: aircox_cms/sections.py:691
msgid "image"
msgstr ""
#: aircox_cms/sections.py:695
msgid ""
"If this item is related to the current page, this image will be used only "
"when the page has not a cover"
msgstr ""
#: aircox_cms/sections.py:700
msgid "width"
msgstr ""
#: aircox_cms/sections.py:702
msgid "if set and > 0, set a maximum width for the image"
msgstr ""
#: aircox_cms/sections.py:705
msgid "height"
msgstr ""
#: aircox_cms/sections.py:707
msgid "if set 0 and > 0, set a maximum height for the image"
msgstr ""
#: aircox_cms/sections.py:710
msgid "resize mode"
msgstr ""
#: aircox_cms/sections.py:713
msgid "if the image is resized, set the resizing mode"
msgstr ""
#: aircox_cms/sections.py:722
msgid "Resizing"
msgstr ""
#: aircox_cms/sections.py:776
msgid "links"
msgstr ""
#: aircox_cms/sections.py:777
msgid ""
"If the list is related to the current page, theses links will be used when "
"there is no links found for this publication"
msgstr ""
#: aircox_cms/sections.py:799
msgid "focus available"
msgstr ""
#: aircox_cms/sections.py:801
msgid "if true, highlight the first focused article found"
msgstr ""
#: aircox_cms/sections.py:804 aircox_cms/sections.py:865
msgid "count"
msgstr ""
#: aircox_cms/sections.py:806
msgid "number of items to display in the list"
msgstr ""
#: aircox_cms/sections.py:809
msgid "text of the url"
msgstr ""
#: aircox_cms/sections.py:812
msgid ""
"use this text to display an URL to the complete list. If empty, does not "
"print an address"
msgstr ""
#: aircox_cms/sections.py:821
msgid "Rendering"
msgstr ""
#: aircox_cms/sections.py:867
msgid "number of items to display in the list (max 100)"
msgstr ""
#: aircox_cms/sections.py:871
msgid "list of logs"
msgstr ""
#: aircox_cms/sections.py:872
msgid "lists of logs"
msgstr ""
#: aircox_cms/sections.py:912
msgid "Section: Timetable"
msgstr ""
#: aircox_cms/sections.py:913
msgid "Sections: Timetable"
msgstr ""
#: aircox_cms/sections.py:936
msgid "Section: publication's info"
msgstr ""
#: aircox_cms/sections.py:937
msgid "Sections: publication's info"
msgstr ""
#: aircox_cms/sections.py:942
msgid "default text"
msgstr ""
#: aircox_cms/sections.py:944
msgid "search"
msgstr ""
#: aircox_cms/sections.py:945
msgid "text to display when the search field is empty"
msgstr ""
#: aircox_cms/sections.py:949
msgid "Section: search field"
msgstr ""
#: aircox_cms/sections.py:950
msgid "Sections: search field"
msgstr ""
#: aircox_cms/sections.py:965
msgid "live title"
msgstr ""
#: aircox_cms/sections.py:967
msgid "text to display when it plays live"
msgstr ""
#: aircox_cms/sections.py:970
msgid "audio streams"
msgstr ""
#: aircox_cms/sections.py:971
msgid "one audio stream per line"
msgstr ""
#: aircox_cms/sections.py:975
msgid "Section: Player"
msgstr ""
#: aircox_cms/templates/aircox_cms/diffusion_page.html:8
msgid "Playlist"
msgstr ""
#: aircox_cms/templates/aircox_cms/diffusion_page.html:22
msgid "Dates of diffusion"
msgstr ""
#: aircox_cms/templates/aircox_cms/diffusion_page.html:36
msgid "Podcasts"
msgstr ""
#: aircox_cms/templates/aircox_cms/event_page.html:6
msgid "Practical information"
msgstr ""
#: aircox_cms/templates/aircox_cms/event_page.html:10
msgid "Date"
msgstr ""
#: aircox_cms/templates/aircox_cms/event_page.html:13
msgid "Place"
msgstr ""
#: aircox_cms/templates/aircox_cms/event_page.html:15
msgid "Price"
msgstr ""
#: aircox_cms/templates/aircox_cms/generic_page.html:16
#, python-format
msgid "Search in publications for <i>%(terms)s</i>"
msgstr ""
#: aircox_cms/templates/aircox_cms/generic_page.html:20
#, python-format
msgid ""
"\n"
" Related to <a href=\"%(url)s\">%(title)s</a>"
msgstr ""
#: aircox_cms/templates/aircox_cms/generic_page.html:24
msgid "All the publications"
msgstr ""
#: aircox_cms/templates/aircox_cms/generic_page.html:41
msgid "More about it"
msgstr ""
#: aircox_cms/templates/aircox_cms/program_page.html:12
msgid "Schedule"
msgstr ""
#: aircox_cms/templates/aircox_cms/program_page.html:18
#, python-format
msgid ""
"\n"
" %(day)s %(start)s until %(end)s, %(frequency)s\n"
" "
msgstr ""
#: aircox_cms/templates/aircox_cms/program_page.html:24
msgid "Rerun"
msgstr ""
#: aircox_cms/templates/aircox_cms/program_page.html:30
msgid "This program is no longer active"
msgstr ""
#: aircox_cms/templates/aircox_cms/publication.html:33
msgid "Go back to the publication"
msgstr ""
#: aircox_cms/templates/aircox_cms/sections/section_publication_info.html:15
msgid "Published by"
msgstr ""
#: aircox_cms/templates/aircox_cms/sections/section_publication_info.html:22
msgid "Published on "
msgstr ""
#: aircox_cms/templates/aircox_cms/sections/section_publication_info.html:30
msgid "Tags"
msgstr ""
#: aircox_cms/templates/aircox_cms/sections/section_publication_info.html:39
msgid "Share"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/comments.html:21
msgid "show more options"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/comments.html:34
msgid "Post!"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/date_list.html:8
msgid "previous days"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/date_list.html:20
msgid "next days"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/list.html:24
msgid "previous page"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/list.html:54
msgid "next page"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:5
msgid "Your browser does not support the <code>audio</code> element."
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:15
msgid "play"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:17
msgid "pause"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:19
msgid "loading..."
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:27
msgid "add to the player"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:28
msgid "more informations"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:29
msgid "remove this sound"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/player.html:44
msgid "enable and disable single mode"
msgstr ""
#: aircox_cms/templates/aircox_cms/snippets/sound_list_item.html:39
msgid "add this sound to the playlist"
msgstr ""
#: aircox_cms/wagtail_hooks.py:36
msgid "Schedules"
msgstr ""
#: aircox_cms/wagtail_hooks.py:44
msgid "Streams"
msgstr ""
#: aircox_cms/wagtail_hooks.py:51
msgid "Advanced"
msgstr ""
#: aircox_cms/wagtail_hooks.py:60
msgid "Sounds"
msgstr ""
#: aircox_cms/wagtail_hooks.py:145
msgid "Today's Diffusions"
msgstr ""

View File

@ -0,0 +1,92 @@
"""
Create missing publications for diffusions and programs already existing.
We limit the creation of diffusion to the elements to those that start at least
in the last 15 days, and to the future ones.
The new publications are not published automatically.
"""
import logging
from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone as tz
from aircox.models import Program, Diffusion
from aircox_cms.models import WebsiteSettings, ProgramPage, DiffusionPage
logger = logging.getLogger('aircox.tools')
class Command (BaseCommand):
help= __doc__
def add_arguments (self, parser):
parser.formatter_class=RawTextHelpFormatter
def handle (self, *args, **options):
for settings in WebsiteSettings.objects.all():
logger.info('start sync for website {}'.format(
str(settings.site)
))
if not settings.auto_create:
logger.warning('auto_create disabled: skip')
continue
if not settings.default_program_parent_page:
logger.warning('no default program page for this website: skip')
continue
# programs
logger.info('Programs...')
parent = settings.default_program_parent_page
qs = Program.objects.filter(
active = True,
stream__isnull = True,
page__isnull = True,
)
for program in qs:
logger.info('- ' + program.name)
page = ProgramPage(
program = program,
title = program.name,
live = False,
)
parent.add_child(instance = page)
# diffusions
logger.info('Diffusions...')
qs = Diffusion.objects.filter(
start__gt = tz.now().date() - tz.timedelta(days = 20),
page__isnull = True,
initial__isnull = True
).exclude(type = Diffusion.Type.unconfirmed)
for diffusion in qs:
if not diffusion.program.page.count():
if not hasattr(diffusion.program, '__logged_diff_error'):
logger.warning(
'the program {} has no page; skip the creation of '
'page for its diffusions'.format(
diffusion.program.name
)
)
diffusion.program.__logged_diff_error = True
continue
logger.info('- ' + str(diffusion))
try:
page = DiffusionPage.from_diffusion(
diffusion, live = False
)
diffusion.program.page.first().add_child(instance = page)
except:
import sys
e = sys.exc_info()[0]
logger.error('Error saving', str(diffusion) + ':', e)
logger.info('done')

718
aircox_cms/models.py Normal file
View File

@ -0,0 +1,718 @@
import datetime
from django.db import models
from django.contrib.auth.models import User
from django.contrib import messages
from django.utils import timezone as tz
from django.utils.translation import ugettext as _, ugettext_lazy
# pages and panels
from wagtail.contrib.settings.models import BaseSetting, register_setting
from wagtail.wagtailcore.models import Page, Orderable, \
PageManager, PageQuerySet
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailadmin.edit_handlers import FieldPanel, FieldRowPanel, \
MultiFieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel
from wagtail.wagtailsearch import index
# snippets
from wagtail.wagtailsnippets.models import register_snippet
# tags
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import TaggedItemBase
# comment clean-up
import bleach
import aircox.models
import aircox_cms.settings as settings
from aircox_cms.utils import image_url
from aircox_cms.sections import *
@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
# general website information
favicon = models.ImageField(
verbose_name = _('favicon'),
null=True, blank=True,
help_text = _('small logo for the website displayed in the browser'),
)
tags = models.CharField(
_('tags'),
max_length=256,
null=True, blank=True,
help_text = _('tags describing the website; used for referencing'),
)
description = models.CharField(
_('public description'),
max_length=256,
null=True, blank=True,
help_text = _('public description of the website; used for referencing'),
)
list_page = models.ForeignKey(
'cms.GenericPage',
verbose_name = _('page for lists'),
help_text=_('page used to display the results of a search and other '
'lists'),
related_name= 'list_page'
)
# comments
accept_comments = models.BooleanField(
default = True,
help_text = _('publish comments automatically without verifying'),
)
allow_comments = models.BooleanField(
default = True,
help_text = _('publish comments automatically without verifying'),
)
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'),
)
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'),
)
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.'),
)
auto_create = models.BooleanField(
_('automatic publications'),
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'
)
)
default_program_parent_page = ParentalKey(
Page,
verbose_name = _('default program parent 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'
),
limit_choices_to = lambda: {
'show_in_menus': True,
'publication__isnull': False,
},
)
panels = [
MultiFieldPanel([
FieldPanel('favicon'),
FieldPanel('tags'),
FieldPanel('description'),
FieldPanel('list_page'),
], heading=_('promotion')),
MultiFieldPanel([
FieldPanel('allow_comments'),
FieldPanel('accept_comments'),
FieldPanel('comment_success_message'),
FieldPanel('comment_wait_message'),
FieldPanel('comment_error_message'),
], heading = _('Comments')),
MultiFieldPanel([
FieldPanel('auto_create'),
FieldPanel('default_program_parent_page'),
], heading = _('Programs and controls')),
]
class Meta:
verbose_name = _('website settings')
#
# Publications
#
@register_snippet
class Comment(models.Model):
publication = models.ForeignKey(
'Publication',
)
published = models.BooleanField(
verbose_name = _('public'),
default = False
)
author = models.CharField(
verbose_name = _('author'),
max_length = 32,
)
email = models.EmailField(
verbose_name = _('email'),
blank = True, null = True,
)
url = models.URLField(
verbose_name = _('website'),
blank = True, null = True,
)
date = models.DateTimeField(
_('date'),
auto_now_add = True,
)
content = models.TextField (
_('comment'),
)
def __str__(self):
# Translators: text shown in the comments list (in admin)
return _('{date}, {author}: {content}...').format(
author = self.author,
date = self.date.strftime('%d %A %Y, %H:%M'),
content = self.content[:128]
)
def make_safe(self):
self.author = bleach.clean(self.author, tags=[])
if self.email:
self.email = bleach.clean(self.email, tags=[])
self.email = self.email.replace('"', '%22')
if self.url:
self.url = bleach.clean(self.url, tags=[])
self.url = self.url.replace('"', '%22')
self.content = bleach.clean(
self.content,
tags=settings.AIRCOX_CMS_BLEACH_COMMENT_TAGS,
attributes=settings.AIRCOX_CMS_BLEACH_COMMENT_ATTRS
)
def save(self, make_safe = True, *args, **kwargs):
if make_safe:
self.make_safe()
return super().save(*args, **kwargs)
class RelatedLink(RelatedLinkBase):
parent = ParentalKey('Publication', related_name='related_links')
class PublicationTag(TaggedItemBase):
content_object = ParentalKey('Publication', related_name='tagged_items')
class Publication(Page):
order_field = 'date'
date = models.DateTimeField(
_('date'),
blank = True, null = True,
auto_now_add = True,
)
publish_as = models.ForeignKey(
'ProgramPage',
verbose_name = _('publish as program'),
on_delete=models.SET_NULL,
blank = True, null = True,
help_text = _('use this program as the author of the publication'),
)
focus = models.BooleanField(
_('focus'),
default = False,
help_text = _('the publication is highlighted;'),
)
allow_comments = models.BooleanField(
_('allow comments'),
default = True,
help_text = _('allow comments')
)
body = RichTextField(blank=True)
cover = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('cover'),
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text = _('image to use as cover of the publication'),
)
summary = models.TextField(
_('summary'),
blank = True, null = True,
help_text = _('summary of the publication'),
)
tags = ClusterTaggableManager(
verbose_name = _('tags'),
through=PublicationTag,
blank=True
)
class Meta:
verbose_name = _('Publication')
verbose_name_plural = _('Publication')
content_panels = [
MultiFieldPanel([
FieldPanel('title'),
FieldPanel('body', classname='full'),
FieldPanel('summary'),
], heading=_('Content'))
]
promote_panels = [
MultiFieldPanel([
ImageChooserPanel('cover'),
FieldPanel('tags'),
FieldPanel('focus'),
], heading=_('Content')),
InlinePanel('related_links', label=_('Links'))
] + Page.promote_panels
settings_panels = Page.settings_panels + [
FieldPanel('publish_as'),
FieldPanel('allow_comments'),
]
search_fields = [
index.SearchField('title', partial_match=True),
index.SearchField('body', partial_match=True),
index.FilterField('live'),
index.FilterField('show_in_menus'),
]
@property
def url(self):
if not self.live:
parent = self.get_parent().specific
return parent and parent.url
return super().url
@property
def icon(self):
return image_url(self.cover, 'fill-64x64')
@property
def small_icon(self):
return image_url(self.cover, 'fill-32x32')
@property
def recents(self):
return self.get_children().type(Publication).not_in_menu().live() \
.order_by('-publication__date')
@property
def comments(self):
return Comment.objects.filter(
publication = self,
published = True,
).order_by('-date')
def save(self, *args, **kwargs):
if not self.date and self.first_published_at:
self.date = self.first_published_at
super().save(*args, **kwargs)
def get_context(self, request, *args, **kwargs):
from aircox_cms.forms import CommentForm
context = super().get_context(request, *args, **kwargs)
view = request.GET.get('view')
page = request.GET.get('page')
if self.allow_comments and \
WebsiteSettings.for_site(request.site).allow_comments:
context['comment_form'] = CommentForm()
if view == 'list':
context['object_list'] = ListBase.from_request(
request, context = context, related = self
)
return context
def serve(self, request):
from aircox_cms.forms import CommentForm
if request.POST and 'comment' in request.POST['type']:
settings = WebsiteSettings.for_site(request.site)
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.publication = self
comment.published = settings.accept_comments
comment.save()
messages.success(request,
settings.comment_success_message
if comment.published else
settings.comment_wait_message,
fail_silently=True,
)
else:
messages.error(
request, settings.comment_error_message, fail_silently=True
)
return super().serve(request)
class ProgramPage(Publication):
program = models.ForeignKey(
aircox.models.Program,
verbose_name = _('program'),
related_name = 'page',
on_delete=models.SET_NULL,
blank=True, null=True,
)
# rss = models.URLField()
email = models.EmailField(
_('email'), blank=True, null=True,
)
email_is_public = models.BooleanField(
_('email is public'),
default = False,
help_text = _('the email addess is accessible to the public'),
)
class Meta:
verbose_name = _('Program')
verbose_name_plural = _('Programs')
content_panels = [
FieldPanel('program'),
] + Publication.content_panels
settings_panels = Publication.settings_panels + [
FieldPanel('email'),
FieldPanel('email_is_public'),
]
def diffs_to_page(self, diffs):
for diff in diffs:
if diff.page.count():
diff.page_ = diff.page.first()
else:
diff.page_ = ListItem(
title = '{}, {}'.format(
self.program.name, diff.date.strftime('%d %B %Y')
),
cover = self.cover,
live = True,
date = diff.start,
)
return [
diff.page_ for diff in diffs if diff.page_.live
]
@property
def next(self):
now = tz.now()
diffs = aircox.models.Diffusion.objects \
.filter(end__gte = now, program = self.program) \
.order_by('start').prefetch_related('page')
return self.diffs_to_page(diffs)
@property
def prev(self):
now = tz.now()
diffs = aircox.models.Diffusion.objects \
.filter(end__lte = now, program = self.program) \
.order_by('-start').prefetch_related('page')
return self.diffs_to_page(diffs)
class Track(aircox.models.Track,Orderable):
sort_order_field = 'position'
diffusion = ParentalKey('DiffusionPage',
related_name='tracks')
panels = [
FieldPanel('artist'),
FieldPanel('title'),
FieldPanel('tags'),
FieldPanel('info'),
]
def save(self, *args, **kwargs):
if self.diffusion.diffusion:
self.related = self.diffusion.diffusion
self.in_seconds = False
super().save(*args, **kwargs)
class DiffusionPage(Publication):
order_field = 'diffusion__start'
diffusion = models.ForeignKey(
aircox.models.Diffusion,
verbose_name = _('diffusion'),
related_name = 'page',
on_delete=models.SET_NULL,
null=True,
limit_choices_to = {
'initial__isnull': True,
},
)
publish_archive = models.BooleanField(
_('publish archive'),
default = False,
help_text = _('publish the podcast of the complete diffusion'),
)
class Meta:
verbose_name = _('Diffusion')
verbose_name_plural = _('Diffusions')
content_panels = Publication.content_panels + [
InlinePanel('tracks', label=_('Tracks')),
]
promote_panels = [
# FieldPanel('diffusion'),
FieldPanel('publish_archive'),
] + Publication.promote_panels
@classmethod
def from_diffusion(cl, diff, model = None, **kwargs):
model = model or cl
model_kwargs = {
'diffusion': diff,
'title': '{}, {}'.format(
diff.program.name, tz.localtime(diff.date).strftime('%d %B %Y')
),
'cover': (diff.program.page.count() and \
diff.program.page.first().cover) or None,
'date': diff.start,
}
model_kwargs.update(kwargs)
r = model(**model_kwargs)
return r
@classmethod
def as_item(cl, diff):
"""
Return a DiffusionPage or ListItem from a Diffusion.
"""
initial = diff.initial or diff
if initial.page.all().count():
item = initial.page.all().first()
else:
item = cl.from_diffusion(diff, ListItem)
item.live = True
item.info = []
# Translators: informations about a diffusion
if diff.initial:
item.info.append(_('Rerun of %(date)s') % {
'date': diff.initial.start.strftime('%A %d')
})
if diff.type == diff.Type.canceled:
item.info.append(_('Cancelled'))
item.info = '; '.join(item.info)
item.date = diff.start
item.css_class = 'diffusion'
return item
def get_archive(self):
"""
Return the diffusion's archive as podcast
"""
if not self.publish_archive or not self.diffusion:
return
sound = self.diffusion.get_archives() \
.filter(public = True).first()
if sound:
sound.detail_url = self.detail_url
return sound
def get_podcasts(self):
"""
Return a list of podcasts, with archive as the first item of the
list when available.
"""
podcasts = []
archive = self.get_archive()
if archive:
podcasts.append(archive)
qs = self.diffusion.get_excerpts().filter(public = True)
podcasts.extend(qs[:])
for podcast in podcasts:
podcast.detail_url = self.url
return podcasts
def save(self, *args, **kwargs):
if self.diffusion:
# sync date
self.date = self.diffusion.start
# update podcasts' attributes
for podcast in self.diffusion.sound_set \
.exclude(type = aircox.models.Sound.Type.removed):
publish = self.live and self.publish_archive \
if podcast.type == podcast.Type.archive else self.live
if podcast.public != publish:
podcast.public = publish
podcast.save()
super().save(*args, **kwargs)
#
# Other type of pages
#
class GenericPage(Page):
"""
Page for simple lists, query is done though request' GET fields.
Look at get_queryset for more information.
"""
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')
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
return context
class DatedListPage(DatedListBase,Page):
body = RichTextField(
_('body'),
blank = True, null = True,
help_text = _('add an extra description for this list')
)
class Meta:
abstract = True
content_panels = [
MultiFieldPanel([
FieldPanel('title'),
FieldPanel('body'),
], heading=_('Content')),
] + DatedListBase.panels
def get_queryset(self, request, context):
"""
Must be implemented by the child
"""
return []
def get_context(self, request, *args, **kwargs):
"""
note: context is updated using self.get_date_context
"""
context = super().get_context(request, *args, **kwargs)
# date navigation
if 'date' in request.GET:
date = request.GET.get('date')
date = self.str_to_date(date)
else:
date = tz.now().date()
context.update(self.get_date_context(date))
# queryset
context['object_list'] = self.get_queryset(request, context)
return context
class LogsPage(DatedListPage):
template = 'aircox_cms/dated_list_page.html'
station = models.ForeignKey(
aircox.models.Station,
verbose_name = _('station'),
null = True,
on_delete=models.SET_NULL,
help_text = _('(required) the station on which the logs happened')
)
age_max = models.IntegerField(
_('maximum age'),
default=15,
help_text = _('maximum days in the past allowed to be shown. '
'0 means no limit')
)
class Meta:
verbose_name = _('Logs')
verbose_name_plural = _('Logs')
content_panels = DatedListBase.panels + [
MultiFieldPanel([
FieldPanel('station'),
FieldPanel('age_max'),
], heading=_('Configuration')),
]
def get_nav_dates(self, date):
"""
Return a list of dates availables for the navigation
"""
# there might be a bug if age_max < nav_days
today = tz.now().date()
first = min(date, today)
first = max( first - tz.timedelta(days = self.nav_days-1),
today - tz.timedelta(days = self.age_max))
return [ first + tz.timedelta(days=i)
for i in range(0, self.nav_days) ]
def get_queryset(self, request, context):
today = tz.now().date()
if context['nav_dates']['next'] > today:
context['nav_dates']['next'] = None
if context['nav_dates']['prev'] < \
today - tz.timedelta(days = self.age_max):
context['nav_dates']['prev'] = None
logs = []
for date in context['nav_dates']['dates']:
items = [ SectionLogsList.as_item(item)
for item in self.station.on_air(date = date) ]
logs.append((date, items))
return logs
class TimetablePage(DatedListPage):
template = 'aircox_cms/dated_list_page.html'
class Meta:
verbose_name = _('Timetable')
verbose_name_plural = _('Timetable')
def get_queryset(self, request, context):
diffs = []
for date in context['nav_dates']['dates']:
items = aircox.models.Diffusion.objects.get_at(date).order_by('start')
items = [ DiffusionPage.as_item(item) for item in items ]
diffs.append((date, items))
return diffs

987
aircox_cms/sections.py Normal file
View File

@ -0,0 +1,987 @@
import datetime
import re
from enum import IntEnum
from django.db import models
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from wagtail.wagtailcore.models import Page, Orderable
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailadmin.edit_handlers import FieldPanel, FieldRowPanel, \
MultiFieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel
from wagtail.wagtailsearch import index
from wagtail.wagtailcore.utils import camelcase_to_underscore
# snippets
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from wagtail.wagtailsnippets.models import register_snippet
from wagtail.wagtailimages.utils import generate_signature
# tags
from modelcluster.models import ClusterableModel
from modelcluster.fields import ParentalKey
from modelcluster.tags import ClusterTaggableManager
from taggit.models import TaggedItemBase
# aircox
import aircox.models
def related_pages_filter(reset_cache=False):
"""
Return a dict that can be used to filter foreignkey to pages'
subtype declared in aircox_cms.models.
This value is stored in cache, but it is possible to reset the
cache using the `reset_cache` parameter.
"""
if not reset_cache and hasattr(related_pages_filter, 'cache'):
return related_pages_filter.cache
import aircox_cms.models as cms
import inspect
related_pages_filter.cache = {
'model__in': list(name.lower() for name, member in
inspect.getmembers(cms,
lambda x: inspect.isclass(x) and issubclass(x, Page)
)
if member != Page
),
}
return related_pages_filter.cache
class ListItem:
"""
Generic normalized element to add item in lists that are not based
on Publication.
"""
title = ''
summary = ''
url = ''
cover = None
date = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
self.specific = self
#
# Base
#
class RelatedLinkBase(Orderable):
url = models.URLField(
_('url'),
null=True, blank=True,
help_text = _('URL of the link'),
)
page = models.ForeignKey(
'wagtailcore.Page',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text = _('Use a page instead of a URL')
)
icon = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('icon'),
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+',
help_text = _('icon to display before the url'),
)
# icon = models.ImageField(
# verbose_name = _('icon'),
# null=True, blank=True,
# help_text = _('icon to display before the url'),
#)
text = models.CharField(
_('text'),
max_length = 64,
null = True, blank=True,
help_text = _('text to display of the link'),
)
class Meta:
abstract = True
panels = [
MultiFieldPanel([
FieldPanel('text'),
ImageChooserPanel('icon'),
FieldPanel('url'),
PageChooserPanel('page'),
], heading=_('link'))
]
def as_dict(self):
"""
Return compiled values from parameters as dict with
'url', 'icon', 'text'
"""
if self.page:
url, text = self.page.url, self.page.title
else:
url, text = self.url, self.url
return {
'url': url,
'text': self.text or text,
'icon': self.icon
}
class ListBase(models.Model):
"""
Generic list
"""
class DateFilter(IntEnum):
none = 0x00
previous = 0x01
next = 0x02
before_related = 0x03,
after_related = 0x04,
date_filter = models.SmallIntegerField(
verbose_name = _('filter by date'),
choices = [ (int(y), _(x.replace('_', ' ')))
for x,y in DateFilter.__members__.items() ],
blank = True, null = True,
)
model = models.ForeignKey(
ContentType,
verbose_name = _('filter by type'),
blank = True, null = True,
on_delete=models.SET_NULL,
help_text = _('if set, select only elements that are of this type'),
limit_choices_to = related_pages_filter,
)
related = models.ForeignKey(
Page,
verbose_name = _('filter by a related page'),
blank = True, null = True,
on_delete=models.SET_NULL,
help_text = _('if set, select children or siblings related to this page'),
)
siblings = models.BooleanField(
verbose_name = _('select siblings of related'),
default = False,
help_text = _('if selected select related publications that are '
'siblings of this one'),
)
asc = models.BooleanField(
verbose_name = _('ascending order'),
default = True,
help_text = _('if selected sort list in the ascending order by date')
)
class Meta:
abstract = True
panels = [
MultiFieldPanel([
FieldPanel('model'),
PageChooserPanel('related'),
FieldPanel('siblings'),
], heading=_('filters')),
MultiFieldPanel([
FieldPanel('date_filter'),
FieldPanel('asc'),
], heading=_('sorting'))
]
def get_queryset(self):
"""
Get queryset based on the arguments. This class is intended to be
reusable by other classes if needed.
"""
from aircox_cms.models import Publication
related = self.related and self.related.specific
# model
if self.model:
qs = self.model.model_class().objects.all()
else:
qs = Publication.objects.all()
qs = qs.live().not_in_menu()
# related
if related:
if self.siblings:
qs = qs.sibling_of(related)
else:
qs = qs.descendant_of(related)
date = self.related.date if hasattr(related, 'date') else \
self.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:
qs = qs.filter(date__gte = date)
# date
date = tz.now()
if self.date_filter == self.DateFilter.previous:
qs = qs.filter(date__lt = date)
elif self.date_filter == self.DateFilter.next:
qs = qs.filter(date__gte = date)
# sort
if self.asc:
return qs.order_by('date', 'pk')
return qs.order_by('-date', '-pk')
def to_url(self, list_page = None, **kwargs):
"""
Return a url parameters from self. Extra named parameters are used
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.
"""
import aircox_cms.models as models
params = {
'date_filter': self.get_date_filter_display(),
'model': self.model and self.model.model,
'asc': self.asc,
'related': self.related,
'siblings': self.siblings,
}
params.update(kwargs)
page = params.get('related') or list_page or \
models.GenericPage.objects.all().first()
if params.get('related'):
params['related'] = True
params = '&'.join([
key if value == True else '{}={}'.format(key, value)
for key, value in params.items()
if value
])
return page.url + '?' + params
@classmethod
def from_request(cl, request, related = None, context = None,
*args, **kwargs):
"""
Return a queryset from the request's GET parameters. Context
can be used to update relative informations.
This function can be used by other views if needed
Parameters:
* date_filter: one of DateFilter attribute's key.
* model: ['program','diffusion','event'] type of the publication
* asc: if present, sort ascending instead of descending
* related: children of the thread passed in arguments only
* siblings: sibling of the related instead of children
* tag: tag to search for
* search: query to search in the publications
* page: page number
Context's fields:
* object_list: the final queryset
* list_selector: dict of { 'tag_query', 'search_query' } plus
arguments passed to ListBase.get_base_queryset
* paginator: paginator object
"""
def set(key, value):
if context is not None:
context[key] = value
date_filter = request.GET.get('date_filter')
model = request.GET.get('model')
kwargs = {
'date_filter':
int(getattr(cl.DateFilter, date_filter))
if date_filter and hasattr(cl.DateFilter, date_filter)
else None,
'model':
ProgramPage if model == 'program' else
DiffusionPage if model == 'diffusion' else
EventPage if model == 'event' else None,
'related': 'related' in request.GET and related,
'siblings': 'siblings' in request.GET,
'asc': 'asc' in request.GET,
}
base_list = cl(**{ k:v for k,v in kwargs.items() if v })
qs = base_list.get_queryset()
# filter by tag
tag = request.GET.get('tag')
if tag:
kwargs['terms'] = tag
qs = qs.filter(tags__name = tag)
# search
search = request.GET.get('search')
if search:
kwargs['terms'] = search
qs = qs.search(search)
set('list_selector', kwargs)
# paginator
if qs:
paginator = Paginator(qs, 30)
try:
qs = paginator.page(request.GET.get('page') or 1)
except PageNotAnInteger:
qs = paginator.page(1)
except EmptyPage:
qs = parginator.page(paginator.num_pages)
set('paginator', paginator)
set('object_list', qs)
return qs
class DatedListBase(models.Model):
"""
List that display items per days. Renders a navigation section on the
top.
"""
nav_days = models.SmallIntegerField(
_('navigation days count'),
default = 7,
help_text = _('number of days to display in the navigation header '
'when we use dates')
)
nav_per_week = models.BooleanField(
_('navigation per week'),
default = False,
help_text = _('if selected, show dates navigation per weeks instead '
'of show days equally around the current date')
)
class Meta:
abstract = True
panels = [
MultiFieldPanel([
FieldPanel('nav_days'),
FieldPanel('nav_per_week'),
], heading=_('Navigation')),
]
@staticmethod
def str_to_date(date):
"""
Parse a string and return a regular date or None.
Format is either "YYYY/MM/DD" or "YYYY-MM-DD" or "YYYYMMDD"
"""
try:
exp = r'(?P<year>[0-9]{4})(-|\/)?(?P<month>[0-9]{1,2})(-|\/)?' \
r'(?P<day>[0-9]{1,2})'
date = re.match(exp, date).groupdict()
return datetime.date(
year = int(date['year']), month = int(date['month']),
day = int(date['day'])
)
except:
return None
def get_nav_dates(self, date):
"""
Return a list of dates availables for the navigation
"""
if self.nav_per_week:
first = date.weekday()
else:
first = int((self.nav_days - 1) / 2)
first = date - tz.timedelta(days = first)
return [ first + tz.timedelta(days=i)
for i in range(0, self.nav_days) ]
def get_date_context(self, date = None):
"""
Return a dict that can be added to the context to be used by
a date_list.
"""
today = tz.now().date()
if not date:
date = today
# next/prev weeks/date bunch
dates = self.get_nav_dates(date)
next = date + tz.timedelta(days=self.nav_days)
prev = date - tz.timedelta(days=self.nav_days)
# context dict
return {
'nav_dates': {
'date': date,
'next': next,
'prev': prev,
'dates': dates,
}
}
#
# Sections
#
@register_snippet
class Section(ClusterableModel):
"""
Section is a container of multiple items of different types
that are used to render extra content related or not the current
page.
A section has an assigned position in the page, and can be restrained
to a given type of page.
"""
name = models.CharField(
_('name'),
max_length=32,
blank = True, null = True,
help_text=_('name of this section (not displayed)'),
)
position = models.CharField(
_('position'),
max_length=16,
blank = True, null = True,
help_text = _('name of the template block in which the section must '
'be set'),
)
model = models.ForeignKey(
ContentType,
verbose_name = _('model'),
blank = True, null = True,
help_text=_('this section is displayed only when the current '
'page or publication is of this type'),
limit_choices_to = related_pages_filter,
)
page = models.ForeignKey(
Page,
verbose_name = _('page'),
blank = True, null = True,
help_text=_('this section is displayed only on this page'),
)
panels = [
MultiFieldPanel([
FieldPanel('name'),
FieldPanel('position'),
FieldPanel('model'),
], heading=_('General')),
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):
"""
Return a queryset of sections that are at the given position.
Filter out Section that are not for the given page.
"""
qs = Section.objects.filter(position = position)
if page:
qs = qs.filter(
models.Q(model__isnull = True) |
models.Q(
model = ContentType.objects.get_for_model(page).pk
) |
models.Q(page = page)
)
return qs
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()
])
class SectionPlace(Orderable):
section = ParentalKey(Section, related_name='places')
item = models.ForeignKey(
'SectionItem',
verbose_name=_('item')
)
panels = [ SnippetChooserPanel('item'), ]
def __str__(self):
return '{}: {}'.format(self.__class__.__name__, self.title or self.pk)
class SectionItemMeta(models.base.ModelBase):
"""
Metaclass for SectionItem, assigning needed values such as `template`.
It needs to load the item's template if the section uses the default
one, and throw error if there is an error in the template.
"""
def __new__(cls, name, bases, attrs):
from django.template.loader import get_template
from django.template import TemplateDoesNotExist
cl = super().__new__(cls, name, bases, attrs)
if not 'template' in attrs:
cl.snake_name = camelcase_to_underscore(name)
cl.template = '{}/sections/{}.html'.format(
cl._meta.app_label,
cl.snake_name,
)
if name != 'SectionItem':
try:
get_template(cl.template)
except TemplateDoesNotExist:
cl.template = 'aircox_cms/sections/section_item.html'
return cl
@register_snippet
class SectionItem(models.Model,metaclass=SectionItemMeta):
"""
Base class for a section item.
"""
real_type = models.CharField(
max_length=32,
blank = True, null = True,
)
title = models.CharField(
_('title'),
max_length=32,
blank = True, null = True,
)
show_title = models.BooleanField(
_('show title'),
default = False,
help_text=_('if set show a title at the head of the section'),
)
css_class = models.CharField(
_('CSS class'),
max_length=64,
blank = True, null = True,
help_text=_('section container\'s "class" attribute')
)
panels = [
MultiFieldPanel([
FieldPanel('title'),
FieldPanel('show_title'),
FieldPanel('css_class'),
], heading=_('General')),
]
@cached_property
def specific(self):
"""
Return a downcasted version of the post if it is from another
model, or itself
"""
if not self.real_type or type(self) != SectionItem:
return self
return getattr(self, self.real_type)
def save(self, *args, **kwargs):
if type(self) != SectionItem and not self.real_type:
self.real_type = type(self).__name__.lower()
return super().save(*args, **kwargs)
def get_context(self, request, page):
"""
Default context attributes:
* self: section being rendered
* page: current page being rendered
* request: request used to render the current page
Other context attributes usable in the default template:
* content: **safe string** set as content of the section
* hide: DO NOT render the section, render only an empty string
"""
return {
'self': self,
'page': page,
'request': request,
}
def render(self, request, page, context, *args, **kwargs):
"""
Render the section. Page is the current publication being rendered.
Rendering is similar to pages, using 'template' attribute set
by default to the app_label/sections/model_name_snake_case.html
If the default template is not found, use SectionItem's one,
that can have a context attribute 'content' that is used to render
content.
"""
context_ = self.get_context(request, *args, page=page, **kwargs)
if context:
context_.update(context)
if context.get('hide'):
return ''
return render_to_string(self.template, context_)
def __str__(self):
return '{}: {}'.format(
(self.real_type or 'section item').replace('section','section '),
self.title or self.pk
)
class SectionRelativeItem(SectionItem):
is_related = models.BooleanField(
_('is related'),
default = False,
help_text=_(
'if set, section is related to the page being processed '
'e.g rendering a list of links will use thoses of the '
'publication instead of an assigned one.'
)
)
class Meta:
abstract=True
panels = SectionItem.panels.copy()
panels[-1] = MultiFieldPanel(
panels[-1].children + [ FieldPanel('is_related') ],
heading = panels[-1].heading
)
def related_attr(self, page, attr):
"""
Return an attribute from the given page if self.is_related,
otherwise retrieve the attribute from self.
"""
return self.is_related and hasattr(page, attr) \
and getattr(page, attr)
@register_snippet
class SectionText(SectionItem):
body = RichTextField()
panels = SectionItem.panels + [
FieldPanel('body'),
]
def get_context(self, request, page):
from wagtail.wagtailcore.rich_text import expand_db_html
context = super().get_context(request, page)
context['content'] = expand_db_html(self.body)
return context
@register_snippet
class SectionImage(SectionRelativeItem):
class ResizeMode(IntEnum):
max = 0x00
min = 0x01
crop = 0x02
image = models.ForeignKey(
'wagtailimages.Image',
verbose_name = _('image'),
related_name='+',
blank=True, null=True,
help_text=_(
'If this item is related to the current page, this image will '
'be used only when the page has not a cover'
)
)
width = models.SmallIntegerField(
_('width'),
blank=True, null=True,
help_text=_('if set and > 0, set a maximum width for the image'),
)
height = models.SmallIntegerField(
_('height'),
blank=True, null=True,
help_text=_('if set 0 and > 0, set a maximum height for the image'),
)
resize_mode = models.SmallIntegerField(
verbose_name = _('resize mode'),
choices = [ (int(y), _(x)) for x,y in ResizeMode.__members__.items() ],
default = int(ResizeMode.max),
help_text=_('if the image is resized, set the resizing mode'),
)
panels = SectionItem.panels + [
ImageChooserPanel('image'),
MultiFieldPanel([
FieldPanel('width'),
FieldPanel('height'),
FieldPanel('resize_mode'),
], heading=_('Resizing'))
]
def get_filter(self):
return \
'original' if not (self.height or self.width) else \
'width-{}'.format(self.width) if not self.height else \
'height-{}'.format(self.height) if not self.width else \
'{}-{}x{}'.format(
self.get_resize_mode_display(),
self.width, self.height
)
def get_context(self, request, page):
context = super().get_context(request, page)
image = self.related_attr(page, 'cover') or self.image
if not image:
return context
if self.width or self.height:
filter_spec = self.get_filter()
filter_spec = (image.id, filter_spec)
url = reverse(
'wagtailimages_serve',
args=(generate_signature(*filter_spec), *filter_spec)
)
else:
url = image.file.url
context['content'] = '<img src="{}"/>'.format(url)
return context
@register_snippet
class SectionLink(RelatedLinkBase, SectionItem):
"""
Render a link to a page or a given url.
Can either be used standalone or in a SectionLinkList
"""
parent = ParentalKey('SectionLinkList', related_name='links',
blank=True, null=True)
panels = SectionItem.panels + RelatedLinkBase.panels
@register_snippet
class SectionLinkList(SectionRelativeItem, ClusterableModel):
"""
Render a list of links. If related to the current page, print
the page's links otherwise, the assigned link list.
Note: assign the link's class to the <a> tag if there is some.
"""
panels = SectionItem.panels + [
InlinePanel('links', label=_('links'), help_text=_(
'If the list is related to the current page, theses links '
'will be used when there is no links found for this publication'
))
]
def get_context(self, request, page):
context = super().get_context(request, page)
links = self.related_attr(page, 'related_link') or self.links
context['object_list'] = links.all()
return context
@register_snippet
class SectionList(ListBase, SectionRelativeItem):
"""
This one is quite badass, but needed: render a list of pages
using given parameters (cf. ListBase).
If focus_available, the first article in the list will be the last
article with a focus, and will be rendered in a bigger size.
"""
focus_available = models.BooleanField(
_('focus available'),
default = False,
help_text = _('if true, highlight the first focused article found')
)
count = models.SmallIntegerField(
_('count'),
default = 5,
help_text = _('number of items to display in the list'),
)
url_text = models.CharField(
_('text of the url'),
max_length=32,
blank = True, null = True,
help_text = _('use this text to display an URL to the complete '
'list. If empty, does not print an address'),
)
panels = SectionRelativeItem.panels + [
MultiFieldPanel([
FieldPanel('focus_available'),
FieldPanel('count'),
FieldPanel('url_text'),
], heading=_('Rendering')),
] + ListBase.panels
def get_context(self, request, page):
from aircox_cms.models import Publication
context = super().get_context(request, page)
if self.is_related:
self.related = page
qs = self.get_queryset()
qs = qs.live()
if self.focus_available:
focus = qs.type(Publication).filter(focus = True).first()
if focus:
focus.css_class = 'focus'
qs = qs.exclude(pk = focus.pk)
else:
focus = None
if not qs.count():
return { 'hide': True }
pages = qs[:self.count - (focus != None)]
context['focus'] = focus
context['object_list'] = pages
if self.url_text:
context['url'] = self.to_url(
list_page = self.is_related and page
)
return context
@register_snippet
class SectionLogsList(SectionItem):
station = models.ForeignKey(
aircox.models.Station,
verbose_name = _('station'),
null = True,
on_delete=models.SET_NULL,
help_text = _('(required) the station on which the logs happened')
)
count = models.SmallIntegerField(
_('count'),
default = 5,
help_text = _('number of items to display in the list (max 100)'),
)
class Meta:
verbose_name = _('list of logs')
verbose_name_plural = _('lists of logs')
panels = SectionItem.panels + [
FieldPanel('station'),
FieldPanel('count'),
]
@staticmethod
def as_item(log):
"""
Return a log object as a DiffusionPage or ListItem.
Supports: Log/Track, Diffusion
"""
from aircox_cms.models import DiffusionPage
print(log, type(log))
if type(log) == aircox.models.Diffusion:
return DiffusionPage.as_item(log)
return ListItem(
title = '{artist} -- {title}'.format(
artist = log.related.artist,
title = log.related.title,
),
summary = log.related.info,
date = log.date,
info = '',
css_class = 'track'
)
def get_context(self, request, page):
context = super().get_context(request, page)
context['object_list'] = [
self.as_item(item)
for item in self.station.on_air(count = min(self.count, 100))
]
return context
@register_snippet
class SectionTimetable(SectionItem,DatedListBase):
class Meta:
verbose_name = _('Section: Timetable')
verbose_name_plural = _('Sections: Timetable')
panels = SectionItem.panels + DatedListBase.panels
def get_queryset(self, context):
from aircox_cms.models import DiffusionPage
diffs = []
for date in context['nav_dates']['dates']:
items = aircox.models.Diffusion.objects.get_at(date).order_by('start')
items = [ DiffusionPage.as_item(item) for item in items ]
diffs.append((date, items))
return diffs
def get_context(self, request, page):
context = super().get_context(request, page)
context.update(self.get_date_context())
context['object_list'] = self.get_queryset(context)
return context
@register_snippet
class SectionPublicationInfo(SectionItem):
class Meta:
verbose_name = _('Section: publication\'s info')
verbose_name_plural = _('Sections: publication\'s info')
@register_snippet
class SectionSearchField(SectionItem):
default_text = models.CharField(
_('default text'),
max_length=32,
default=_('search'),
help_text=_('text to display when the search field is empty'),
)
class Meta:
verbose_name = _('Section: search field')
verbose_name_plural = _('Sections: search field')
panels = SectionItem.panels + [
FieldPanel('default_text'),
]
def get_context(self, request, page):
from aircox_cms.models import GenericPage
context = super().get_context(request, page)
return context
@register_snippet
class SectionPlayer(SectionItem):
live_title = models.CharField(
_('live title'),
max_length = 32,
help_text = _('text to display when it plays live'),
)
streams = models.TextField(
_('audio streams'),
help_text = _('one audio stream per line'),
)
class Meta:
verbose_name = _('Section: Player')
panels = SectionItem.panels + [
FieldPanel('live_title'),
FieldPanel('streams'),
]
def get_context(self, request, page):
context = super().get_context(request, page)
context['streams'] = self.streams.split('\r\n')
return context

20
aircox_cms/settings.py Executable file
View File

@ -0,0 +1,20 @@
import os
from django.conf import settings
def ensure (key, default):
globals()[key] = getattr(settings, key, default)
ensure('AIRCOX_CMS_BLEACH_COMMENT_TAGS', [
'i', 'emph', 'b', 'strong', 'strike', 's',
'p', 'span', 'quote','blockquote','code',
'sup', 'sub', 'a',
])
ensure('AIRCOX_CMS_BLEACH_COMMENT_ATTRS', {
'*': ['title'],
'a': ['href', 'rel'],
})

13
aircox_cms/signals.py Normal file
View File

@ -0,0 +1,13 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
import aircox.models
@receiver(post_save, sender=programs.Program)
def on_new_program(sender, instance, created, *args):
import aircox_cms.models as models
if not created or instance.page.count():
return

View File

@ -0,0 +1,24 @@
/*
input, textarea, select, .richtext, .tagit {
padding: 0.4em;
font-size: 1em;
}
label {
padding-top: 0em;
}
.fields > li, .field-col {
padding-top: 0.2em;
padding-bottom: 0.2em;
}
.full input, .full textarea, .full .richtext {
padding-top: 0.4em;
padding-bottom: 0.4em;
}
.title input, .title textarea, .title .richtext {
font-size: 1.2em;
}
*/

View File

@ -0,0 +1,357 @@
/**
* Define rules for the default layouts, and some useful classes
*/
body {
margin: 0em;
padding: 0em;
}
h1, h2, h3, h4, h5 {
margin: 0.4em 0em;
}
ul {
margin: 0em;
}
/** classes **/
.float_right {
float: right;
}
.float_left {
float: left;
}
.small {
font-size: 0.8em;
}
.icon {
max-width: 2em;
max-height: 2em;
vertical-align: middle;
}
.small_icon {
max-height: 1.5em;
vertical-align: middle;
}
.flex_row {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
flex-direction: row;
}
.flex_column {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
.flex_row > .flex_item,
.flex_column > .flex_item {
-webkit-flex: auto;
flex: auto;
}
/** content: menus **/
nav.menu {
padding: 0.4em;
}
.menu.top {
padding: 0.2em;
height: 2.5em;
margin-bottom: 1em;
background-color: white;
box-shadow: 0em 0em 0.2em black;
}
.menu.top * {
vertical-align: bottom;
}
.menu.top > section {
display: inline-block;
}
.menu.top a {
display: inline-block;
margin: 0.2em 1em;
}
.page_left, .page_right {
max-width: 16em;
}
.page_left > section,
.page_right > section {
margin-bottom: 1em;
}
/** content: list & items **/
.list {
width: 100%;
}
ul.list {
padding: 0.4em;
}
.list_item {
margin: 0.4em 0;
}
.list_item > *:not(:last-child) {
margin-right: 0.4em;
}
.list_item img.cover.big {
display: block;
}
.list_item img.cover.small {
margin-right: 0.4em;
border-radius: 0.4em;
float: left;
min-height: 64px;
}
.list_item > * {
margin: 0em 0.2em;
vertical-align: middle;
}
.list nav {
text-align: center;
font-size: 0.9em;
}
/** content: date list **/
.date_list nav {
text-align:center;
}
.date_list nav a {
display: inline-block;
width: 4em;
}
.date_list nav a[selected] {
color: #007EDF;
border-bottom: 0.2em #007EDF dotted;
}
.date_list ul:not([selected]) {
display: none;
}
.date_list ul:target {
display: block;
}
.date_list h2 {
display: none;
}
.date_list_item .cover.small {
width: 64px;
margin: 0.4em;
}
.date_list_item h3 {
margin-top: 0em;
}
/** content: comments **/
.comments form input:not([type=checkbox]),
.comments form textarea {
display: inline-block;
width: 100%;
max-height: 6em;
margin: 0.2em 0em;
padding: 0.2em;
}
.comments form input[type=checkbox],
.comments form button[type=submit] {
vertical-align:bottom;
margin: 0.2em 0em;
text-align: center;
}
.comments form button[type=submit] {
float: right;
}
.comments form #show_more:not(:checked) ~ .extra {
display: none;
}
.comments label[for="show_more"] {
font-size: 0.8em;
}
.comments ul {
margin-top: 2.5em;
}
.comment {
list-style: none;
border: 1px #818181 dotted;
margin: 0.4em 0em;
}
.comment .metadata {
font-size: 0.9em;
}
.comment time {
float: right;
}
/** content: player **/
.player {
}
.player:not([seekable]) > .controls > .progress {
display: none;
}
.player .controls {
margin-top: 1em;
text-align: right;
}
.player .controls > * {
margin: 0em 0.2em;
}
.player .controls .single {
display: none;
}
.player .controls .single + label {
display: inline-block;
font-size: 1em;
padding: 0.1em;
width: 1.5em;
height: 1.0em;
text-align: center;
box-shadow: inset 0em 0em 0.1em #818181;
}
.player .controls .single:not(:checked) + label {
border-left: 2px #818181 solid;
color: black;
}
.player .controls .single:checked + label {
border-right: 2px #818181 solid;
}
.player .playlist .item {
margin: 0em;
padding: 0.2em 0.4em;
cursor: pointer;
}
.player .playlist .item:hover {
color: #007EDF;
}
.player .item > * {
vertical-align: middle;
}
.player .playlist .item .actions {
display: none;
font-size: 0.9em;
}
.player .playlist .item:hover .actions {
display: inline;
}
.player .item[selected] {
border-left: 1px #007EDF solid;
font-size: 1.0em;
}
.player .item:not([selected]) {
}
.player .button {
display: inline-block;
cursor: pointer;
height: 2.0em;
background: none;
border: none;
font-size: 1.4em;
}
.player .button > img {
max-height: 2.0em;
}
.player:not([state]) .item[selected] .button > img:not(.play),
.player[state="paused"] .item[selected] .button > img:not(.play),
.player[state="playing"] .item[selected] .button > img:not(.pause),
.player[state="loading"] .item[selected] .button > img:not(.loading)
{
display: none;
}
.player .item:not([selected]) .button > img.play {
display: block;
}
.player .item:not([selected]) .button > img:not(.play) {
display: none;
}
main .player .actions .action:not(.add),
.section_player .actions .action.add,
.player .list_item.live:hover .actions {
display: none;
}
/** content: page **/
main .body ~ section:not(.comments) {
width: calc(50% - 1em);
vertical-align: top;
display: inline-block;
}
.meta > .share {
margin-top: 0.8em;
}
.meta .author .summary {
display: none;
}
/** content: others **/
.list_item.track .title {
display: inline;
font-style: italic;
font-weight: normal;
font-size: 0.9em;
}

View File

@ -0,0 +1,179 @@
/*
* Define a default theme, that is the one for RadioCampus
*
* Colors:
* - light:
* - background: #F2F2F2
* - color: #000
*
* - dark:
* - background: #212121
* - color: #007EDF
*
* - info:
* - generic (time,url,...): #616161
* - additional: #007EDF
* - active: #007EDF
*/
/** main **/
body {
background-color: #F2F2F2;
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
margin: 0em;
padding: 0em;
}
h1, h2, h3 {
font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif;
}
h1:first-letter, h2:first-letter, h3:first-letter {
text-transform: capitalize;
}
h1 { font-size: 1.4em; }
h2 { font-size: 1.2em; }
h3 { font-size: 0.9em; }
h2 * { vertical-align: middle; }
/** info **/
time, .tags {
font-size: 0.9em;
color: #616161;
}
.info {
font-size: 0.9em;
padding: 0.1em;
color: #007EDF;
}
a {
cursor: pointer;
text-decoration: none;
color: #616161;
}
a:hover {
color: #007EDF;
}
a:hover > .small_icon {
box-shadow: 0em 0em 0.1em #007EDF;
}
.error { color: red; }
.warning { color: orange; }
.success { color: green; }
/** page **/
.page > nav {
width: 16em;
overflow: hidden;
}
main {
background-color: rgba(255,255,255,0.9);
padding: 1em;
margin: 0em 2em;
box-shadow: 0em 0em 0.2em black;
width: 60%;
}
main:not(.detail) h1 {
margin: 0em 0em 0.4em 0em;
}
main.detail {
padding: 0em;
}
main.detail > .content {
padding: 1em;
}
main.detail > header {
padding: 0em;
margin: 0em;
}
main.detail > header h1.title {
position: relative;
width: calc(70% - 0.8em);
margin: 0em;
margin-bottom: -2em;
padding: 0.4em;
background-color: rgba(255,255,255,0.8);
}
main.detail > header img.cover {
width: 70%;
vertical-align: middle;
display: inline-block;
}
main.detail header .summary {
display: inline-block;
padding: 1em;
width: calc(20% - 2em);
vertical-align: middle;
font-size: 1.2em;
font-weight: bold;
background-color: white;
}
/** player **/
.player[state='playing'] .item[selected] .button > img {
animation-duration: 4s;
animation-iteration-count: infinite;
animation-name: blink;
}
@keyframes blink {
from {
opacity: 1.0;
}
50% {
opacity: 0.3;
}
to {
opacity: 1.0;
}
}
.player[state="loading"] .item[selected] .button > img.loading {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-name: rotate;
animation-timing-function: linear;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,130 @@
This is a human-readable summary of (and not a substitute for) the license.
http://creativecommons.org/licenses/by-sa/4.0/
------
Disclaimer
This license is acceptable for Free Cultural Works.
You are free to:
Share — copy and redistribute the material in any medium or format
Adapt — remix, transform, and build upon the material
for any purpose, even commercially.
The licensor cannot revoke these freedoms as long as you follow the license terms.
Under the following terms:
Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
------
Creative Commons Attribution-ShareAlike 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 Definitions.
Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
Licensor means the individual(s) or entity(ies) granting rights under this Public License.
Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 Scope.
License grant.
Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
reproduce and Share the Licensed Material, in whole or in part; and
produce, reproduce, and Share Adapted Material.
Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
Term. The term of this Public License is specified in Section 6(a).
Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
Downstream recipients.
Offer from the Licensor Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
Additional offer from the Licensor Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapters License You apply.
No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
Other rights.
Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
Patent and trademark rights are not licensed under this Public License.
To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
Attribution.
If You Share the Licensed Material (including in modified form), You must:
retain the following if it is supplied by the Licensor with the Licensed Material:
identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
a copyright notice;
a notice that refers to this Public License;
a notice that refers to the disclaimer of warranties;
a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
ShareAlike.
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
The Adapters License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 Disclaimer of Warranties and Limitation of Liability.
Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 Other Terms and Conditions.
The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 Interpretation.
For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.

View File

@ -0,0 +1,14 @@
# Android Developer Icons
Android Developer Icons is a custom icon set, created by [Opoloo](http://www.opoloo.com/). Included are:
* 250 hand-crafted, pixel-perfect icons in 5 sizes and 14 colors
* an icon font, made from the set
* all sources: .svg, .ai, .eps, .eot, .ttf, .woff
## License
[Attribution-ShareAlike 4.0 International CC BY-SA 4.0]
(http://creativecommons.org/licenses/by-sa/4.0/)
## More
Were always happy to hear from you, whether its a question about the icon set or giving us a heads-up where you used our beautiful little icons in a project. If you like, follow us at [Google+](https://plus.google.com/u/0/b/104776915031333350956/+Opoloo/posts), on [Twitter](https://twitter.com/Opoloo), or [shoot us an email](mailto: info@opoloo.com).

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,423 @@
// TODO
// - live streams as item;
// - add to playlist button
//
/// Return a human-readable string from seconds
function duration_str(seconds) {
seconds = Math.floor(seconds);
var hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
var minutes = Math.floor(seconds / 60);
seconds -= minutes * 60;
var str = hours ? (hours < 10 ? '0' + hours : hours) + ':' : '';
str += (minutes < 10 ? '0' + minutes : minutes) + ':';
str += (seconds < 10 ? '0' + seconds : seconds);
return str;
}
function Sound(title, detail, duration, streams, cover, on_air) {
this.title = title;
this.detail = detail;
this.duration = duration;
this.streams = streams.splice ? streams.sort() : [streams];
this.cover = cover;
this.on_air = on_air;
}
Sound.prototype = {
title: '',
detail: '',
streams: undefined,
duration: undefined,
cover: undefined,
on_air: false,
item: undefined,
get seekable() {
return this.duration != undefined;
},
make_item: function(playlist, base_item) {
if(this.item)
return;
var item = base_item.cloneNode(true);
item.removeAttribute('style');
item.querySelector('.title').innerHTML = this.title;
if(this.seekable)
item.querySelector('.duration').innerHTML =
duration_str(this.duration);
if(this.detail)
item.querySelector('.detail').href = this.detail;
if(playlist.player.show_cover && this.cover)
item.querySelector('img.play').src = this.cover;
item.sound = this;
this.item = item;
// events
var self = this;
item.querySelector('.action.remove').addEventListener(
'click', function(event) { playlist.remove(self); }, false
);
item.querySelector('.action.add').addEventListener(
'click', function(event) {
player.playlist.add(new Sound(
title = self.title,
detail = self.detail,
duration = self.duration,
streams = self.streams,
cover = self.cover,
on_air = self.on_air
));
}, false
);
item.addEventListener('click', function(event) {
if(event.target.className.indexOf('action') != -1)
return;
playlist.select(self, true)
}, false);
},
}
function Playlist(player) {
this.player = player;
this.playlist = player.player.querySelector('.playlist');
this.item_ = player.player.querySelector('.playlist .item');
this.sounds = []
}
Playlist.prototype = {
on_air: undefined,
sounds: undefined,
sound: undefined,
/// Find a sound by its streams, and return it if found
find: function(streams) {
streams = streams.splice ? streams.sort() : streams;
return this.sounds.find(function(sound) {
// comparing array
if(!sound.streams || sound.streams.length != streams.length)
return false;
for(var i = 0; i < streams.length; i++)
if(sound.streams[i] != streams[i])
return false;
return true
});
},
add: function(sound, container) {
var sound_ = this.find(sound.streams);
if(sound_)
return sound_;
if(sound.on_air)
this.on_air = sound;
sound.make_item(this, this.item_);
(container || this.playlist).appendChild(sound.item);
this.sounds.push(sound);
this.save();
return sound;
},
remove: function(sound) {
var index = this.sounds.indexOf(sound);
if(index != -1)
this.sounds.splice(index,1);
this.playlist.removeChild(sound.item);
this.save();
this.player.stop()
this.next(false);
},
select: function(sound, play = true) {
this.player.playlist = this;
if(this.sound == sound) {
if(play)
this.player.play();
return;
}
if(this.sound)
this.unselect(this.sound);
this.sound = sound;
// audio
this.player.load_sound(this.sound);
// attributes
var container = this.player.player;
sound.item.setAttribute('selected', 'true');
if(!sound.on_air)
sound.item.querySelector('.content').insertBefore(
this.player.progress.item,
sound.item.querySelector('.content .duration')
)
if(sound.seekable)
container.setAttribute('seekable', 'true');
else
container.removeAttribute('seekable');
// play
if(play)
this.player.play();
},
unselect: function(sound) {
sound.item.removeAttribute('selected');
},
next: function(play = true) {
var index = this.sounds.indexOf(this.sound);
if(index < 0)
return;
index++;
if(index < this.sounds.length)
this.select(this.sounds[index]);
},
// storage
save: function() {
var list = [];
for(var i in this.sounds) {
var sound = Object.assign({}, this.sounds[i])
delete sound.item;
list.push(sound);
}
this.player.store.set('playlist', list);
},
load: function() {
var list = this.player.store.get('playlist');
var container = document.createDocumentFragment();
for(var i in list) {
var sound = list[i];
sound = new Sound(sound.title, sound.detail, sound.duration,
sound.streams, sound.cover, sound.on_air)
this.add(sound, container)
}
this.playlist.appendChild(container);
},
}
function Player(id, on_air_url, show_cover) {
this.id = id;
this.on_air_url = on_air_url;
this.show_cover = show_cover;
this.store = new Store('player_' + id);
// html sounds
this.player = document.getElementById(id);
this.audio = this.player.querySelector('audio');
this.on_air = this.player.querySelector('.on_air');
this.progress = {
item: this.player.querySelector('.controls .progress'),
bar: this.player.querySelector('.controls .progress progress'),
duration: this.player.querySelector('.controls .progress .duration')
}
this.controls = {
single: this.player.querySelector('input.single'),
}
this.playlist = new Playlist(this);
this.playlist.load();
this.init_events();
this.load();
this.update_on_air();
}
Player.prototype = {
/// current item being played
sound: undefined,
on_air_url: undefined,
init_events: function() {
var self = this;
function time_from_progress(event) {
bounding = self.progress.bar.getBoundingClientRect()
offset = (event.clientX - bounding.left);
return offset * self.audio.duration / bounding.width;
}
function update_info() {
var progress = self.progress;
var pos = self.audio.currentTime;
// progress
if(!self.sound || !self.sound.seekable ||
!pos || self.audio.duration == Infinity)
{
progress.duration.innerHTML = '';
progress.bar.value = 0;
return;
}
progress.bar.value = pos;
progress.bar.max = self.audio.duration;
progress.duration.innerHTML = duration_str(pos);
}
// audio
this.audio.addEventListener('playing', function() {
self.player.setAttribute('state', 'playing');
}, false);
this.audio.addEventListener('pause', function() {
self.player.setAttribute('state', 'paused');
}, false);
this.audio.addEventListener('loadstart', function() {
self.player.setAttribute('state', 'loading');
}, false);
this.audio.addEventListener('loadeddata', function() {
self.player.removeAttribute('state');
}, false);
this.audio.addEventListener('timeupdate', update_info, false);
this.audio.addEventListener('ended', function() {
self.player.removeAttribute('state');
if(!self.controls.single.checked)
self.next(true);
}, false);
// progress
progress = this.progress.bar;
progress.addEventListener('click', function(event) {
player.audio.currentTime = time_from_progress(event);
}, false);
progress.addEventListener('mouseout', update_info, false);
progress.addEventListener('mousemove', function(event) {
if(self.audio.duration == Infinity || isNaN(self.audio.duration))
return;
var pos = time_from_progress(event);
self.progress.duration.innerHTML = duration_str(pos);
}, false);
},
update_on_air: function() {
if(!this.on_air_url)
return;
var self = this;
window.setTimeout(function() {
self.update_on_air();
}, 60*2000);
if(!this.playlist.on_air)
return;
var req = new XMLHttpRequest();
req.open('GET', this.on_air_url, true);
req.onreadystatechange = function() {
if(req.readyState != 4 || (req.status != 200 &&
req.status != 0))
return;
var data = JSON.parse(req.responseText)
if(data.type == 'track')
data = {
title: '♫ ' + (data.artist ? data.artist + ' — ' : '') +
data.title,
url: ''
}
else
data = {
title: data.title,
info: '',
url: data.url
}
var on_air = self.playlist.on_air;
on_air = on_air.item.querySelector('.content');
if(data.url)
on_air.innerHTML =
'<a href="' + data.url + '">' + data.title + '</a>';
else
on_air.innerHTML = data.title;
};
req.send();
},
play: function() {
if(this.audio.paused)
this.audio.play();
else
this.audio.pause();
},
stop: function() {
this.audio.pause();
this.player.removeAttribute('state');
},
__mime_type: function(path) {
ext = path.substr(path.lastIndexOf('.')+1);
return 'audio/' + ext;
},
load_sound: function(sound) {
var audio = this.audio;
audio.pause();
var sources = audio.querySelectorAll('source');
for(var i = 0; i < sources.length; i++)
audio.removeChild(sources[i]);
streams = sound.streams;
for(var i = 0; i < streams.length; i++) {
var source = document.createElement('source');
source.src = streams[i];
source.type = this.__mime_type(source.src);
audio.appendChild(source);
}
audio.load();
},
save: function() {
// TODO: move stored sound into playlist
this.store.set('player', {
single: this.controls.single.checked,
sound: this.playlist.sound && this.playlist.sound.streams,
});
},
load: function() {
var data = this.store.get('player');
if(!data)
return;
this.controls.single.checked = data.single;
if(data.sound)
this.playlist.sound = this.playlist.find(data.sound);
},
}

View File

@ -0,0 +1,68 @@
/// Helper to provide a tab+panel functionnality; the tab and the selected
/// element will have an attribute "selected".
/// We assume a common ancestor between tab and panel at a maximum level
/// of 2.
/// * tab: corresponding tab
/// * panel_selector is used to select the right panel object.
function select_tab(tab, panel_selector) {
var parent = tab.parentNode.parentNode;
var panel = parent.querySelector(panel_selector);
// unselect
var qs = parent.querySelectorAll('*[selected]');
for(var i = 0; i < qs.length; i++)
if(qs[i] != tab && qs[i] != panel)
qs[i].removeAttribute('selected');
panel.setAttribute('selected', 'true');
tab.setAttribute('selected', 'true');
}
/// Utility to store objects in local storage. Data are stringified in JSON
/// format in order to keep type.
function Store(prefix) {
this.prefix = prefix;
}
Store.prototype = {
// save data to localstorage, or remove it if data is null
set: function(key, data) {
key = this.prefix + '.' + key;
if(data == undefined) {
localStorage.removeItem(this.prefix);
return;
}
localStorage.setItem(key, JSON.stringify(data))
},
// load data from localstorage
get: function(key) {
try {
key = this.prefix + '.' + key;
var data = localStorage.getItem(key);
if(data)
return JSON.parse(data);
}
catch(e) { console.log(e, data); }
},
// return true if the given item is stored
exists: function(key) {
key = this.prefix + '.' + key;
return (localStorage.getItem(key) != null);
},
// update a field in the stored data
update: function(key, field_key, value) {
data = this.get(key) || {};
if(value)
data[field_key] = value;
else
delete data[field_key];
this.set(key, data);
},
}

View File

@ -0,0 +1,82 @@
{% load staticfiles %}
{% load i18n %}
{% load wagtailimages_tags %}
{% load wagtailsettings_tags %}
{% load aircox_cms %}
{% get_settings %}
<html>
<head>
<meta charset="utf-8">
<meta name="application-name" content="aircox-cms">
<meta name="description" content="{{ settings.cms.WebsiteSettings.description }}">
<meta name="keywords" content="{{ page.tags.all|default:settings.cms.WebsiteSettings.tags }}">
{% with favicon=settings.cms.WebsiteSettings.favicon %}
<link rel="icon" href="{{ favicon.url }}" />
{% endwith %}
{% block css %}
<link rel="stylesheet" href="{% static 'aircox_cms/css/layout.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'aircox_cms/css/theme.css' %}" type="text/css" />
{% block css_extras %}{% endblock %}
{% endblock %}
<script src="{% static 'aircox_cms/js/utils.js' %}"></script>
<script src="{% static 'aircox_cms/js/player.js' %}"></script>
<title>{{ page.title }}</title>
</head>
<body>
<div class="menu top">
{% render_sections position="top" %}
</div>
<header class="header">
{% render_sections position="header" %}
</header>
<div class="page flex_row">
<nav class="menu page_left flex_item">
{% render_sections position="page_left" %}
</nav>
<main class="flex_item {% if not object_list %}detail{% endif %}">
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
<header>
{% block title %}
<h1>{{ page.title }}</h1>
{% endblock %}
</header>
{% block content %}
{% endblock %}
</main>
<nav class="menu page_right flex_item">
{% render_sections position="page_right" %}
</nav>
</div>
{% block footer %}
<footer class="footer">
{% render_sections position="footer" %}
<div class="small float_right">Propulsed by
<a href="https://github.com/bkfox/aircox">Aircox</a>
</div>
</footer>
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,15 @@
{% extends "aircox_cms/base_site.html" %}
{# display a timetable of planified diffusions by days #}
{% load wagtailcore_tags %}
{% block content %}
{% if page.body %}
<div class="body">
{{ page.body|richtext }}
</div>
{% endif %}
{% include "aircox_cms/snippets/date_list.html" %}
{% endblock %}

View File

@ -0,0 +1,68 @@
{% extends "aircox_cms/publication.html" %}
{% load i18n %}
{% block content_extras %}
{% with tracks=page.tracks.all %}
{% if tracks %}
<section class="playlist">
<h2>{% trans "Playlist" %}</h2>
<ul>
{% for track in tracks %}
<li><span class="artist">{{ track.artist }}</span>
<span class="title">{{ track.title }}</span>
{% if track.info %} <span class="info">{{ track.info }}</span>{% endif %}
</li>
{% endfor %}
</ul>
</section>
{% endif %}
{% endwith %}
<section class="dates">
<h2>{% trans "Dates of diffusion" %}</h2>
<ul>
{% with diffusion=page.diffusion %}
<li>{{ diffusion.date|date:"l d F Y, H:i" }}</li>
{% for diffusion in diffusion.diffusion_set.all %}
<li>{{ diffusion.date|date:"l d F Y, H:i" }}</li>
{% endfor %}
{% endwith %}
</ul>
</section>
{% with podcasts=self.get_podcasts %}
{% if podcasts %}
<section class="podcasts list">
<h2>{% trans "Podcasts" %}</h2>
<div id="player_diff_{{ page.id }}" class="player">
{% include 'aircox_cms/snippets/player.html' %}
<script>
var podcasts = new Player('player_diff_{{ page.id }}', undefined, true)
{% for item in podcasts %}
{% if not item.embed %}
podcasts.playlist.add(new Sound(
title='{{ item.name|escape }}',
detail='{{ item.detail_url }}',
duration={{ item.duration|date:"H*3600+i*60+s" }},
streams='{{ item.url }}',
{% if page and page.cover %}cover='{{ page.icon }}',{% endif %}
undefined
));
{% endif %}
{% endfor %}
</script>
<p>
{% for item in podcasts %}
{% if item.embed %}
{{ item.embed|safe }}
{% endif %}
{% endfor %}
</p>
</div>
</section>
{% endif %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "aircox_cms/publication.html" %}
{% load i18n %}
{% block content %}
<div>
<h2>{% trans "Practical information" %}</h2>
<ul>
{% with start=page.start|date:'l d F H:i' %}
{% with end=page.end|date:'l d F H:i' %}
<li><b>{% trans "Date" %}</b>:
{% transblock %}{{ start }} until {{ end }}{% endtransblock %}
</li>
<li><b>{% trans "Place" %}</b>: {{ page.address }}</li>
{% if page.price %}
<li><b>{% trans "Price" %}</b>: {{ page.price }}</li>
{% endif %}
{% if page.info %}<li>{{ page.info }}</li>{% endif %}
{% endwith %}
{% endwith %}
</ul>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% 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

@ -0,0 +1,34 @@
{% extends "aircox_cms/publication.html" %}
{# generic page to display programs #}
{% load i18n %}
{% load wagtailcore_tags %}
{# TODO message if program is no more active #}
{% block content_extras %}
<section class="schedule">
{% if page.program.active %}
<h2>{% trans "Schedule" %}</h2>
<ul>
{% for schedule in page.program.schedule_set.all %}
<li>
{% with frequency=schedule.get_frequency_display day=schedule.date|date:'l' %}
{% with start=schedule.date|date:"H:i" end=schedule.end|date:"H:i" %}
{% blocktrans %}
{{ day }} {{ start }} until {{ end }}, {{ frequency }}
{% endblocktrans %}
{% endwith %}
{% endwith %}
{% if schedule.initial %}
<span class="info">{% trans "Rerun" %}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div class="warning">{% trans "This program is no longer active" %}</div>
{% endif %}
</section>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "aircox_cms/base_site.html" %}
{% load i18n %}
{% load wagtailcore_tags %}
{% load wagtailimages_tags %}
{% load aircox_cms %}
{% if not object_list %}
{% block title %}
<h1 class="title">{{ page.title }}</h1>
{% if page.cover %}
{% image page.cover max-600x480 class="cover" height="" width="" %}
{% endif %}
<section class="summary">
{% if page.summary %}
{{ page.summary }}
{% else %}
{{ page.body|richtext|truncatewords:24 }}
{% endif %}
</section>
{% endblock %}
{% endif %}
{% block content %}
{% if object_list %}
{# list view #}
<section class="body summary">
{{ page.summary }}
<a href="?" class="go_back">{% trans "Go back to the publication" %}</a>
</section>
{% with list_paginator=paginator %}
{% include "aircox_cms/snippets/list.html" %}
{% endwith %}
{% else %}
{# detail view #}
<div class="content">
<section class="body">
{{ page.body|richtext}}
</section>
{% block content_extras %}{% endblock %}
<div class="post_content">
{% render_sections position="post_content" %}
</div>
<section class="comments">
{% include "aircox_cms/snippets/comments.html" %}
</section>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,8 @@
<section class="section_item {{ self.css_class }} {{ self.snake_name }}">
{% block title %}
{% if self.show_title %}<h2>{{ self.title }}</h2>{% endif %}
{% endblock %}
{% block content %}{{ content|safe }}{% endblock %}
</section>

View File

@ -0,0 +1,13 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% load wagtailimages_tags %}
{% block content %}
{% with link=self.as_dict %}
<a href="{{ link.url }}">
{% if link.icon %}{% image link.icon fill-32x32 class="icon link_icon" height='' width='' %}{% endif %}
{{ link.text }}
</a>
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% load wagtailimages_tags %}
{% block content %}
{% for item in object_list %}
{% with link=item.as_dict %}
<a href="{{ link.url }}"
{% if item.css_class %}class="{{ item.css_class }}"{% endif %}>
{% if link.icon %}{% image link.icon fill-24x24 class="icon" %}{% endif %}
{{ link.text }}
</a>
{% endwith %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% block content %}
{% if focus %}
{% with item=focus item_big_cover=True %}
{% include "aircox_cms/snippets/list_item.html" %}
{% endwith %}
{% endif %}
{% with url=url url_text=self.url_text %}
{% include "aircox_cms/snippets/list.html" %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% block content %}
{% with item_date_format="H:i" list_css_class="date_list" list_no_cover=True %}
{% for item in object_list %}
{% include "aircox_cms/snippets/date_list_item.html" %}
{% endfor %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends 'aircox_cms/sections/section_item.html' %}
{% block content %}
<div id="player" class="player">
{% include "aircox_cms/snippets/player.html" %}
</div>
<script>
var player = new Player('player', '{% url 'aircox.on_air' %}');
var sound = player.playlist.add(
new Sound(
'{{ self.live_title }}',
'', undefined,
streams=[ {% for stream in streams %}'{{ stream }}',{% endfor %} ],
cover = undefined,
on_air = true
)
);
sound.item.className += ' live';
player.playlist.select(sound, false);
</script>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% load i18n %}
{% load static %}
{% load wagtailsettings_tags %}
{% block content %}
<div class="meta">
<div class="author">
{% if page.publish_as %}
{% with item=page.publish_as item_date_format='' %}
{% include "aircox_cms/snippets/list_item.html" %}
{% endwith %}
{% elif page.owner %}
{% trans "Published by" %}
{{ page.owner }}
{% endif %}
</div>
{% with page_date=page.specific.date %}
{% if page_date %}
<time datetime="{{ page_date }}">
<b>{% trans "Published on " %}</b>
{{ page_date|date:'l d F, H:i' }}
</time>
{% endif %}
{% endwith %}
{% with list_page=settings.cms.WebsiteSettings.list_page %}
{% if list_page and page.tags.count %}
<div class="tags"><b>{% trans "Tags" %}</b>
{% for tag in page.tags.all %}
<a href="{{ list_page }}?tag={{ tag }}">{{ tag }}</a>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="share">
<img src="{% static "aircox_cms/images/share.png" %}" alt="{% trans "Share" %}"
class="small_icon">
<a href="mailto:?&body={{ page.full_url|urlencode }}"
target="new">
<img src="{% static "aircox_cms/images/mail.png" %}" alt="Mail" class="small_icon">
</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{ page.full_url|urlencode }}"
target="new">
<img src="{% static "aircox_cms/images/facebook.png" %}" alt="Facebook" class="small_icon">
</a>
<a href="https://twitter.com/intent/tweet?text={{ page.full_url|urlencode }}"
target="new">
<img src="{% static "aircox_cms/images/twitter.png" %}" alt="Twitter" class="small_icon">
</a>
<a href="https://plus.google.com/share?url={{ page.full_url|urlencode }}"
target="new">
<img src="{% static "aircox_cms/images/gplus.png" %}" alt="Google Plus" class="small_icon">
</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% load i18n %}
{% load static %}
{% load wagtailsettings_tags %}
{% block content %}
{% with list_page=settings.cms.WebsiteSettings.list_page %}
<form action="{{ list_page.url }}" method="GET">
<img src="{% static "aircox_cms/images/search.png" %}" class="icon"/>
<input type="text" name="search" placeholder="{{ self.default_text }}">
<input type="submit" style="display: none;">
</form>
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "aircox_cms/sections/section_item.html" %}
{% block content %}
{% include "aircox_cms/snippets/date_list.html" %}
{% endblock %}

View File

@ -0,0 +1,55 @@
{% load i18n %}
{% load static %}
{% load honeypot %}
{% if comment_form or page.comments %}
<h2><img src="{% static "aircox_cms/images/comments.png" %}" class="icon">{% trans "Comments" %}</h2>
{% endif %}
{% if comment_form %}
{% with comment_form as form %}
{{ form.non_field_errors }}
<form action="" method="POST">
{% csrf_token %}
{% render_honeypot_field "hp_website" %}
<div>
<input type="hidden" name="type" value="comments">
{{ form.author.errors }}
{{ form.author }}
<div>
<input type="checkbox" value="1" id="show_more">
<label for="show_more">{% trans "show more options" %}</label>
<div class="extra">
{{ form.email.errors }}
{{ form.email }}
{{ form.url.errors }}
{{ form.url }}
</div>
</div>
</div>
<div>
{{ form.content.errors }}
{{ form.content }}
<button type="submit">{% trans "Post!" %}</button>
</div>
</form>
{% endwith %}
{% endif %}
<ul class="list">
{% for comment in page.comments %}
<li class="comment">
<div class="metadata">
<a {% if comment.url %}href="{{ comment.url }}" {% endif %}
class="author">{{ comment.author }}</a>
<time datetime="{{ comment.date }}">
{{ comment.date|date:'l d F, H:i' }}
</time>
</div>
<div class="body">{{ comment.content }}</div>
</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,39 @@
{% load i18n %}
{# FIXME: get current complete URL #}
<div class="list date_list">
{% if nav_dates %}
<nav class="nav_dates">
{% if nav_dates.prev %}
<a href="?date={{ nav_dates.prev|date:"Y-m-d" }}" title="{% trans "previous days" %}"></a>
{% endif %}
{% for day in nav_dates.dates %}
<a onclick="select_tab(this, '.panel[data-date=\'{{day|date:"Y-m-d"}}\']');"
{% if day == nav_dates.date %}selected{% endif %}
class="tab {% if day == nav_dates.date %}today{% endif %}">
{{ day|date:'D. d' }}
</a>
{% endfor %}
{% if nav_dates.next %}
<a href="?date={{ nav_dates.next|date:"Y-m-d" }}" title="{% trans "next days" %}"></a>
{% endif %}
</nav>
{% endif %}
{% for day, list in object_list %}
<ul class="panel {% if day == nav_dates.date %}class="today"{% endif %}"
{% if day == nav_dates.date %}selected{% endif %}
data-date="{{day|date:"Y-m-d"}}">
{# you might like to hide it by default -- this more for sections #}
<h2>{{ day|date:'l d F' }}</h2>
{% with item_date_format="H:i" %}
{% for item in list %}
{% include "aircox_cms/snippets/date_list_item.html" %}
{% endfor %}
{% endwith %}
</ul>
{% endfor %}
</div>

View File

@ -0,0 +1,45 @@
{% comment %}
Configurable item to be put in a dated list. Work like list_item, the layout
is just a bit different.
{% endcomment %}
{% load wagtailimages_tags %}
<a {% if item.url %}href="{{ item.url }}" {% endif %}
class="list_item date_list_item {% if not item_big_cover %}flex_row {% endif %}{% if item.css_class %}{{ item.css_class }}{% endif %}">
{% if not item.show_in_menus and item.date and item_date_format != '' %}
{% with date_format=item_date_format|default_if_none:'l d F, H:i' %}
<time datetime="{{ item.date }}">
{{ item.date|date:date_format }}
</time>
{% endwith %}
{% endif %}
{% if not list_no_cover %}
{% if item_big_cover %}
{% image item.cover max-640x480 class="cover big" height="" width="" %}
{% elif item.cover %}
{% image item.cover fill-64x64 class="cover small" %}
{% else %}
<div class="cover small"></div>
{% endif %}
{% endif %}
<div class="flex_item">
<h3 class="title">{{ item.title }}</h3>
{% if item.summary %}<div class="summary">{{ item.summary }}</div>{% endif %}
{% if item.info %}
<span class="info">{{ item.info|safe }}</span>
{% endif %}
{% if item.extra %}
<div class="extra"></div>
{% endif %}
</div>
</a>

View File

@ -0,0 +1,65 @@
{% comment %}
Options:
- list_css_class: extra class for the main list container
- list_paginator: paginator object to display pagination at the bottom;
{% endcomment %}
{% load i18n %}
{% load aircox_cms %}
<ul class="list {{ list_css_class|default:'' }}">
{% for page in object_list %}
{% with item=page.specific %}
{% include "aircox_cms/snippets/list_item.html" %}
{% endwith %}
{% endfor %}
{# we use list_paginator to avoid conflicts when there are multiple lists #}
{% if list_paginator and list_paginator.num_pages > 1 %}
<nav>
{% with list_paginator.num_pages as num_pages %}
{% if object_list.has_previous %}
<a href="?page={{ object_list.previous_page_number }}">
{% trans "previous page" %}
</a>
{% endif %}
{% if object_list.number > 3 %}
<a href="?page=1">1</a>
{% if object_list.number > 4 %}
&#8230;
{% endif %}
{% endif %}
{% for i in object_list.number|around:2 %}
{% if i == object_list.number %}
{{ object_list.number }}
{% elif i > 0 and i <= num_pages %}
<a href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% with object_list.number|add:"2" as max %}
{% if max < num_pages %}
{% if max|add:"1" < num_pages %}
&#8230;
{% endif %}
<a href="?page={{ num_pages }}">{{ num_pages }}</a>
{% endif %}
{% endwith %}
{% if object_list.has_next %}
<a href="?page={{ object_list.next_page_number }}">
{% trans "next page" %}
</a>
{% endif %}
{% endwith %}
</nav>
{% elif url and url_text %}
<nav><a href="{{ url }}">{{ url_text }}</a></nav>
{% endif %}
</ul>

View File

@ -0,0 +1,51 @@
{% comment %}
Configurable item to be put in a list. Support standard Publication or
ListItem instance.
Options:
* item: item to render. Fields: title, summary, cover, url, date, info, css_class
* item_date_format: format passed to the date filter instead of default one. If
it is an empty string, do not print the date.
* item_big_cover: cover should is big instead of thumbnail (width: 600)
{% endcomment %}
{% load static %}
{% load wagtailimages_tags %}
<a {% if item.url %}href="{{ item.url }}" {% endif %}
class="list_item {% if not item_big_cover %}flex_row {% endif %}{% if item.css_class %}{{ item.css_class }}{% endif %}">
{% if item.cover %}
{% if item_big_cover %}
{% image item.cover max-640x480 class="cover big" height="" width="" %}
{% else %}
{% image item.cover fill-64x64 class="cover small" %}
{% endif %}
{% endif %}
<div class="flex_item">
<h3 class="title">{{ item.title }}</h3>
{% if item.info %}
<span class="info">{{ item.info|safe }}</span>
{% endif %}
{% if not item.show_in_menus and item.date and item_date_format != '' %}
{% with date_format=item_date_format|default:'l d F, H:i' %}
<time datetime="{{ item.date }}">
{% if item.diffusion %}
<img src="{% static "aircox_cms/images/clock.png" %}" class="small_icon">
{{ item.diffusion.start|date:date_format }}
{% else %}
{{ item.date|date:date_format }}
{% endif %}
</time>
{% endwith %}
{% endif %}
</div>
{% if item.extra %}
<div class="extra"></div>
{% endif %}
</a>

View File

@ -0,0 +1,46 @@
{% load staticfiles %}
{% load i18n %}
<audio preload="metadata">
{% trans "Your browser does not support the <code>audio</code> element." %}
{% for stream in streams %}
<source src="{{ stream }}" />
{% endfor %}
</audio>
<div class="playlist">
<li class='item list_item flex_row' style="display: none;">
<div class="button">
<img src="{% static "aircox_cms/images/play.png" %}" class="play"
title="{% trans "play" %}" />
<img src="{% static "aircox_cms/images/pause.png" %}" class="pause"
title="{% trans "pause" %}" />
<img src="{% static "aircox_cms/images/loading.png" %}" class="loading"
title="{% trans "loading..." %}" />
</div>
<div class="flex_item">
<h3 class="title flex_item">{{ self.live_title }}</h3>
<div class="content flex_row">
<span class="info duration flex_item"></span>
<span class="actions">
<a class="action add" title="{% trans "add to the player" %}">+</a>
<a class="action detail" title="{% trans "more informations" %}"></a>
<a class="action remove" title="{% trans "remove this sound" %}"></a>
</span>
</div>
</div>
</li>
</div>
<div class="controls">
<span class="progress">
<span class="info duration"></span>
<progress class="flex_item progress" value="0" max="1"></progress>
</span>
<input type="checkbox" class="single" id="player_single_mode">
<label for="player_single_mode" class="info"
title="{% trans "enable and disable single mode" %}"></label>
</div>

View File

@ -0,0 +1,42 @@
{% load static %}
{% load i18n %}
{% if item.embed %}
{% else %}
{# TODO: complete archive podcast -> info #}
<script>
function add_sound_{{ item.id }}(event) {
var sound = new Sound(
title='{{ item.name|escape }}',
detail='{{ item.detail_url }}',
duration={{ item.duration|date:"H*3600+i*60+s" }},
streams='{{ item.url }}',
{% if page and page.cover %}cover='{{ page.icon }}'{% endif %}
);
sound = player.playlist.add(sound);
if(event.target.dataset.action != 'add')
player.select(sound, true);
}
</script>
<a class="list_item sound flex_row" onclick="add_sound_{{ item.id }}(event)">
<img src="{% static "aircox_cms/images/listen.png" %}" class="icon"/>
<h3 class="flex_item">{{ item.name }}</h3>
<time class="info">
{% if item.duration.hour > 0 %}
{{ item.duration|date:'H:i:s' }}
{% else %}
{{ item.duration|date:'i:s' }}
{% endif %}
</time>
<img src="{% static "aircox_cms/images/add.png" %}" class="icon"
data-action='add' alt="{% trans "add this sound to the playlist" %}"/>
</a>
{% endif %}

View File

@ -0,0 +1,29 @@
from django import template
from django.utils.safestring import mark_safe
from aircox.cms.sections import Section
register = template.Library()
@register.filter
def around(page_num, n):
"""
Return a range of value around a given number.
"""
return range(page_num-n, page_num+n+1)
@register.simple_tag(takes_context=True)
def render_sections(context, position = None):
"""
Render all sections at the given position (filter out base on page
models' too, cf. Section.model).
"""
request = context.get('request')
page = context.get('page')
return mark_safe(''.join(
section.render(request, page=page, context = {
'settings': context.get('settings')
})
for section in Section.get_sections_at(position, page)
))

3
aircox_cms/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
aircox_cms/utils.py Normal file
View File

@ -0,0 +1,10 @@
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

2
aircox_cms/views.py Normal file
View File

@ -0,0 +1,2 @@
from django.shortcuts import render

181
aircox_cms/wagtail_hooks.py Normal file
View File

@ -0,0 +1,181 @@
from django.utils import timezone as tz
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.utils.html import format_html
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem, Menu, SubmenuMenuItem
from wagtail.contrib.modeladmin.options import \
ModelAdmin, ModelAdminGroup, modeladmin_register
import aircox.models
import aircox_cms.models as models
class ProgramAdmin(ModelAdmin):
model = aircox.models.Program
menu_label = _('Programs')
menu_icon = 'pick'
menu_order = 200
list_display = ('name', 'active')
search_fields = ('name',)
class DiffusionAdmin(ModelAdmin):
model = aircox.models.Diffusion
menu_label = _('Diffusions')
menu_icon = 'date'
menu_order = 200
list_display = ('program', 'start', 'end', 'frequency', 'initial')
list_filter = ('frequency', 'start', 'program')
class ScheduleAdmin(ModelAdmin):
model = aircox.models.Schedule
menu_label = _('Schedules')
menu_icon = 'time'
menu_order = 200
list_display = ('program', 'frequency', 'duration', 'initial')
list_filter = ('frequency', 'date', 'duration', 'program')
class StreamAdmin(ModelAdmin):
model = aircox.models.Stream
menu_label = _('Streams')
menu_icon = 'time'
menu_order = 200
list_display = ('program', 'delay', 'begin', 'end')
list_filter = ('program', 'delay', 'begin', 'end')
class AdvancedAdminGroup(ModelAdminGroup):
menu_label = _("Advanced")
menu_icon = 'plus-inverse'
items = (ProgramAdmin, DiffusionAdmin, ScheduleAdmin, StreamAdmin)
modeladmin_register(AdvancedAdminGroup)
class SoundAdmin(ModelAdmin):
model = aircox.models.Sound
menu_label = _('Sounds')
menu_icon = 'media'
menu_order = 350
list_display = ('name', 'duration', 'type', 'path', 'good_quality', 'public')
list_filter = ('type', 'good_quality', 'public')
search_fields = ('name', 'path')
modeladmin_register(SoundAdmin)
## Hooks
@hooks.register('insert_editor_css')
def editor_css():
return format_html(
'<link rel="stylesheet" href="{}">',
static('aircox_cms/css/cms.css')
)
class GenericMenu(Menu):
page_model = models.Publication
def __init__(self):
super().__init__('')
def get_queryset(self):
pass
def get_title(self, item):
pass
def get_parent(self, item):
pass
def get_page_url(self, page_model, item):
if item.page.count():
return reverse('wagtailadmin_pages:edit', args=[item.page.first().id])
parent_page = self.get_parent(item)
if not parent_page:
return ''
return reverse(
'wagtailadmin_pages:add', args= [
page_model._meta.app_label, page_model._meta.model_name,
parent_page.id
]
)
@property
def registered_menu_items(self):
now = tz.now()
last_max = now - tz.timedelta(minutes = 10)
qs = self.get_queryset()
return [
MenuItem(self.get_title(x), self.get_page_url(self.page_model, x))
for x in qs
]
class DiffusionsMenu(GenericMenu):
"""
Menu to display diffusions of today
"""
page_model = models.DiffusionPage
def get_queryset(self):
return aircox.models.Diffusion.objects.filter(
type = aircox.models.Diffusion.Type.normal,
start__contains = tz.now().date(),
initial__isnull = True,
).order_by('start')
def get_title(self, item):
return item.program.name
def get_parent(self, item):
return item.program.page.first()
@hooks.register('register_admin_menu_item')
def register_programs_menu_item():
return SubmenuMenuItem(
_('Today\'s Diffusions'), DiffusionsMenu(),
classnames='icon icon-folder-open-inverse', order=101
)
class ProgramsMenu(GenericMenu):
"""
Menu to display all active programs
"""
page_model = models.DiffusionPage
def get_queryset(self):
return aircox.models.Program.objects \
.filter(active = True, page__isnull = False) \
.filter(stream__isnull = True) \
.order_by('name')
def get_title(self, item):
return item.name
def get_parent(self, item):
# TODO: #Station / get current site
from aircox_cms.models import WebsiteSettings
settings = WebsiteSettings.objects.first()
if not settings:
return
return settings.default_program_parent_page
@hooks.register('register_admin_menu_item')
def register_programs_menu_item():
return SubmenuMenuItem(
_('Programs'), ProgramsMenu(),
classnames='icon icon-folder-open-inverse', order=102
)