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

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

96
cms/decorators.py Normal file
View File

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

View File

@ -212,7 +212,7 @@ class Post (models.Model, Routable):
abstract = True 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)

View File

@ -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

View File

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

View File

@ -8,65 +8,7 @@
{% block content %} {% 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 }}

View File

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

View File

@ -3,7 +3,7 @@
<html> <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>

View File

@ -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):
""" """

View File

@ -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).

View File

@ -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,

View File

@ -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');

View File

@ -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;">&lt;</a> <a href="{{ prev_week }}" onclick="update_schedule(event); return true;">&lt;</a>