rewrite tests + fix error in schedule generator

This commit is contained in:
bkfox
2016-07-28 14:45:26 +02:00
parent d1debc5b9f
commit 502af1dba0
9 changed files with 532 additions and 128 deletions

View File

@ -414,6 +414,9 @@ class DiffusionPage(Publication):
related_name = 'page',
on_delete=models.SET_NULL,
null=True,
limit_choices_to = {
'initial__isnull': True,
},
)
class Meta:
@ -423,7 +426,7 @@ class DiffusionPage(Publication):
content_panels = [
FieldPanel('diffusion'),
] + Publication.content_panels + [
InlinePanel('tracks', label=_('Tracks'))
InlinePanel('tracks', label=_('Tracks')),
]

View File

@ -890,8 +890,8 @@ class SectionLogsList(SectionItem):
@register_snippet
class SectionTimetable(SectionItem,DatedListBase):
class Meta:
verbose_name = _('timetable')
verbose_name_plural = _('timetable')
verbose_name = _('Section: Timetable')
verbose_name_plural = _('Sections: Timetable')
panels = SectionItem.panels + DatedListBase.panels
@ -914,8 +914,8 @@ class SectionTimetable(SectionItem,DatedListBase):
@register_snippet
class SectionPublicationInfo(SectionItem):
class Meta:
verbose_name = _('section with publication\'s info')
verbose_name = _('sections with publication\'s info')
verbose_name = _('Section: publication\'s info')
verbose_name_plural = _('Sections: publication\'s info')
@register_snippet
class SectionSearchField(SectionItem):
@ -933,8 +933,8 @@ class SectionSearchField(SectionItem):
)
class Meta:
verbose_name = _('search field')
verbose_name_plural = _('search fields')
verbose_name = _('Section: search field')
verbose_name_plural = _('Sections: search field')
panels = SectionItem.panels + [
PageChooserPanel('page'),
@ -946,6 +946,32 @@ class SectionSearchField(SectionItem):
context = super().get_context(request, page)
list_page = self.page or ListPage.objects.live().first()
context['list_page'] = list_page
print(context, self.template)
return context
@register_snippet
class SectionPlayer(SectionItem):
live_title = models.CharField(
_('live title'),
max_length = 32,
help_text = _('text to display when it plays live'),
)
streams = models.TextField(
_('audio streams'),
help_text = _('one audio stream per line'),
)
class Meta:
verbose_name = _('Section: Player')
panels = SectionItem.panels + [
FieldPanel('live_title'),
FieldPanel('streams'),
]
def get_context(self, request, page):
context = super().get_context(request, page)
context['streams'] = self.streams.split('\r\n')
return context

280
cms/static/cms/js/player.js Normal file
View File

@ -0,0 +1,280 @@
// TODO
// - multiple sources for an item
// - live streams as item;
// - add to playlist button
//
/// Return a human-readable string from seconds
function duration_str(seconds) {
seconds = Math.floor(seconds);
var hours = Math.floor(seconds / 3600);
seconds -= hours;
var minutes = Math.floor(seconds / 60);
seconds -= minutes;
var str = hours ? (hours < 10 ? '0' + hours : hours) + ':' : '';
str += (minutes < 10 ? '0' + minutes : minutes) + ':';
str += (seconds < 10 ? '0' + seconds : seconds);
return str;
}
function Sound(title, detail, stream, duration) {
this.title = title;
this.detail = detail;
this.stream = stream;
this.duration = duration;
}
Sound.prototype = {
title: '',
detail: '',
stream: '',
duration: undefined,
item: undefined,
get seekable() {
return this.duration != undefined;
},
}
function PlayerPlaylist(player) {
this.player = player;
this.playlist = player.player.querySelector('.playlist');
this.item_ = player.player.querySelector('.playlist .item');
this.items = []
}
PlayerPlaylist.prototype = {
items: undefined,
find: function(stream) {
return this.items.find(function(v) {
return v.stream == item;
});
},
add: function(sound, container) {
if(this.find(sound.stream))
return;
var item = this.item_.cloneNode(true);
item.removeAttribute('style');
console.log(sound)
item.querySelector('.title').innerHTML = sound.title;
if(sound.seekable)
item.querySelector('.duration').innerHTML =
duration_str(sound.duration);
if(sound.detail)
item.querySelector('.detail').href = sound.detail;
item.sound = sound;
sound.item = item;
var self = this;
item.querySelector('.action.remove').addEventListener(
'click', function(event) { self.remove(sound); }, false
);
(container || this.playlist).appendChild(item);
this.items.push(sound);
this.save();
},
remove: function(sound) {
var index = this.items.indexOf(sound);
if(index != -1)
this.items.splice(index,1);
this.playlist.removeChild(sound.item);
this.save();
},
save: function() {
var list = [];
for(var i in this.items) {
var sound = Object.assign({}, this.items[i])
delete sound.item;
list.push(sound);
}
this.player.store.set('playlist', list);
},
load: function() {
var list = [];
var container = document.createDocumentFragment();
for(var i in list)
this.add(list[i], container)
this.playlist.appendChild(container);
},
}
function Player(id) {
this.store = new Store('player');
// html items
this.player = document.getElementById(id);
this.box = this.player.querySelector('.box');
this.audio = this.player.querySelector('audio');
this.controls = {
duration: this.box.querySelector('.duration'),
progress: this.player.querySelector('progress'),
single: this.player.querySelector('input.single'),
}
this.playlist = new PlayerPlaylist(this);
this.playlist.load();
this.init_events();
this.load();
}
Player.prototype = {
/// current item being played
sound: undefined,
init_events: function() {
var self = this;
function time_from_progress(event) {
bounding = self.controls.progress.getBoundingClientRect()
offset = (event.clientX - bounding.left);
return offset * self.audio.duration / bounding.width;
}
function update_info() {
var controls = self.controls;
// progress
if( !self.sound.seekable ||
self.audio.duration == Infinity) {
controls.duration.innerHTML = '';
controls.progress.value = 0;
return;
}
var pos = self.audio.currentTime;
controls.progress.value = pos;
controls.progress.max = self.audio.duration;
controls.duration.innerHTML = duration_str(sound.duration);
}
// audio
this.audio.addEventListener('playing', function() {
self.player.setAttribute('state', 'playing');
}, false);
this.audio.addEventListener('pause', function() {
self.player.setAttribute('state', 'paused');
}, false);
this.audio.addEventListener('loadstart', function() {
self.player.setAttribute('state', 'stalled');
}, false);
this.audio.addEventListener('loadeddata', function() {
self.player.removeAttribute('state');
}, false);
this.audio.addEventListener('timeupdate', update_info, false);
this.audio.addEventListener('ended', function() {
if(!self.controls.single.checked)
self.next(true);
}, false);
// buttons
this.box.querySelector('button.play').onclick = function() {
self.play();
};
// progress
progress = this.controls.progress;
progress.addEventListener('click', function(event) {
player.audio.currentTime = time_from_progress(event);
}, false);
progress.addEventListener('mouseout', update_info, false);
progress.addEventListener('mousemove', function(event) {
if(self.audio.duration == Infinity)
return;
var pos = time_from_progress(event);
self.controls.duration.innerHTML = duration_str(pos);
}, false);
},
play: function() {
if(this.audio.paused)
this.audio.play();
else
this.audio.pause();
},
unselect: function(sound) {
sound.item.removeAttribute('selected');
},
select: function(sound, play = true) {
if(this.sound)
this.unselect(this.sound);
this.audio.pause();
// if stream is a list, use <source>
if(sound.stream.splice) {
this.audio.src="";
var sources = this.audio.querySelectorAll('source');
for(var i in sources)
this.audio.removeChild(sources[i]);
for(var i in sound.stream) {
var source = document.createElement('source');
source.src = sound.stream[i];
}
}
else
this.audio.src = sound.stream;
this.audio.load();
this.sound = sound;
sound.item.setAttribute('selected', 'true');
this.box.querySelector('.title').innerHTML = sound.title;
if(play)
this.play();
},
next: function() {
var index = this.playlist.items.indexOf(this.sound);
if(index < 0)
return;
index++;
if(index < this.playlist.items.length)
this.select(this.playlist.items[index], true);
},
save: function() {
this.store.set('player', {
single: this.controls.single.checked,
sound: this.sound && this.sound.stream,
});
},
load: function() {
var data = this.store.get('player');
this.controls.single.checked = data.single;
this.sound = this.playlist.find(data.stream);
},
update_on_air: function() {
},
}

View File

@ -0,0 +1,68 @@
/// Helper to provide a tab+panel functionnality; the tab and the selected
/// element will have an attribute "selected".
/// We assume a common ancestor between tab and panel at a maximum level
/// of 2.
/// * tab: corresponding tab
/// * panel_selector is used to select the right panel object.
function select_tab(tab, panel_selector) {
var parent = tab.parentNode.parentNode;
var panel = parent.querySelector(panel_selector);
// unselect
var qs = parent.querySelectorAll('*[selected]');
for(var i = 0; i < qs.length; i++)
if(qs[i] != tab && qs[i] != panel)
qs[i].removeAttribute('selected');
panel.setAttribute('selected', 'true');
tab.setAttribute('selected', 'true');
}
/// Utility to store objects in local storage. Data are stringified in JSON
/// format in order to keep type.
function Store(prefix) {
this.prefix = prefix;
}
Store.prototype = {
// save data to localstorage, or remove it if data is null
set: function(key, data) {
key = this.prefix + '.' + key;
if(data == undefined) {
localStorage.removeItem(prefix);
return;
}
localStorage.setItem(key, JSON.stringify(data))
},
// load data from localstorage
get: function(key) {
try {
key = this.prefix + '.' + key;
var data = localStorage.getItem(key);
if(data)
return JSON.parse(data);
}
catch(e) { console.log(e, data); }
},
// return true if the given item is stored
exists: function(key) {
key = this.prefix + '.' + key;
return (localStorage.getItem(key) != null);
},
// update a field in the stored data
update: function(key, field_key, value) {
data = this.get(key) || {};
if(value)
data[field_key] = value;
else
delete data[field_key];
this.set(key, data);
},
}

View File

@ -25,6 +25,9 @@
{% block css_extras %}{% endblock %}
{% endblock %}
<script src="{% static 'cms/js/utils.js' %}"></script>
<script src="{% static 'cms/js/player.js' %}"></script>
<title>{{ page.title }}</title>
</head>
<body>

View File

@ -0,0 +1,61 @@
{% extends 'cms/sections/section_item.html' %}
{% load staticfiles %}
{% load i18n %}
{% block content %}
<style>
</style>
<div id="player">
<div class="box">
<audio preload="metadata">
{% trans "Your browser does not support the <code>audio</code> element." %}
{% for stream in streams %}
<source src="{{ stream }}" />
{% endfor %}
</audio>
<button class="play" onclick="Player.play()"
title="{% trans "play/pause" %}"></button>
<h3 class="title">{{ self.live_title }}</h3>
<div>
<div class="info duration"></div>
<progress value="0" max="1"></progress>
<input type="checkbox" class="single" id="player_single_mode">
<label for="player_single_mode"></label>
</div>
</div>
<div class="playlist">
<li class='item' style="display: none;">
<h2 class="title">{{ self.live_title }}</h2>
<div class="info duration"></div>
<div class="actions">
<a class="action detail" title="{% trans "more informations" %}"></a>
<a class="action remove" title="{% trans "remove this sound" %}"></a>
</div>
</li>
</div>
<div class='item on_air'>
<h2 class="title"></h2>
<a class="url"></a>
</div>
</div>
<script>
var player = new Player('player');
player.playlist.add(new Sound('{{ self.live_title }}', '', [
{% for stream in streams %}'{{ stream }}',{% endfor %}
]));
</script>
{% endblock %}

View File

@ -1,27 +1,5 @@
{% load i18n %}
<script>
/// Function used to select a panel on a tab selection.
/// The tab should be at max level -2 of the main container
/// The panel must have a class "panel"
function select_tab(target) {
parent = target.parentNode.parentNode;
var date = target.dataset.date;
panel = parent.querySelector('.panel[data-date="' + date + '"]');
// unselect
qs = parent.querySelectorAll('*[selected]');
for(var i = 0; i < qs.length; i++)
if(qs[i].dataset.date != date)
qs[i].removeAttribute('selected');
console.log(panel, target, date);
panel.setAttribute('selected', 'true');
target.setAttribute('selected', 'true');
}
</script>
{# FIXME: get current complete URL #}
<div class="list date_list">
{% if nav_dates %}
@ -31,7 +9,7 @@
{% endif %}
{% for day in nav_dates.dates %}
<a onclick="select_tab(this);" data-date="day_{{day|date:"Y-m-d"}}"
<a onclick="select_tab(this, '.panel[data-date=\'{{day|date:"Y-m-d"}}\']');"
{% if day == nav_dates.date %}selected{% endif %}
class="tab {% if day == nav_dates.date %}today{% endif %}">
{{ day|date:'D. d' }}
@ -47,7 +25,7 @@
{% for day, list in object_list %}
<ul class="panel {% if day == nav_dates.date %}class="today"{% endif %}"
{% if day == nav_dates.date %}selected{% endif %}
data-date="day_{{day|date:"Y-m-d"}}">
data-date="{{day|date:"Y-m-d"}}">
{# you might like to hide it by default -- this more for sections #}
<h2>{{ day|date:'l d F' }}</h2>
{% with object_list=list item_date_format="H:i" %}