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
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
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 %}
|
||||
<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 }}
|
||||
|
|
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>
|
||||
<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>
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;"><</a>
|
||||
|
|
Loading…
Reference in New Issue
Block a user