rewrite tests + fix error in schedule generator
This commit is contained in:
		@ -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')),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										280
									
								
								cms/static/cms/js/player.js
									
									
									
									
									
										Normal 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() {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										68
									
								
								cms/static/cms/js/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								cms/static/cms/js/utils.js
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										61
									
								
								cms/templates/cms/sections/section_player.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								cms/templates/cms/sections/section_player.html
									
									
									
									
									
										Normal 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 %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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" %}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import calendar
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
import logging
 | 
			
		||||
@ -400,6 +401,18 @@ class Schedule(models.Model):
 | 
			
		||||
        date = date_or_default(date, True).replace(day=1)
 | 
			
		||||
        freq = self.frequency
 | 
			
		||||
 | 
			
		||||
        # last of the month
 | 
			
		||||
        if freq == Schedule.Frequency.last:
 | 
			
		||||
            date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
 | 
			
		||||
 | 
			
		||||
            # end of month before the wanted weekday: move one week back
 | 
			
		||||
            if date.weekday() < self.date.weekday():
 | 
			
		||||
                date -= datetime.timedelta(days = 7)
 | 
			
		||||
 | 
			
		||||
            delta = self.date.weekday() - date.weekday()
 | 
			
		||||
            date += datetime.timedelta(days = delta)
 | 
			
		||||
            return [self.normalize(date)]
 | 
			
		||||
 | 
			
		||||
        # move to the first day of the month that matches the schedule's weekday
 | 
			
		||||
        # check on SO#3284452 for the formula
 | 
			
		||||
        first_weekday = date.weekday()
 | 
			
		||||
@ -408,19 +421,9 @@ class Schedule(models.Model):
 | 
			
		||||
                                    - first_weekday + sched_weekday)
 | 
			
		||||
        month = date.month
 | 
			
		||||
 | 
			
		||||
        # last of the month
 | 
			
		||||
        if freq == Schedule.Frequency.last:
 | 
			
		||||
            date += tz.timedelta(days = 4 * 7)
 | 
			
		||||
            next_date = date + tz.timedelta(days = 7)
 | 
			
		||||
            if next_date.month == month:
 | 
			
		||||
                date = next_date
 | 
			
		||||
            return [self.normalize(date)]
 | 
			
		||||
 | 
			
		||||
        dates = []
 | 
			
		||||
        if freq == Schedule.Frequency.one_on_two:
 | 
			
		||||
            # NOTE previous algorithm was based on the week number, but this
 | 
			
		||||
            # approach is wrong because number of weeks in a year can be
 | 
			
		||||
            # 52 or 53. This also clashes with the first week of the year.
 | 
			
		||||
            # check date base on a diff of dates base on a 14 days delta
 | 
			
		||||
            diff = as_date(date, False) - as_date(self.date, False)
 | 
			
		||||
            if diff.days % 14:
 | 
			
		||||
                date += tz.timedelta(days = 7)
 | 
			
		||||
@ -445,36 +448,31 @@ class Schedule(models.Model):
 | 
			
		||||
        If exclude_saved, exclude all diffusions that are yet in the database.
 | 
			
		||||
        """
 | 
			
		||||
        dates = self.dates_of_month(date)
 | 
			
		||||
        saved = Diffusion.objects.filter(start__in = dates,
 | 
			
		||||
                                         program = self.program)
 | 
			
		||||
        diffusions = []
 | 
			
		||||
 | 
			
		||||
        duration = utils.to_timedelta(self.duration)
 | 
			
		||||
 | 
			
		||||
        # existing diffusions
 | 
			
		||||
        for item in saved:
 | 
			
		||||
        for item in Diffusion.objects.filter(
 | 
			
		||||
                program = self.program, start__in = dates):
 | 
			
		||||
            if item.start in dates:
 | 
			
		||||
                dates.remove(item.start)
 | 
			
		||||
            if not exclude_saved:
 | 
			
		||||
                diffusions.append(item)
 | 
			
		||||
 | 
			
		||||
        # others
 | 
			
		||||
        for date in dates:
 | 
			
		||||
            first_date = date
 | 
			
		||||
            if self.initial:
 | 
			
		||||
                first_date -= self.date - self.initial.date
 | 
			
		||||
 | 
			
		||||
            first_diffusion = Diffusion.objects.filter(start = first_date,
 | 
			
		||||
                                                       program = self.program)
 | 
			
		||||
            first_diffusion = first_diffusion[0] if first_diffusion.count() \
 | 
			
		||||
                              else None
 | 
			
		||||
            diffusions.append(Diffusion(
 | 
			
		||||
                                 program = self.program,
 | 
			
		||||
                                 type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                                 initial = first_diffusion if self.initial else None,
 | 
			
		||||
                                 start = date,
 | 
			
		||||
                                 end = date + duration,
 | 
			
		||||
                             ))
 | 
			
		||||
        # new diffusions
 | 
			
		||||
        duration = utils.to_timedelta(self.duration)
 | 
			
		||||
        if self.initial:
 | 
			
		||||
            delta = self.date - self.initial.date
 | 
			
		||||
        diffusions += [
 | 
			
		||||
            Diffusion(
 | 
			
		||||
                program = self.program,
 | 
			
		||||
                type = Diffusion.Type.unconfirmed,
 | 
			
		||||
                initial = \
 | 
			
		||||
                    Diffusion.objects.filter(start = date - delta).first() \
 | 
			
		||||
                        if self.initial else None,
 | 
			
		||||
                start = date,
 | 
			
		||||
                end = date + duration,
 | 
			
		||||
            ) for date in dates
 | 
			
		||||
        ]
 | 
			
		||||
        return diffusions
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
 | 
			
		||||
@ -1,79 +1,66 @@
 | 
			
		||||
import datetime
 | 
			
		||||
import calendar
 | 
			
		||||
import logging
 | 
			
		||||
from dateutil.relativedelta import relativedelta
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
 | 
			
		||||
from aircox.programs.models import *
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('aircox.test')
 | 
			
		||||
logger.setLevel('INFO')
 | 
			
		||||
 | 
			
		||||
class Programs (TestCase):
 | 
			
		||||
    def setUp (self):
 | 
			
		||||
        stream = Stream.objects.get_or_create(
 | 
			
		||||
            name = 'diffusions',
 | 
			
		||||
            defaults = { 'type': Stream.Type.schedule }
 | 
			
		||||
        )[0]
 | 
			
		||||
        Program.objects.create(name = 'source', stream = stream)
 | 
			
		||||
        Program.objects.create(name = 'microouvert', stream = stream)
 | 
			
		||||
class ScheduleCheck (TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.schedules = [
 | 
			
		||||
            Schedule(
 | 
			
		||||
                date = tz.now(),
 | 
			
		||||
                duration = datetime.time(1,30),
 | 
			
		||||
                frequency = frequency,
 | 
			
		||||
            )
 | 
			
		||||
            for frequency in Schedule.Frequency.__members__.values()
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.schedules = {}
 | 
			
		||||
        self.programs = {}
 | 
			
		||||
    def test_frequencies(self):
 | 
			
		||||
        for schedule in self.schedules:
 | 
			
		||||
            logger.info('- test frequency %s' % schedule.get_frequency_display())
 | 
			
		||||
            date = schedule.date
 | 
			
		||||
            count = 24
 | 
			
		||||
            while count:
 | 
			
		||||
                logger.info('- month %(month)s/%(year)s' % {
 | 
			
		||||
                    'month': date.month,
 | 
			
		||||
                    'year': date.year
 | 
			
		||||
                })
 | 
			
		||||
                count -= 1
 | 
			
		||||
                dates = schedule.dates_of_month(date)
 | 
			
		||||
                if schedule.frequency == schedule.Frequency.one_on_two:
 | 
			
		||||
                    self.check_one_on_two(schedule, date, dates)
 | 
			
		||||
                elif schedule.frequency == schedule.Frequency.last:
 | 
			
		||||
                    self.check_last(schedule, date, dates)
 | 
			
		||||
                else:
 | 
			
		||||
                    pass
 | 
			
		||||
                date += relativedelta(months = 1)
 | 
			
		||||
 | 
			
		||||
    def test_create_programs_schedules (self):
 | 
			
		||||
        program = Program.objects.get(name = 'source')
 | 
			
		||||
    def check_one_on_two(self, schedule, date, dates):
 | 
			
		||||
        for date in dates:
 | 
			
		||||
            delta = date.date() - schedule.date.date()
 | 
			
		||||
            self.assertEqual(delta.days % 14, 0)
 | 
			
		||||
 | 
			
		||||
        sched_0 = self.create_schedule(program, 'one on two', [
 | 
			
		||||
                tz.datetime(2015, 10, 2, 18),
 | 
			
		||||
                tz.datetime(2015, 10, 16, 18),
 | 
			
		||||
                tz.datetime(2015, 10, 30, 18),
 | 
			
		||||
            ]
 | 
			
		||||
        )
 | 
			
		||||
        sched_1 = self.create_schedule(program, 'one on two', [
 | 
			
		||||
                tz.datetime(2015, 10, 5, 18),
 | 
			
		||||
                tz.datetime(2015, 10, 19, 18),
 | 
			
		||||
            ],
 | 
			
		||||
            rerun = sched_0
 | 
			
		||||
        )
 | 
			
		||||
    def check_last(self, schedule, date, dates):
 | 
			
		||||
        month_info = calendar.monthrange(date.year, date.month)
 | 
			
		||||
        date = datetime.date(date.year, date.month, month_info[1])
 | 
			
		||||
 | 
			
		||||
        self.programs[program.pk] = program
 | 
			
		||||
        # end of month before the wanted weekday: move one week back
 | 
			
		||||
        if date.weekday() < schedule.date.weekday():
 | 
			
		||||
            date -= datetime.timedelta(days = 7)
 | 
			
		||||
 | 
			
		||||
        program = Program.objects.get(name = 'microouvert')
 | 
			
		||||
        # special case with november first week starting on sunday
 | 
			
		||||
        sched_2 = self.create_schedule(program, 'first and third', [
 | 
			
		||||
                tz.datetime(2015, 11, 6, 18),
 | 
			
		||||
                tz.datetime(2015, 11, 20, 18),
 | 
			
		||||
            ],
 | 
			
		||||
            date = tz.datetime(2015, 10, 23, 18),
 | 
			
		||||
        )
 | 
			
		||||
        date -= datetime.timedelta(days = date.weekday())
 | 
			
		||||
        date += datetime.timedelta(days = schedule.date.weekday())
 | 
			
		||||
        self.assertEqual(date, dates[0].date())
 | 
			
		||||
 | 
			
		||||
    def check_n_of_week(self, schedule, date, dates):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def create_schedule (self, program, frequency, dates, date = None, rerun = None):
 | 
			
		||||
        frequency = Schedule.Frequency[frequency]
 | 
			
		||||
        schedule = Schedule(
 | 
			
		||||
            program = program,
 | 
			
		||||
            frequency = frequency,
 | 
			
		||||
            date = date or dates[0],
 | 
			
		||||
            rerun = rerun,
 | 
			
		||||
            duration = datetime.time(1, 30)
 | 
			
		||||
        )
 | 
			
		||||
        print(schedule.__dict__)
 | 
			
		||||
        schedule.save()
 | 
			
		||||
 | 
			
		||||
        self.schedules[schedule.pk] = (schedule, dates)
 | 
			
		||||
        return schedule
 | 
			
		||||
 | 
			
		||||
    def test_check_schedule (self):
 | 
			
		||||
        for schedule, dates in self.schedules:
 | 
			
		||||
            dates = [ tz.make_aware(date) for date in dates ]
 | 
			
		||||
            dates.sort()
 | 
			
		||||
 | 
			
		||||
            # dates
 | 
			
		||||
            dates_ = schedule.dates_of_month(dates[0])
 | 
			
		||||
            dates_.sort()
 | 
			
		||||
            self.assertEqual(dates_, dates)
 | 
			
		||||
 | 
			
		||||
            # diffusions
 | 
			
		||||
            dates_ = schedule.diffusions_of_month(dates[0])
 | 
			
		||||
            dates_ = [date_.date for date_ in dates_]
 | 
			
		||||
            dates_.sort()
 | 
			
		||||
            self.assertEqual(dates_, dates)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user