merge aircox and aircox_instance
0
aircox_cms/__init__.py
Normal file
3
aircox_cms/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
aircox_cms/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CmsConfig(AppConfig):
|
||||
name = 'cms'
|
44
aircox_cms/forms.py
Normal 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'))
|
||||
|
||||
|
821
aircox_cms/locale/fr/LC_MESSAGES/django.po
Normal 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 ""
|
92
aircox_cms/management/commands/programs_to_cms.py
Normal 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
@ -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
@ -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
@ -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
@ -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
|
||||
|
||||
|
24
aircox_cms/static/aircox_cms/css/cms.css
Normal 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;
|
||||
}
|
||||
*/
|
357
aircox_cms/static/aircox_cms/css/layout.css
Normal 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;
|
||||
}
|
||||
|
||||
|
179
aircox_cms/static/aircox_cms/css/theme.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
130
aircox_cms/static/aircox_cms/images/LICENSE.TXT
Normal 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 Adapter’s 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 Adapter’s 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.
|
||||
|
14
aircox_cms/static/aircox_cms/images/README.md
Normal 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
|
||||
We‘re always happy to hear from you, whether it’s 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).
|
BIN
aircox_cms/static/aircox_cms/images/add.png
Normal file
After Width: | Height: | Size: 327 B |
BIN
aircox_cms/static/aircox_cms/images/clock.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
aircox_cms/static/aircox_cms/images/comments.png
Normal file
After Width: | Height: | Size: 701 B |
BIN
aircox_cms/static/aircox_cms/images/facebook.png
Normal file
After Width: | Height: | Size: 545 B |
BIN
aircox_cms/static/aircox_cms/images/feed.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
aircox_cms/static/aircox_cms/images/gplus.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
aircox_cms/static/aircox_cms/images/grow.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
aircox_cms/static/aircox_cms/images/home.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
aircox_cms/static/aircox_cms/images/list.png
Normal file
After Width: | Height: | Size: 391 B |
BIN
aircox_cms/static/aircox_cms/images/listen.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
aircox_cms/static/aircox_cms/images/loading.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
aircox_cms/static/aircox_cms/images/mail.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
aircox_cms/static/aircox_cms/images/on_air.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
aircox_cms/static/aircox_cms/images/pause.png
Normal file
After Width: | Height: | Size: 311 B |
BIN
aircox_cms/static/aircox_cms/images/play.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
aircox_cms/static/aircox_cms/images/search.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
aircox_cms/static/aircox_cms/images/share.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
aircox_cms/static/aircox_cms/images/tiles_large.png
Normal file
After Width: | Height: | Size: 316 B |
BIN
aircox_cms/static/aircox_cms/images/tumblr.png
Normal file
After Width: | Height: | Size: 696 B |
BIN
aircox_cms/static/aircox_cms/images/twitter.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
423
aircox_cms/static/aircox_cms/js/player.js
Normal 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);
|
||||
},
|
||||
}
|
||||
|
||||
|
68
aircox_cms/static/aircox_cms/js/utils.js
Normal 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);
|
||||
},
|
||||
}
|
||||
|
||||
|
82
aircox_cms/templates/aircox_cms/base_site.html
Normal 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>
|
15
aircox_cms/templates/aircox_cms/dated_list_page.html
Normal 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 %}
|
||||
|
68
aircox_cms/templates/aircox_cms/diffusion_page.html
Normal 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 %}
|
||||
|
||||
|
24
aircox_cms/templates/aircox_cms/event_page.html
Normal 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 %}
|
||||
|
||||
|
||||
|
61
aircox_cms/templates/aircox_cms/generic_page.html
Normal 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 %}
|
||||
|
||||
|
||||
|
34
aircox_cms/templates/aircox_cms/program_page.html
Normal 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 %}
|
||||
|
60
aircox_cms/templates/aircox_cms/publication.html
Normal 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 %}
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
13
aircox_cms/templates/aircox_cms/sections/section_link.html
Normal 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 %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
||||
|
16
aircox_cms/templates/aircox_cms/sections/section_list.html
Normal 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 %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
27
aircox_cms/templates/aircox_cms/sections/section_player.html
Normal 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 %}
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -0,0 +1,6 @@
|
||||
{% extends "aircox_cms/sections/section_item.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "aircox_cms/snippets/date_list.html" %}
|
||||
{% endblock %}
|
||||
|
55
aircox_cms/templates/aircox_cms/snippets/comments.html
Normal 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>
|
||||
|
39
aircox_cms/templates/aircox_cms/snippets/date_list.html
Normal 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>
|
||||
|
45
aircox_cms/templates/aircox_cms/snippets/date_list_item.html
Normal 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>
|
||||
|
||||
|
65
aircox_cms/templates/aircox_cms/snippets/list.html
Normal 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 %}
|
||||
…
|
||||
{% 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 %}
|
||||
…
|
||||
{% 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>
|
||||
|
||||
|
51
aircox_cms/templates/aircox_cms/snippets/list_item.html
Normal 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>
|
||||
|
||||
|
46
aircox_cms/templates/aircox_cms/snippets/player.html
Normal 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>
|
||||
|
@ -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 %}
|
||||
|
29
aircox_cms/templatetags/aircox_cms.py
Normal 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
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
10
aircox_cms/utils.py
Normal 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
@ -0,0 +1,2 @@
|
||||
from django.shortcuts import render
|
||||
|
181
aircox_cms/wagtail_hooks.py
Normal 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
|
||||
)
|
||||
|
||||
|