add 'parts' system + script; work on player; create list_item.html template; update on_air
This commit is contained in:
parent
5da8762f77
commit
3936580275
96
cms/decorators.py
Normal file
96
cms/decorators.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,7 @@ class Post (models.Model, Routable):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class RelatedPostBase (models.base.ModelBase):
|
class RelatedMeta (models.base.ModelBase):
|
||||||
"""
|
"""
|
||||||
Metaclass for RelatedPost children.
|
Metaclass for RelatedPost children.
|
||||||
"""
|
"""
|
||||||
|
@ -310,7 +310,7 @@ class RelatedPostBase (models.base.ModelBase):
|
||||||
return model
|
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
|
Post linked to an object of other model. This object is accessible through
|
||||||
the field "related".
|
the field "related".
|
||||||
|
@ -346,29 +346,6 @@ class RelatedPost (Post, metaclass = RelatedPostBase):
|
||||||
"""
|
"""
|
||||||
Relation descriptor used to generate and manage the related object.
|
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!
|
Be careful with post_to_rel!
|
||||||
* There is no check of permissions when related object is synchronised
|
* There is no check of permissions when related object is synchronised
|
||||||
from the post, so be careful when enabling post_to_rel.
|
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
|
(sub-)class thread_model, the related parent is set to None
|
||||||
"""
|
"""
|
||||||
model = 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
|
post_to_rel = False
|
||||||
|
"""
|
||||||
|
update related object when the post is saved, using bindings
|
||||||
|
"""
|
||||||
rel_to_post = False
|
rel_to_post = False
|
||||||
|
"""
|
||||||
|
update the post when related object is updated, using bindings
|
||||||
|
"""
|
||||||
thread_model = None
|
thread_model = None
|
||||||
|
"""
|
||||||
|
generated by the metaclass, points to the RelatedPost model
|
||||||
|
generated for the bindings.thread object.
|
||||||
|
"""
|
||||||
field_args = None
|
field_args = None
|
||||||
|
"""
|
||||||
|
dict of arguments to pass to the ForeignKey constructor, such as
|
||||||
|
`ForeignKey(related_model, **field_args)`
|
||||||
|
"""
|
||||||
auto_create = False
|
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):
|
def get_rel_attr(self, attr):
|
||||||
attr = self._relation.bindings.get(attr)
|
attr = self._relation.bindings.get(attr)
|
||||||
|
|
|
@ -3,9 +3,10 @@ Define different Section css_class that can be used by views.Sections;
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.templatetags.static import static
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.views.generic.base import View
|
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.contrib import messages
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||||
|
@ -75,9 +76,6 @@ class Section(Viewable, View):
|
||||||
* title: title of the section
|
* title: title of the section
|
||||||
* header: header of the section
|
* header: header of the section
|
||||||
* footer: footer of the section
|
* footer: footer of the section
|
||||||
|
|
||||||
* force_object: (can be persistent) related object
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
template_name = 'aircox/cms/website.html'
|
template_name = 'aircox/cms/website.html'
|
||||||
|
|
||||||
|
@ -88,7 +86,6 @@ class Section(Viewable, View):
|
||||||
title = ''
|
title = ''
|
||||||
header = ''
|
header = ''
|
||||||
footer = ''
|
footer = ''
|
||||||
force_object = None
|
|
||||||
|
|
||||||
request = None
|
request = None
|
||||||
object = None
|
object = None
|
||||||
|
|
163
cms/static/aircox/cms/scripts.js
Normal file
163
cms/static/aircox/cms/scripts.js
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,65 +8,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul class="content">
|
<ul class="content">
|
||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
<li {% if item.css_class %}class="{{ item.css_class }}"{% endif %}
|
{% include "aircox/cms/list_item.html" %}
|
||||||
{% 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 %}
|
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div class="message empty">
|
<div class="message empty">
|
||||||
{{ list.message_empty }}
|
{{ list.message_empty }}
|
||||||
|
|
65
cms/templates/aircox/cms/list_item.html
Normal file
65
cms/templates/aircox/cms/list_item.html
Normal 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>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
{# FIXME: page tags #}
|
{# FIXME: extra head block #}
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="application-name" content="aircox-cms">
|
<meta name="application-name" content="aircox-cms">
|
||||||
<meta name="description" content="{{ website.description }}">
|
<meta name="description" content="{{ website.description }}">
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
{% if website.styles %}
|
{% if website.styles %}
|
||||||
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<script src="{% static "aircox/cms/scripts.js" %}"></script>
|
||||||
<title>{% if title %}{{ title }} - {% endif %}{{ website.name }}</title>
|
<title>{% if title %}{{ title }} - {% endif %}{{ website.name }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.utils.text import slugify
|
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
|
||||||
import aircox.cms.routes as routes_
|
import aircox.cms.routes as routes_
|
||||||
|
@ -35,6 +35,8 @@ class Website:
|
||||||
## components
|
## components
|
||||||
urls = []
|
urls = []
|
||||||
"""list of urls generated thourgh registrations"""
|
"""list of urls generated thourgh registrations"""
|
||||||
|
parts = []
|
||||||
|
"""list of registered parts (done through sections registration)"""
|
||||||
registry = {}
|
registry = {}
|
||||||
"""dict of registered models by their name"""
|
"""dict of registered models by their name"""
|
||||||
|
|
||||||
|
@ -43,7 +45,8 @@ class Website:
|
||||||
* menus: a list of menus to add to the website
|
* menus: a list of menus to add to the website
|
||||||
"""
|
"""
|
||||||
self.registry = {}
|
self.registry = {}
|
||||||
self.urls = []
|
self.parts = []
|
||||||
|
self.urls = [ url(r'^parts/', include(self.parts)) ]
|
||||||
self.menus = {}
|
self.menus = {}
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
@ -93,9 +96,23 @@ class Website:
|
||||||
model._website = self
|
model._website = self
|
||||||
return name
|
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,
|
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 a view using given name and routes. If model is given,
|
||||||
register the views for it.
|
register the views for it.
|
||||||
|
@ -110,6 +127,11 @@ class Website:
|
||||||
|
|
||||||
if not view_kwargs.get('menus'):
|
if not view_kwargs.get('menus'):
|
||||||
view_kwargs['menus'] = self.menus
|
view_kwargs['menus'] = self.menus
|
||||||
|
|
||||||
|
if sections:
|
||||||
|
self.register_parts(sections)
|
||||||
|
view_kwargs['sections'] = sections
|
||||||
|
|
||||||
view = view.as_view(
|
view = view.as_view(
|
||||||
website = self,
|
website = self,
|
||||||
**view_kwargs
|
**view_kwargs
|
||||||
|
@ -150,6 +172,7 @@ class Website:
|
||||||
elif menu.position in ('left', 'right'):
|
elif menu.position in ('left', 'right'):
|
||||||
menu.tag = 'side'
|
menu.tag = 'side'
|
||||||
self.menus[menu.position] = menu
|
self.menus[menu.position] = menu
|
||||||
|
self.register_parts(menu.sections)
|
||||||
|
|
||||||
def get_menu(self, position):
|
def get_menu(self, position):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -19,6 +19,7 @@ a website in a fast and simple manner.
|
||||||
|
|
||||||
|
|
||||||
## Sections
|
## Sections
|
||||||
|
* **Player**: player widget
|
||||||
* **Diffusions**: generic section list to retrieve diffusions by date, related
|
* **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
|
or not to a specific Program. If wanted, can show schedule in the header of
|
||||||
the section (with indication of reruns).
|
the section (with indication of reruns).
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
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.models as cms
|
||||||
import aircox.cms.routes as routes
|
import aircox.cms.routes as routes
|
||||||
import aircox.cms.sections as sections
|
import aircox.cms.sections as sections
|
||||||
|
import aircox.cms.decorators as decorators
|
||||||
|
|
||||||
import aircox.website.models as models
|
import aircox.website.models as models
|
||||||
|
|
||||||
|
|
||||||
|
@decorators.parts
|
||||||
class Player(sections.Section):
|
class Player(sections.Section):
|
||||||
"""
|
"""
|
||||||
Display a player that is cool.
|
Display a player that is cool.
|
||||||
|
@ -20,8 +24,9 @@ class Player(sections.Section):
|
||||||
"""
|
"""
|
||||||
#default_sounds
|
#default_sounds
|
||||||
|
|
||||||
@staticmethod
|
@decorators.part
|
||||||
def on_air():
|
@decorators.template(template_name = 'aircox/cms/list_item.html')
|
||||||
|
def on_air(cl, request):
|
||||||
"""
|
"""
|
||||||
View that return what is on air formatted in JSON.
|
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():
|
if not qs or not qs[0].is_date_in_my_range():
|
||||||
return None
|
return ''
|
||||||
|
|
||||||
qs = qs[0]
|
qs = qs[0]
|
||||||
post = models.Diffusion.objects.filter(related = qs)
|
post = models.Diffusion.objects.filter(related = qs)
|
||||||
if not post:
|
if not post:
|
||||||
post = models.Program.objects.filter(related = qs.program)
|
post = models.Program.objects.filter(related = qs.program)
|
||||||
|
|
||||||
if not post:
|
if not post:
|
||||||
post = ListItem(title = qs.program.name)
|
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):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
context.update({
|
context.update({
|
||||||
'base_template': 'aircox/cms/section.html',
|
'base_template': 'aircox/cms/section.html',
|
||||||
'live_streams': self.live_streams,
|
'live_streams': self.live_streams,
|
||||||
|
|
|
@ -56,10 +56,10 @@
|
||||||
|
|
||||||
.playlists nav a {
|
.playlists nav a {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlists nav > *[selected],
|
.playlists nav > *[selected] {
|
||||||
.playlists .item[selected] {
|
|
||||||
color: black;
|
color: black;
|
||||||
border-top-right-radius: 0.2em;
|
border-top-right-radius: 0.2em;
|
||||||
background-color: rgba(0, 126, 223, 0.8);
|
background-color: rgba(0, 126, 223, 0.8);
|
||||||
|
@ -80,10 +80,6 @@
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist .item .title {
|
|
||||||
color: #007EDF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlist .item .info {
|
.playlist .item .info {
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
@ -94,6 +90,9 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playlist .item[selected] .title {
|
||||||
|
color: #007EDF;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<div id="player">
|
<div id="player">
|
||||||
|
@ -139,6 +138,7 @@ function Playlist(id, name, items) {
|
||||||
player.select_playlist(self);
|
player.select_playlist(self);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, true);
|
}, true);
|
||||||
|
this.tab.className = 'tab';
|
||||||
this.tab.innerHTML = name;
|
this.tab.innerHTML = name;
|
||||||
|
|
||||||
player.playlists.appendChild(this.playlist);
|
player.playlists.appendChild(this.playlist);
|
||||||
|
@ -163,10 +163,12 @@ Playlist.prototype = {
|
||||||
else
|
else
|
||||||
self.playlist.appendChild(item);
|
self.playlist.appendChild(item);
|
||||||
|
|
||||||
|
info.item = item;
|
||||||
|
item.info = info;
|
||||||
|
|
||||||
item.querySelector('.title').innerHTML = info.title;
|
item.querySelector('.title').innerHTML = info.title;
|
||||||
item.querySelector('.detail').href = info.url;
|
item.querySelector('.detail').href = info.url;
|
||||||
item.querySelector('.info').innerHTML = info.info || '';
|
item.querySelector('.info').innerHTML = info.info || '';
|
||||||
item.info = info;
|
|
||||||
|
|
||||||
item.addEventListener('click', function(event) {
|
item.addEventListener('click', function(event) {
|
||||||
if(event.target.className.indexOf('detail') != -1)
|
if(event.target.className.indexOf('detail') != -1)
|
||||||
|
@ -174,7 +176,6 @@ Playlist.prototype = {
|
||||||
player.select(event.currentTarget.info);
|
player.select(event.currentTarget.info);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
this.items.push(info);
|
this.items.push(info);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -228,6 +229,16 @@ player = {
|
||||||
this.select(this.live.items[0], false)
|
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 a given item { title, src }
|
||||||
play: function() {
|
play: function() {
|
||||||
var player = this.player;
|
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 the current track to play, and start playing it
|
||||||
select: function(item, play = true) {
|
select: function(item, play = true) {
|
||||||
var audio = this.audio;
|
var audio = this.audio;
|
||||||
|
var player = this.player;
|
||||||
|
|
||||||
audio.pause();
|
audio.pause();
|
||||||
audio.src = item.stream;
|
audio.src = item.stream;
|
||||||
audio.load()
|
audio.load()
|
||||||
|
|
||||||
self.item = item;
|
if(this.item && this.item.item)
|
||||||
this.set_info(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)
|
if(play)
|
||||||
this.play();
|
this.play();
|
||||||
},
|
},
|
||||||
|
|
||||||
/// select current playlist to show
|
/// remove selection using the given selector.
|
||||||
select_playlist: function(playlist) {
|
__unselect: function (selector) {
|
||||||
v = this.player.querySelectorAll('.playlists *[selected]');
|
v = this.player.querySelectorAll(selector);
|
||||||
if(v)
|
if(v)
|
||||||
for(var i = 0; i < v.length; i++)
|
for(var i = 0; i < v.length; i++)
|
||||||
v[i].removeAttribute('selected');
|
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
|
self.playlist = playlist
|
||||||
playlist.playlist.setAttribute('selected', 'true');
|
playlist.playlist.setAttribute('selected', 'true');
|
||||||
|
|
|
@ -22,27 +22,14 @@ function update_schedule(event) {
|
||||||
return;
|
return;
|
||||||
console.log(schedule.className)
|
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 = [ {% for field in list.fields %}"fields={{ field }}",{% endfor %} ];
|
||||||
fields = fields.join('&');
|
fields = fields.join('&');
|
||||||
|
|
||||||
xhr.open('GET', url + '?embed=1&' + fields, true);
|
part = new Part(url, 'embed&' + fields);
|
||||||
xhr.send();
|
part.get().select({
|
||||||
|
'header': ['header', 'innerHTML', true],
|
||||||
|
'content': ['.content', 'innerHTML', true],
|
||||||
|
}).map(schedule).send();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<a href="{{ prev_week }}" onclick="update_schedule(event); return true;"><</a>
|
<a href="{{ prev_week }}" onclick="update_schedule(event); return true;"><</a>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user