localstorage; parts becomes expose
This commit is contained in:
parent
833e7a551d
commit
9769e0e617
|
@ -5,75 +5,6 @@ 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(name):
|
||||
"""
|
||||
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
|
||||
* hide_empty: an empty context returns an empty string
|
||||
"""
|
||||
def wrapper(func):
|
||||
def view_(cl, request, *args, **kwargs):
|
||||
context = func(cl, request, *args, **kwargs)
|
||||
def template_(func):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if kwargs.get('cl'):
|
||||
context = func(kwargs.pop('cl'), request, *args, **kwargs)
|
||||
else:
|
||||
context = func(request, *args, **kwargs)
|
||||
if not context:
|
||||
return ''
|
||||
context['embed'] = True
|
||||
return render_to_string(name, context, request=request)
|
||||
view_.__name__ = func.__name__
|
||||
return view_
|
||||
return wrapper
|
||||
return template_
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class Route:
|
|||
|
||||
@classmethod
|
||||
def get_view_name(cl, name):
|
||||
return name + '_' + cl.name
|
||||
return name + '.' + cl.name
|
||||
|
||||
@classmethod
|
||||
def as_url(cl, name, view, view_kwargs = None):
|
||||
|
|
|
@ -35,8 +35,8 @@ class Website:
|
|||
## components
|
||||
urls = []
|
||||
"""list of urls generated thourgh registrations"""
|
||||
parts = []
|
||||
"""list of registered parts (done through sections registration)"""
|
||||
exposures = []
|
||||
"""list of registered exposures (done through sections registration)"""
|
||||
registry = {}
|
||||
"""dict of registered models by their name"""
|
||||
|
||||
|
@ -45,8 +45,8 @@ class Website:
|
|||
* menus: a list of menus to add to the website
|
||||
"""
|
||||
self.registry = {}
|
||||
self.parts = []
|
||||
self.urls = [ url(r'^parts/', include(self.parts)) ]
|
||||
self.exposures = []
|
||||
self.urls = [ url(r'^exp/', include(self.exposures)) ]
|
||||
self.menus = {}
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
@ -96,18 +96,18 @@ class Website:
|
|||
model._website = self
|
||||
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__'):
|
||||
sections = [sections]
|
||||
|
||||
for section in sections:
|
||||
if not hasattr(section, '_parts'):
|
||||
if not hasattr(section, '_exposure'):
|
||||
continue
|
||||
self.parts += [
|
||||
url for url in section._parts
|
||||
self.exposures += [
|
||||
url for url in section._exposure.items
|
||||
if url not in self.urls
|
||||
]
|
||||
|
||||
|
@ -129,7 +129,7 @@ class Website:
|
|||
view_kwargs['menus'] = self.menus
|
||||
|
||||
if sections:
|
||||
self.register_parts(sections)
|
||||
self.register_exposures(sections)
|
||||
view_kwargs['sections'] = sections
|
||||
|
||||
view = view.as_view(
|
||||
|
@ -172,7 +172,7 @@ class Website:
|
|||
elif menu.position in ('left', 'right'):
|
||||
menu.tag = 'side'
|
||||
self.menus[menu.position] = menu
|
||||
self.register_parts(menu.sections)
|
||||
self.register_exposures(menu.sections)
|
||||
|
||||
def get_menu(self, position):
|
||||
"""
|
||||
|
|
|
@ -12,7 +12,7 @@ import aircox.cms.decorators as decorators
|
|||
import aircox.website.models as models
|
||||
|
||||
|
||||
@decorators.parts
|
||||
@decorators.expose
|
||||
class Player(sections.Section):
|
||||
"""
|
||||
Display a player that is cool.
|
||||
|
@ -24,8 +24,7 @@ class Player(sections.Section):
|
|||
"""
|
||||
#default_sounds
|
||||
|
||||
@decorators.part
|
||||
@decorators.template('aircox/cms/list_item.html')
|
||||
@decorators.expose
|
||||
def on_air(cl, request):
|
||||
qs = programs.Diffusion.get(
|
||||
now = True,
|
||||
|
@ -47,6 +46,8 @@ class Player(sections.Section):
|
|||
'item': post,
|
||||
'list': sections.List,
|
||||
}
|
||||
on_air._exposure.template_name = 'aircox/cms/list_item.html'
|
||||
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
|
|
|
@ -5,36 +5,28 @@
|
|||
|
||||
{% block header %}
|
||||
<style>
|
||||
#player {
|
||||
background-color: #212121;
|
||||
color: #818181;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
|
||||
.player-box {
|
||||
border-top-left-radius: 0.5em;
|
||||
border-top-right-radius: 0.5em;
|
||||
border: 1px #212121 solid;
|
||||
border-bottom: none;
|
||||
padding-top: 0.2em;
|
||||
}
|
||||
|
||||
.player-box * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.player-box h3 {
|
||||
.player-box h3, #player h2 {
|
||||
display: inline-block;
|
||||
font-size: 0.9em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.player-button {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
margin-right: 1em;
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.player-button img {
|
||||
|
@ -47,8 +39,6 @@
|
|||
}
|
||||
|
||||
#player .on_air {
|
||||
background-color: #414141;
|
||||
box-shadow: inset 0 0 0.2em rgba(0, 0, 0, 0.5);
|
||||
padding: 0.2em;
|
||||
margin: 0.2em 0em;
|
||||
}
|
||||
|
@ -58,43 +48,27 @@
|
|||
display: inline;
|
||||
}
|
||||
|
||||
#player .on_air .title:before {
|
||||
content: "{% trans "on air //" %}";
|
||||
color: red;
|
||||
margin: 0.2em;
|
||||
}
|
||||
|
||||
#player .on_air a {
|
||||
float: right;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.playlists {}
|
||||
|
||||
.playlists nav {
|
||||
font-size: 0.8em;
|
||||
border-bottom: 1px solid #007EDF;
|
||||
}
|
||||
|
||||
.playlists nav a {
|
||||
padding: 0.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.playlists nav > *[selected] {
|
||||
color: black;
|
||||
border-top-right-radius: 0.2em;
|
||||
background-color: rgba(0, 126, 223, 0.8);
|
||||
.playlists {
|
||||
}
|
||||
|
||||
.playlists ul:not([selected]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.playlists nav a.close {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.playlist {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 15em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.playlist .item > * {
|
||||
|
@ -105,17 +79,12 @@
|
|||
|
||||
.playlist .item .info {
|
||||
float: right;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.playlist .item a {
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.playlist .item[selected] .title {
|
||||
color: #007EDF;
|
||||
}
|
||||
</style>
|
||||
<div id="player">
|
||||
<li class='item' style="display: none;">
|
||||
|
@ -135,7 +104,8 @@
|
|||
</audio>
|
||||
|
||||
<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>
|
||||
|
||||
<h3 class="title"></h3>
|
||||
|
@ -147,17 +117,26 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="playlists">
|
||||
<nav></nav>
|
||||
<nav>
|
||||
<a onclick="player.select_playlist()" class="close"
|
||||
title="{% trans "close" %}">✖</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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.store = store;
|
||||
|
||||
var self = this;
|
||||
this.playlist = document.createElement('ul');
|
||||
this.playlist.setAttribute('id', id);
|
||||
this.playlist.setAttribute('id', 'playlist-' + name );
|
||||
this.playlist.className = 'playlist list';
|
||||
|
||||
this.tab = document.createElement('a');
|
||||
|
@ -166,12 +145,14 @@ function Playlist(id, name, items) {
|
|||
event.preventDefault();
|
||||
}, true);
|
||||
this.tab.className = 'tab';
|
||||
this.tab.innerHTML = name;
|
||||
this.tab.innerHTML = tab;
|
||||
|
||||
player.playlists.appendChild(this.playlist);
|
||||
player.playlists.querySelector('nav').appendChild(this.tab);
|
||||
|
||||
self.items = [];
|
||||
this.items = [];
|
||||
if(store)
|
||||
this.load()
|
||||
if(items)
|
||||
this.add_list(items);
|
||||
}
|
||||
|
@ -188,7 +169,7 @@ Playlist.prototype = {
|
|||
if(container)
|
||||
container.appendChild(item);
|
||||
else
|
||||
self.playlist.appendChild(item);
|
||||
this.playlist.appendChild(item);
|
||||
|
||||
info.item = item;
|
||||
item.info = info;
|
||||
|
@ -208,6 +189,9 @@ Playlist.prototype = {
|
|||
event.preventDefault();
|
||||
}, true);
|
||||
this.items.push(info);
|
||||
|
||||
if(this.store)
|
||||
this.save()
|
||||
},
|
||||
|
||||
/// Add a list of items (optimized)
|
||||
|
@ -217,6 +201,26 @@ Playlist.prototype = {
|
|||
this.add(items[i], 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.playlists = this.player.querySelector('.playlists');
|
||||
this.live = new Playlist(
|
||||
'playlist-live',
|
||||
"{% trans "live" %}",
|
||||
'live',
|
||||
"☰ {% trans "live" %}",
|
||||
[ {% for sound in live_streams %}
|
||||
{ title: "{{ sound.title }}",
|
||||
url: "{{ sound.url }}",
|
||||
|
@ -242,8 +246,7 @@ player = {
|
|||
}, {% endfor %} ]
|
||||
);
|
||||
this.recents = new Playlist(
|
||||
'playlist-recents',
|
||||
"{% trans "recents" %}",
|
||||
'recents', '☀ {% trans "recents" %}',
|
||||
[ {% for sound in last_sounds %}
|
||||
{ title: "{{ sound.title }}",
|
||||
url: "{{ sound.url }}",
|
||||
|
@ -255,15 +258,18 @@ player = {
|
|||
info: "{{ sound.related.duration|date:"i:s" }}",
|
||||
}, {% 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.update_on_air();
|
||||
},
|
||||
|
||||
/// update on air informations
|
||||
update_on_air: function() {
|
||||
part = Part('{% url "player_on_air" %}').get()
|
||||
part = Part('{% url "exp.player.on_air" %}').get()
|
||||
.select({
|
||||
title: '.title',
|
||||
url: ['.url', 'href'],
|
||||
|
@ -332,8 +338,10 @@ player = {
|
|||
this.__unselect('.playlists .playlist[selected]');
|
||||
|
||||
self.playlist = playlist
|
||||
if(playlist) {
|
||||
playlist.playlist.setAttribute('selected', 'true');
|
||||
playlist.tab.setAttribute('selected', 'true');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user