add 'parts' system + script; work on player; create list_item.html template; update on_air

This commit is contained in:
bkfox 2016-06-14 03:33:26 +02:00
parent 5da8762f77
commit 3936580275
12 changed files with 466 additions and 144 deletions

96
cms/decorators.py Normal file
View File

@ -0,0 +1,96 @@
from django.template.loader import render_to_string
from django.conf.urls import url
from django.core.urlresolvers import reverse
from django.utils.text import slugify
def __part_normalize(value, default):
value = value if value else default
return slugify(value.lower())
def parts(cls, name = None, pattern = None):
"""
the decorated class is a parts class, and contains part
functions. Look `part` decorator doc for more info.
"""
name = __part_normalize(name, cls.__name__)
pattern = __part_normalize(pattern, cls.__name__)
cls._parts = []
for part in cls.__dict__.values():
if not hasattr(part, 'is_part'):
continue
part.name = name + '_' + part.name
part.pattern = pattern + '/' + part.pattern
part = url(part.pattern, name = part.name,
view = part, kwargs = {'cl': cls})
cls._parts.append(part)
return cls
def part(view, name = None, pattern = None):
"""
A part function is a view that is used to retrieve data dynamically,
e.g. from Javascript with XMLHttpRequest. A part function is a classmethod
that returns a string and has the following signature:
`part(cl, request, parent, *args, **kwargs)`
When a section with parts is added to the website, the parts' urls
are added to the website's one and make them available.
A part function can have the following parameters:
* name: part.name or part.__name__
* pattern: part.pattern or part.__name__
An extra method `url` is added to the part function to return the adequate
url.
Theses are combined with the containing parts class params such as:
* name: parts.name + '_' + part.name
* pattern: parts.pattern + '/' + part.pattern
The parts class will have an attribute '_parts' as list of generated
urls.
"""
if hasattr(view, 'is_part'):
return view
def view_(request, as_str = False, cl = None, *args, **kwargs):
v = view(cl, request, *args, **kwargs)
if as_str:
return v
return HttpResponse(v)
def url(*args, **kwargs):
return reverse(view_.name, *args, **kwargs)
view_.name = __part_normalize(name, view.__name__)
view_.pattern = __part_normalize(pattern, view.__name__)
view_.is_part = True
view_.url = url
return view_
def template(template_name):
"""
the decorated function returns a context that is used to
render a template value.
* template_name: name of the template to use
* hide_empty: an empty context returns an empty string
"""
def wrapper(func):
def view_(cl, request, *args, **kwargs):
context = func(cl, request, *args, **kwargs)
if not context and hide_empty:
return ''
context['embed'] = True
return render_to_string(template_name, context, request=request)
view_.__name__ = func.__name__
return view_
return wrapper

View File

@ -212,7 +212,7 @@ class Post (models.Model, Routable):
abstract = True
class RelatedPostBase (models.base.ModelBase):
class RelatedMeta (models.base.ModelBase):
"""
Metaclass for RelatedPost children.
"""
@ -310,7 +310,7 @@ class RelatedPostBase (models.base.ModelBase):
return model
class RelatedPost (Post, metaclass = RelatedPostBase):
class RelatedPost (Post, metaclass = RelatedMeta):
"""
Post linked to an object of other model. This object is accessible through
the field "related".
@ -346,29 +346,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
"""
Relation descriptor used to generate and manage the related object.
* model: model of the related object
* bindings: values that are bound between the post and the related
object. When the post is saved, these fields are updated on it.
It is a dict of { post_attr: rel_attr }
If there is a post_attr "thread", the corresponding rel_attr is used
to update the post thread to the correct Post model (in order to
establish a parent-child relation between two models)
When a callable is set as bound value, it will be called to retrieve
the value, as: callable_func(post, related)
Note: bound values can be any value, not only Django field.
* post_to_rel: auto update related object when post is updated
* rel_to_post: auto update the post when related object is updated
* thread_model: generated by the metaclass, points to the RelatedPost
model generated for the bindings.thread object.
* field_args: dict of arguments to pass to the ForeignKey constructor,
such as: ForeignKey(related_model, **field_args)
* auto_create: automatically create a RelatedPost for each new item of
the related object and init it with bounded values. Use 'post_save'
signal. If auto_create is callable, use `auto_create(related_object)`.
Be careful with post_to_rel!
* There is no check of permissions when related object is synchronised
from the post, so be careful when enabling post_to_rel.
@ -376,12 +353,48 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
(sub-)class thread_model, the related parent is set to None
"""
model = None
bindings = None # values to map { post_attr: rel_attr }
"""
model of the related object
"""
bindings = None
"""
dict of `post_attr: rel_attr` that represent bindings of values
between the post and the related object. Field are updated according
to `post_to_rel` and `rel_to_post`.
If there is a post_attr "thread", the corresponding rel_attr is used
to update the post thread to the correct Post model (in order to
establish a parent-child relation between two models)
When a callable is set as `rel_attr`, it will be called to retrieve
the value, as `rel_attr(post, related)`
note: bound values can be any value, not only Django field.
"""
post_to_rel = False
"""
update related object when the post is saved, using bindings
"""
rel_to_post = False
"""
update the post when related object is updated, using bindings
"""
thread_model = None
"""
generated by the metaclass, points to the RelatedPost model
generated for the bindings.thread object.
"""
field_args = None
"""
dict of arguments to pass to the ForeignKey constructor, such as
`ForeignKey(related_model, **field_args)`
"""
auto_create = False
"""
automatically create a RelatedPost for each new item of the related
object and init it with bounded values. Use 'post_save' signal. If
auto_create is callable, use `auto_create(related_object)`.
"""
def get_rel_attr(self, attr):
attr = self._relation.bindings.get(attr)

View File

@ -3,9 +3,10 @@ Define different Section css_class that can be used by views.Sections;
"""
import re
from django.templatetags.static import static
from django.template.loader import render_to_string
from django.views.generic.base import View
from django.templatetags.static import static
from django.http import HttpResponse
from django.contrib import messages
from django.utils.html import escape
from django.utils.translation import ugettext as _, ugettext_lazy
@ -75,9 +76,6 @@ class Section(Viewable, View):
* title: title of the section
* header: header of the section
* footer: footer of the section
* force_object: (can be persistent) related object
"""
template_name = 'aircox/cms/website.html'
@ -88,7 +86,6 @@ class Section(Viewable, View):
title = ''
header = ''
footer = ''
force_object = None
request = None
object = None

View File

@ -0,0 +1,163 @@
function Part(url = '', params = '') {
return new Part_(url, params);
}
// Small utility used to make XMLHttpRequests, and map results to other
// objects
function Part_(url = '', params = '') {
this.url = url;
this.params = params;
this.selectors = [];
this.actions = [];
}
Part_.prototype = {
// XMLHttpRequest object used to retrieve data
xhr: null,
// delayed actions that have been registered
actions: null,
// registered selectors
selectors: null,
/// parse request result and save in this.stanza
__parse_dom: function() {
var doc = document.implementation.createHTMLDocument('xhr').documentElement;
doc.innerHTML = this.xhr.responseText;
this.stanza = doc;
},
// make an xhr request, and call callback(err, xhr) if given
get: function() {
var self = this;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState != 4)
return
// TODO: error handling
var err = self.xhr.status != 200 && self.xhr.status;
if(err)
return;
for(var i = 0; i < self.actions.length; i++)
self.actions[i].apply(self);
}
if(this.params)
xhr.open('GET', this.url + '?' + this.params, true);
else
xhr.open('GET', this.url, true);
this.xhr = xhr;
return this;
},
// send request
send: function() {
this.xhr.send();
return this;
},
// set selectors. if callback is set, call this callback
// once data are retrieved with an object of
// `selector_name: select_result`
select: function(selectors, callback = undefined) {
for(var i in selectors) {
selector = selectors[i];
if(!selector.sort)
selector = [selector]
selector = new Part_.Selector(i, selector[0], selector[1], selector[2])
this.selectors.push(selector)
}
if(callback) {
var self = this;
this.actions.push(function() {
var r = {}
for(var i in self.selectors)
r[i] = self.selectors[i].select(self.stanza);
callback(r);
});
}
return this;
},
// map data using this.selectors on xhr result *and* dest
map: function(dest) {
var self = this;
this.actions.push(function() {
if(!self.stanza)
self.__parse_dom();
for(var i = 0; i < self.selectors.length; i++) {
selector = self.selectors[i]
selector.map(self.stanza, dest);
}
});
return this;
},
// add an action to the list of actions
on: function(callback) {
this.actions.push(callback)
return this;
},
};
Part_.Selector = function(name, selector, attribute = null, all = false) {
this.name = name;
this.selector = selector;
this.attribute = attribute;
this.all = all;
}
Part_.Selector.prototype = {
select: function(obj, use_attr = true) {
if(!this.all) {
obj = obj.querySelectorAll(this.selector)
if(obj)
obj = obj[0];
return (this.attribute && use_attr && obj) ? obj[this.attribute] : obj;
}
obj = obj.querySelectorAll(this.selector);
if(!obj)
return;
r = []
for(var i = 0; i < obj.length; i++) {
r.push(this.attribute && use_attr ? obj[i][this.attribute] : obj[i])
}
return r;
},
map: function(src, dst) {
src_qs = this.select(src, false);
dst_qs = this.select(dst, false);
if(!src_qs || !dst_qs)
return
if(!this.all) {
src_qs = [ src_qs ];
dst_qs = [ dst_qs ];
}
var size = Math.min(src_qs.length, dst_qs.length);
for(var i = 0; i < size; i++) {
var src = src_qs[i];
var dst = dst_qs[i];
if(this.attribute)
dst[this.attribute] = src[this.attribute];
else
dst.parentNode.replaceChild(src, dst);
}
},
}

View File

@ -8,65 +8,7 @@
{% block content %}
<ul class="content">
{% for item in object_list %}
<li {% if item.css_class %}class="{{ item.css_class }}"{% endif %}
{% for k, v in item.attrs.items %}
{{ k }} = "{{ v|addslashes }}"
{% endfor %} >
{% if item.url %}
<a href="{{ item.url }}">
{% endif %}
{% if 'image' in list.fields and item.image %}
<img src="{% thumbnail item.image list.image_size crop %}">
{% endif %}
<div class="content">
{% if 'title' in list.fields and item.title %}
<h2 class="title">{{ item.title }}</h2>
{% endif %}
{% if 'content' in list.fields and item.content %}
<div class="text">
{% if list.truncate %}
{{ item.content|striptags|truncatewords:list.truncate }}
{% else %}
{{ item.content|striptags }}
{% endif %}
</div>
{% endif %}
</div>
<div class="meta">
{% if item.date and 'date' in list.fields or 'time' in list.fields %}
<time datetime="{{ item.date }}">
{% if 'date' in list.fields %}
<span class="date">
{{ item.date|date:'D. d F' }}
</span>
{% endif %}
{% if 'time' in list.fields %}
<span class="time">
{{ item.date|date:'H:i' }}
</span>
{% endif %}
</time>
{% endif %}
{% if item.author and 'author' in list.fields %}
<span class="author">
{{ item.author }}
</span>
{% endif %}
{% if item.info and 'info' in list.fields %}
<span class="info">
{{ item.info }}
</span>
{% endif %}
</div>
{% if item.url %}
</a>
{% endif %}
{% include "aircox/cms/list_item.html" %}
{% empty %}
<div class="message empty">
{{ list.message_empty }}

View File

@ -0,0 +1,65 @@
{% load i18n %}
{% load thumbnail %}
<li {% if item.css_class %}class="{{ item.css_class }}"{% endif %}
{% for k, v in item.attrs.items %}
{{ k }} = "{{ v|addslashes }}"
{% endfor %} >
{% if item.url %}
<a class="url" href="{{ item.url }}">
{% endif %}
{% if 'image' in list.fields and item.image %}
<img class="image" src="{% thumbnail item.image list.image_size crop %}">
{% endif %}
<div class="body">
{% if 'title' in list.fields and item.title %}
<h2 class="title">{{ item.title }}</h2>
{% endif %}
{% if 'content' in list.fields and item.content %}
<div class="content">
{% if list.truncate %}
{{ item.content|striptags|truncatewords:list.truncate }}
{% else %}
{{ item.content|striptags }}
{% endif %}
</div>
{% endif %}
</div>
<div class="meta">
{% if item.date and 'date' in list.fields or 'time' in list.fields %}
<time datetime="{{ item.date }}">
{% if 'date' in list.fields %}
<span class="date">
{{ item.date|date:'D. d F' }}
</span>
{% endif %}
{% if 'time' in list.fields %}
<span class="time">
{{ item.date|date:'H:i' }}
</span>
{% endif %}
</time>
{% endif %}
{% if item.author and 'author' in list.fields %}
<span class="author">
{{ item.author }}
</span>
{% endif %}
{% if item.info and 'info' in list.fields %}
<span class="info">
{{ item.info }}
</span>
{% endif %}
</div>
{% if item.url %}
</a>
{% endif %}
</li>

View File

@ -3,7 +3,7 @@
<html>
<head>
{# FIXME: page tags #}
{# FIXME: extra head block #}
<meta charset="utf-8">
<meta name="application-name" content="aircox-cms">
<meta name="description" content="{{ website.description }}">
@ -13,6 +13,7 @@
{% if website.styles %}
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
{% endif %}
<script src="{% static "aircox/cms/scripts.js" %}"></script>
<title>{% if title %}{{ title }} - {% endif %}{{ website.name }}</title>
</head>
<body>

View File

@ -1,5 +1,5 @@
from django.utils.text import slugify
from django.conf.urls import url
from django.conf.urls import include, url
import aircox.cms.routes as routes
import aircox.cms.routes as routes_
@ -35,6 +35,8 @@ class Website:
## components
urls = []
"""list of urls generated thourgh registrations"""
parts = []
"""list of registered parts (done through sections registration)"""
registry = {}
"""dict of registered models by their name"""
@ -43,7 +45,8 @@ class Website:
* menus: a list of menus to add to the website
"""
self.registry = {}
self.urls = []
self.parts = []
self.urls = [ url(r'^parts/', include(self.parts)) ]
self.menus = {}
self.__dict__.update(kwargs)
@ -93,9 +96,23 @@ class Website:
model._website = self
return name
def register_parts(self, sections):
"""
Register parts that are used in the given sections.
"""
if not hasattr(sections, '__iter__'):
sections = [sections]
for section in sections:
if not hasattr(section, '_parts'):
continue
self.parts += [
url for url in section._parts
if url not in self.urls
]
def register(self, name, routes = [], view = views.PageView,
model = None, **view_kwargs):
model = None, sections = None, **view_kwargs):
"""
Register a view using given name and routes. If model is given,
register the views for it.
@ -110,6 +127,11 @@ class Website:
if not view_kwargs.get('menus'):
view_kwargs['menus'] = self.menus
if sections:
self.register_parts(sections)
view_kwargs['sections'] = sections
view = view.as_view(
website = self,
**view_kwargs
@ -150,6 +172,7 @@ class Website:
elif menu.position in ('left', 'right'):
menu.tag = 'side'
self.menus[menu.position] = menu
self.register_parts(menu.sections)
def get_menu(self, position):
"""

View File

@ -19,6 +19,7 @@ a website in a fast and simple manner.
## Sections
* **Player**: player widget
* **Diffusions**: generic section list to retrieve diffusions by date, related
or not to a specific Program. If wanted, can show schedule in the header of
the section (with indication of reruns).

View File

@ -1,3 +1,5 @@
import json
from django.utils import timezone as tz
from django.utils.translation import ugettext as _, ugettext_lazy
@ -5,10 +7,12 @@ import aircox.programs.models as programs
import aircox.cms.models as cms
import aircox.cms.routes as routes
import aircox.cms.sections as sections
import aircox.cms.decorators as decorators
import aircox.website.models as models
@decorators.parts
class Player(sections.Section):
"""
Display a player that is cool.
@ -20,8 +24,9 @@ class Player(sections.Section):
"""
#default_sounds
@staticmethod
def on_air():
@decorators.part
@decorators.template(template_name = 'aircox/cms/list_item.html')
def on_air(cl, request):
"""
View that return what is on air formatted in JSON.
"""
@ -31,19 +36,27 @@ class Player(sections.Section):
)
if not qs or not qs[0].is_date_in_my_range():
return None
return ''
qs = qs[0]
post = models.Diffusion.objects.filter(related = qs)
if not post:
post = models.Program.objects.filter(related = qs.program)
if not post:
post = ListItem(title = qs.program.name)
return post
else:
post = post[0]
return {
'item': post,
'list': sections.List,
}
return json.dumps({ 'title': post.title, 'url': post.url() })
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update({
'base_template': 'aircox/cms/section.html',
'live_streams': self.live_streams,

View File

@ -56,14 +56,14 @@
.playlists nav a {
padding: 0.2em;
cursor: pointer;
}
.playlists nav > *[selected],
.playlists .item[selected] {
color: black;
border-top-right-radius: 0.2em;
background-color: rgba(0, 126, 223, 0.8);
}
.playlists nav > *[selected] {
color: black;
border-top-right-radius: 0.2em;
background-color: rgba(0, 126, 223, 0.8);
}
.playlists ul:not([selected]) {
display: none;
@ -80,10 +80,6 @@
vertical-align: center;
}
.playlist .item .title {
color: #007EDF;
}
.playlist .item .info {
float: right;
font-size: 0.8em;
@ -94,6 +90,9 @@
font-weight: bold;
}
.playlist .item[selected] .title {
color: #007EDF;
}
</style>
<div id="player">
@ -139,6 +138,7 @@ function Playlist(id, name, items) {
player.select_playlist(self);
event.preventDefault();
}, true);
this.tab.className = 'tab';
this.tab.innerHTML = name;
player.playlists.appendChild(this.playlist);
@ -163,10 +163,12 @@ Playlist.prototype = {
else
self.playlist.appendChild(item);
info.item = item;
item.info = info;
item.querySelector('.title').innerHTML = info.title;
item.querySelector('.detail').href = info.url;
item.querySelector('.info').innerHTML = info.info || '';
item.info = info;
item.addEventListener('click', function(event) {
if(event.target.className.indexOf('detail') != -1)
@ -174,7 +176,6 @@ Playlist.prototype = {
player.select(event.currentTarget.info);
event.preventDefault();
}, true);
this.items.push(info);
},
@ -228,6 +229,16 @@ player = {
this.select(this.live.items[0], false)
},
/// update "on_air"
get_on_air: function() {
part = Part('').select({
'title': '.title',
'url': 'a',
}, function(r) {
document.title = r.title;
});
},
/// play a given item { title, src }
play: function() {
var player = this.player;
@ -243,35 +254,45 @@ player = {
}
},
/// update info on the player from the given item
set_info: function(item) {
var player = this.player;
player.querySelectorAll('#simple-player .title')[0]
.innerHTML = item.title;
player.querySelectorAll('.playlists')[0]
.setAttribute('playlist', item.playlist);
},
/// select the current track to play, and start playing it
select: function(item, play = true) {
var audio = this.audio;
var player = this.player;
audio.pause();
audio.src = item.stream;
audio.load()
self.item = item;
this.set_info(item);
if(this.item && this.item.item)
this.item.item.removeAttribute('selected')
this.item = item;
if(item.item)
console.log(item.item)
item.item.setAttribute('selected', 'true');
player.querySelectorAll('#simple-player .title')[0]
.innerHTML = item.title;
player.querySelectorAll('.playlists')[0]
.setAttribute('playlist', item.playlist);
if(play)
this.play();
},
/// select current playlist to show
select_playlist: function(playlist) {
v = this.player.querySelectorAll('.playlists *[selected]');
/// remove selection using the given selector.
__unselect: function (selector) {
v = this.player.querySelectorAll(selector);
if(v)
for(var i = 0; i < v.length; i++)
v[i].removeAttribute('selected');
},
/// select current playlist to show
select_playlist: function(playlist) {
this.__unselect('.playlists nav .tab[selected]');
this.__unselect('.playlists .playlist[selected]');
self.playlist = playlist
playlist.playlist.setAttribute('selected', 'true');

View File

@ -22,27 +22,14 @@ function update_schedule(event) {
return;
console.log(schedule.className)
// xhr
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if(xhr.readyState != 4 || xhr.status != 200 && xhr.status)
return;
var obj = document.implementation.createHTMLDocument('result');
obj.documentElement.innerHTML = xhr.responseText;
obj = obj.documentElement;
schedule.querySelector('header').innerHTML =
obj.querySelector('header').innerHTML;
schedule.querySelector('.content').innerHTML =
obj.querySelector('.content').innerHTML;
}
fields = [ {% for field in list.fields %}"fields={{ field }}",{% endfor %} ];
fields = fields.join('&');
xhr.open('GET', url + '?embed=1&' + fields, true);
xhr.send();
part = new Part(url, 'embed&' + fields);
part.get().select({
'header': ['header', 'innerHTML', true],
'content': ['.content', 'innerHTML', true],
}).map(schedule).send();
}
</script>
<a href="{{ prev_week }}" onclick="update_schedule(event); return true;">&lt;</a>