localstorage; parts becomes expose

This commit is contained in:
bkfox 2016-06-14 14:34:10 +02:00
parent 833e7a551d
commit 9769e0e617
5 changed files with 211 additions and 164 deletions

View File

@ -5,75 +5,6 @@ from django.core.urlresolvers import reverse
from django.utils.text import slugify 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(name): def template(name):
""" """
the decorated function returns a context that is used to the decorated function returns a context that is used to
@ -82,16 +13,123 @@ def template(name):
* template_name: name of the template to use * template_name: name of the template to use
* hide_empty: an empty context returns an empty string * hide_empty: an empty context returns an empty string
""" """
def wrapper(func): def template_(func):
def view_(cl, request, *args, **kwargs): def wrapper(request, *args, **kwargs):
context = func(cl, request, *args, **kwargs) if kwargs.get('cl'):
context = func(kwargs.pop('cl'), request, *args, **kwargs)
else:
context = func(request, *args, **kwargs)
if not context: if not context:
return '' return ''
context['embed'] = True context['embed'] = True
return render_to_string(name, context, request=request) return render_to_string(name, context, request=request)
view_.__name__ = func.__name__ return wrapper
return view_ return template_
return wrapper
class Exposure:
"""
Define an exposure. Look at @expose decorator.
"""
name = None
"""generated view name"""
pattern = None
"""url pattern"""
items = None
"""for classes: list of url objects for exposed methods"""
template_name = None
"""
for methods: exposed method return a context to be use with
the given template. The view will be wrapped in @template
"""
item = None
"""
exposed item
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def url(self, *args, **kwargs):
"""
reverse url for this exposure
"""
return reverse(self.name, *args, **kwargs)
def prefix(self, parent):
"""
prefix exposure with the given parent
"""
self.name = parent.name + '.' + self.name
self.pattern = parent.pattern + '/' + self.pattern
def expose(item):
"""
Expose a class and its methods as views. This allows data to be
retrieved dynamiccaly from client (e.g. with javascript).
To expose a method of a class, you must expose the class, then the
method.
The exposed method has the following signature:
`func(cl, request, parent, *args, **kwargs) -> str`
Data related to the exposure are put in the `_exposure` attribute,
as instance of Exposure.
To add extra parameter, such as template_name, just update the correct
field in func._exposure, that will be taken in account at the class
decoration.
The exposed method will be prefix'ed with it's parent class exposure.
When adding views to a website, the exposure of their sections are
added to the list of url.
"""
def get_attr(attr, default):
v = (hasattr(item, attr) and getattr(item, attr)) or default
return slugify(v.lower())
name = get_attr('name', item.__name__)
pattern = get_attr('pattern', item.__name__)
exp = Exposure(name = name, pattern = pattern, item = item)
# expose a class container: set _exposure attribute
if type(item) == type:
exp.name = 'exp.' + exp.name
exp.items = []
for func in item.__dict__.values():
if not hasattr(func, '_exposure'):
continue
sub = func._exposure
sub.prefix(exp)
# FIXME: template warping lose args
if sub.template_name:
sub.item = template(sub.template_name)(sub.item)
func = url(sub.pattern, name = sub.name,
view = func, kwargs = {'cl': item})
exp.items.append(func)
item._exposure = exp;
return item
# expose a method: wrap it
else:
if hasattr(item, '_exposure'):
del item._exposure
def wrapper(request, as_str = False, *args, **kwargs):
v = exp.item(request, *args, **kwargs)
if as_str:
return v
return HttpResponse(v)
wrapper._exposure = exp;
return wrapper

View File

@ -43,7 +43,7 @@ class Route:
@classmethod @classmethod
def get_view_name(cl, name): def get_view_name(cl, name):
return name + '_' + cl.name return name + '.' + cl.name
@classmethod @classmethod
def as_url(cl, name, view, view_kwargs = None): def as_url(cl, name, view, view_kwargs = None):

View File

@ -35,8 +35,8 @@ class Website:
## components ## components
urls = [] urls = []
"""list of urls generated thourgh registrations""" """list of urls generated thourgh registrations"""
parts = [] exposures = []
"""list of registered parts (done through sections registration)""" """list of registered exposures (done through sections registration)"""
registry = {} registry = {}
"""dict of registered models by their name""" """dict of registered models by their name"""
@ -45,8 +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.parts = [] self.exposures = []
self.urls = [ url(r'^parts/', include(self.parts)) ] self.urls = [ url(r'^exp/', include(self.exposures)) ]
self.menus = {} self.menus = {}
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@ -96,18 +96,18 @@ class Website:
model._website = self model._website = self
return name return name
def register_parts(self, sections): def register_exposures(self, sections):
""" """
Register parts that are used in the given sections. Register exposures that are used in the given sections.
""" """
if not hasattr(sections, '__iter__'): if not hasattr(sections, '__iter__'):
sections = [sections] sections = [sections]
for section in sections: for section in sections:
if not hasattr(section, '_parts'): if not hasattr(section, '_exposure'):
continue continue
self.parts += [ self.exposures += [
url for url in section._parts url for url in section._exposure.items
if url not in self.urls if url not in self.urls
] ]
@ -129,7 +129,7 @@ class Website:
view_kwargs['menus'] = self.menus view_kwargs['menus'] = self.menus
if sections: if sections:
self.register_parts(sections) self.register_exposures(sections)
view_kwargs['sections'] = sections view_kwargs['sections'] = sections
view = view.as_view( view = view.as_view(
@ -172,7 +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) self.register_exposures(menu.sections)
def get_menu(self, position): def get_menu(self, position):
""" """

View File

@ -12,7 +12,7 @@ import aircox.cms.decorators as decorators
import aircox.website.models as models import aircox.website.models as models
@decorators.parts @decorators.expose
class Player(sections.Section): class Player(sections.Section):
""" """
Display a player that is cool. Display a player that is cool.
@ -24,8 +24,7 @@ class Player(sections.Section):
""" """
#default_sounds #default_sounds
@decorators.part @decorators.expose
@decorators.template('aircox/cms/list_item.html')
def on_air(cl, request): def on_air(cl, request):
qs = programs.Diffusion.get( qs = programs.Diffusion.get(
now = True, now = True,
@ -47,6 +46,8 @@ class Player(sections.Section):
'item': post, 'item': post,
'list': sections.List, 'list': sections.List,
} }
on_air._exposure.template_name = 'aircox/cms/list_item.html'
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)

View File

@ -5,50 +5,40 @@
{% block header %} {% block header %}
<style> <style>
#player {
background-color: #212121;
color: #818181;
border-radius: 0.2em;
}
.player-box { .player-box {
border-top-left-radius: 0.5em; padding-top: 0.2em;
border-top-right-radius: 0.5em;
border: 1px #212121 solid;
border-bottom: none;
} }
.player-box * { .player-box * {
vertical-align: middle; vertical-align: middle;
} }
.player-box h3 { .player-box h3, #player h2 {
display: inline-block; display: inline-block;
font-size: 0.9em;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.player-button { .player-button {
display: inline-block; display: inline-block;
height: 2em; height: 1.2em;
width: 2em; width: 1.2em;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
margin-right: 1em; margin-right: 0.4em;
}
.player-button img {
height: inherit;
box-shadow: none;
} }
.player-button img { #player[play] .player-button img {
height: inherit; margin-left: -100%;
box-shadow: none; }
}
#player[play] .player-button img {
margin-left: -100%;
}
#player .on_air { #player .on_air {
background-color: #414141;
box-shadow: inset 0 0 0.2em rgba(0, 0, 0, 0.5);
padding: 0.2em; padding: 0.2em;
margin: 0.2em 0em; margin: 0.2em 0em;
} }
@ -58,43 +48,27 @@
display: inline; display: inline;
} }
#player .on_air .title:before {
content: "{% trans "on air //" %}";
color: red;
margin: 0.2em;
}
#player .on_air a { #player .on_air a {
float: right; float: right;
color: black;
} }
.playlists {} .playlists {
}
.playlists nav { .playlists ul:not([selected]) {
font-size: 0.8em; display: none;
border-bottom: 1px solid #007EDF; }
}
.playlists nav a { .playlists nav a.close {
padding: 0.2em; float: right;
cursor: pointer; }
}
.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;
}
.playlist { .playlist {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 15em;
overflow-y: auto;
} }
.playlist .item > * { .playlist .item > * {
@ -105,17 +79,12 @@
.playlist .item .info { .playlist .item .info {
float: right; float: right;
font-size: 0.8em;
} }
.playlist .item a { .playlist .item a {
float: right; float: right;
font-weight: bold;
} }
.playlist .item[selected] .title {
color: #007EDF;
}
</style> </style>
<div id="player"> <div id="player">
<li class='item' style="display: none;"> <li class='item' style="display: none;">
@ -135,7 +104,8 @@
</audio> </audio>
<span class="player-button" onclick="player.play()"> <span class="player-button" onclick="player.play()">
<img src="{% static "aircox/website/player_button.png" %}"> <img src="{% static "aircox/website/player_button.png" %}"
alt="{% trans "play audio" %}">
</span> </span>
<h3 class="title"></h3> <h3 class="title"></h3>
@ -147,17 +117,26 @@
</div> </div>
</div> </div>
<div class="playlists"> <div class="playlists">
<nav></nav> <nav>
<a onclick="player.select_playlist()" class="close"
title="{% trans "close" %}">✖</a>
</nav>
</div> </div>
</div> </div>
<script> <script>
function Playlist(id, name, items) { // Create a Playlist:
// * name: name of the playlist, used for container id and storage
// * tab: text to put in the tab
// * items: list of items to append
// * store: store the playlist in localStorage
function Playlist(name, tab, items, store = false) {
this.name = name; this.name = name;
this.store = store;
var self = this; var self = this;
this.playlist = document.createElement('ul'); this.playlist = document.createElement('ul');
this.playlist.setAttribute('id', id); this.playlist.setAttribute('id', 'playlist-' + name );
this.playlist.className = 'playlist list'; this.playlist.className = 'playlist list';
this.tab = document.createElement('a'); this.tab = document.createElement('a');
@ -166,12 +145,14 @@ function Playlist(id, name, items) {
event.preventDefault(); event.preventDefault();
}, true); }, true);
this.tab.className = 'tab'; this.tab.className = 'tab';
this.tab.innerHTML = name; this.tab.innerHTML = tab;
player.playlists.appendChild(this.playlist); player.playlists.appendChild(this.playlist);
player.playlists.querySelector('nav').appendChild(this.tab); player.playlists.querySelector('nav').appendChild(this.tab);
self.items = []; this.items = [];
if(store)
this.load()
if(items) if(items)
this.add_list(items); this.add_list(items);
} }
@ -188,7 +169,7 @@ Playlist.prototype = {
if(container) if(container)
container.appendChild(item); container.appendChild(item);
else else
self.playlist.appendChild(item); this.playlist.appendChild(item);
info.item = item; info.item = item;
item.info = info; item.info = info;
@ -208,6 +189,9 @@ Playlist.prototype = {
event.preventDefault(); event.preventDefault();
}, true); }, true);
this.items.push(info); this.items.push(info);
if(this.store)
this.save()
}, },
/// Add a list of items (optimized) /// Add a list of items (optimized)
@ -217,6 +201,26 @@ Playlist.prototype = {
this.add(items[i], container); this.add(items[i], container);
this.playlist.appendChild(container); this.playlist.appendChild(container);
}, },
/// Save a playlist to local storage
save: function() {
pl = []
for(var i = 0; i < this.items.length; i++)
info = Object.assign({}, this.items[i])
info.item = undefined;
pl.push(info);
localStorage.setItem('playlist.' + self.name,
JSON.stringify(pl))
},
/// Load playlist from local storage
load: function() {
pl = localStorage.getItem('playlist.' + self.name)
if(pl)
this.add_list(JSON.parse(pl));
},
} }
@ -232,8 +236,8 @@ player = {
this.audio = this.player.querySelector('audio'); this.audio = this.player.querySelector('audio');
this.playlists = this.player.querySelector('.playlists'); this.playlists = this.player.querySelector('.playlists');
this.live = new Playlist( this.live = new Playlist(
'playlist-live', 'live',
"{% trans "live" %}", "{% trans "live" %}",
[ {% for sound in live_streams %} [ {% for sound in live_streams %}
{ title: "{{ sound.title }}", { title: "{{ sound.title }}",
url: "{{ sound.url }}", url: "{{ sound.url }}",
@ -242,8 +246,7 @@ player = {
}, {% endfor %} ] }, {% endfor %} ]
); );
this.recents = new Playlist( this.recents = new Playlist(
'playlist-recents', 'recents', '☀ {% trans "recents" %}',
"{% trans "recents" %}",
[ {% for sound in last_sounds %} [ {% for sound in last_sounds %}
{ title: "{{ sound.title }}", { title: "{{ sound.title }}",
url: "{{ sound.url }}", url: "{{ sound.url }}",
@ -255,15 +258,18 @@ player = {
info: "{{ sound.related.duration|date:"i:s" }}", info: "{{ sound.related.duration|date:"i:s" }}",
}, {% endfor %} ] }, {% endfor %} ]
); );
this.favorite = new Playlist(
'favorite', '♥ {% trans "favorites" %}', null, true);
this.playlist = new Playlist(
'playlist', '★ {% trans "playlist" %}', null, true);
this.select_playlist(this.recents);
this.select(this.live.items[0], false) this.select(this.live.items[0], false)
this.update_on_air(); this.update_on_air();
}, },
/// update on air informations /// update on air informations
update_on_air: function() { update_on_air: function() {
part = Part('{% url "player_on_air" %}').get() part = Part('{% url "exp.player.on_air" %}').get()
.select({ .select({
title: '.title', title: '.title',
url: ['.url', 'href'], url: ['.url', 'href'],
@ -332,8 +338,10 @@ player = {
this.__unselect('.playlists .playlist[selected]'); this.__unselect('.playlists .playlist[selected]');
self.playlist = playlist self.playlist = playlist
playlist.playlist.setAttribute('selected', 'true'); if(playlist) {
playlist.tab.setAttribute('selected', 'true'); playlist.playlist.setAttribute('selected', 'true');
playlist.tab.setAttribute('selected', 'true');
}
}, },
} }