This commit is contained in:
Élie Bouttier 2016-10-16 23:36:03 +02:00
parent bd53789ad7
commit 7b9e252a97
8 changed files with 174 additions and 2 deletions

View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% load i18n %}
{% block planningtab %} active{% endblock %}
{% block content %}
<h1>{% trans "Program" %}</h1>
{{ program }}
{% endblock %}

View File

@ -8,4 +8,5 @@ urlpatterns = [
url(r'^room/add/$', views.RoomCreate.as_view(), name='add-room'),
url(r'^room/(?P<slug>[-\w]+)/$', views.RoomDetail.as_view(), name='show-room'),
url(r'^room/(?P<slug>[-\w]+)/edit/$', views.RoomUpdate.as_view(), name='edit-room'),
url(r'^program/$', views.program, name='program'),
]

110
planning/utils.py Normal file
View File

@ -0,0 +1,110 @@
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.timezone import localtime
from datetime import datetime, timedelta
from copy import deepcopy
from collections import OrderedDict, namedtuple
from itertools import islice
from .models import Room
from proposals.models import Talk
Event = namedtuple('Event', ['talk', 'row', 'rowcount'])
class Program:
def __init__(self, site):
self.rooms = Room.objects.filter(site=site)
self.talks = Talk.objects.\
filter(site=site, room__in=self.rooms.all(), start_date__isnull=False).\
filter(Q(duration__gt=0) | Q(event__duration__gt=0)).\
exclude(accepted=False).\
order_by('start_date')
self.timeslots = []
for talk in self.talks.all():
duration = talk.estimated_duration()
assert(duration)
d1 = talk.start_date
d2 = d1 + timedelta(minutes=duration)
if d1 not in self.timeslots:
self.timeslots.append(d1)
if d2 not in self.timeslots:
self.timeslots.append(d2)
self.timeslots = sorted(self.timeslots)
self.cols = OrderedDict([(room, 1) for room in self.rooms])
self.rows = OrderedDict([(timeslot, OrderedDict([(room, []) for room in self.rooms])) for timeslot in self.timeslots[:-1]])
for talk in self.talks:
self._add_talk(talk)
def _add_talk(self, talk):
room = talk.room
d1 = self.timeslots.index(talk.start_date)
d2 = self.timeslots.index(talk.start_date + timedelta(minutes=talk.duration))
col = None
for row, timeslot in enumerate(islice(self.timeslots, d1, d2)):
if col is None:
col = 0
while col < len(self.rows[timeslot][room]) and self.rows[timeslot][room][col]:
col += 1
self.cols[room] = max(self.cols[room], col+1)
event = Event(talk=talk, row=row, rowcount=d2-d1)
while len(self.rows[timeslot][room]) <= col:
self.rows[timeslot][room].append(None)
self.rows[timeslot][room][col] = event
def _header(self):
output = '<td>Room</td>'
room_cell = '<td%(options)s>%(name)s<br><b>%(label)s</b></td>'
for room, colspan in self.cols.items():
options = ' colspan="%d"' % colspan
output += room_cell % {'name': room.name, 'label': room.label, 'options': options}
return '<tr>%s</tr>' % output
def _body(self):
row = '<tr style="%(style)s">%(timeslot)s%(content)s</tr>'
cell = '<td%(options)s>%(content)s</td>'
output = []
for ts, rooms in self.rows.items():
content = ''
for room, events in rooms.items():
for i in range(self.cols[room]):
options = ''
cellcontent = ''
if i < len(events) and events[i]:
event = events[i]
if event.row != 0:
continue
options = ' rowspan="%d" bgcolor="%s"' % (event.rowcount, event.talk.event.color)
cellcontent = str(event.talk) + '' + event.talk.get_speakers_str()
content += cell % {'options': options, 'content': cellcontent}
style, timeslot = self._timeslot(ts)
output.append(row % {
'style': style,
'timeslot': timeslot,
'content': content,
})
return '\n'.join(output)
def _timeslot(self, ts):
template = '<td>%(content)s</td>'
start = ts
end = self.timeslots[self.timeslots.index(ts)+1]
duration = (end - start).seconds / 60
print(start, end, duration)
date_to_string = lambda date: datetime.strftime(localtime(date), '%H:%M')
style = 'height: %dpx;' % int(duration * 0.8)
timeslot = '<td>%s %s</td>' % tuple(map(date_to_string, [start, end]))
return style, timeslot
def __str__(self):
template = """<table class="table table-bordered text-center">\n%(header)s\n%(body)s\n</table>"""
return mark_safe(template % {
'header': self._header(),
'body': self._body(),
})

View File

@ -5,9 +5,11 @@ from django.views.generic import CreateView, DetailView, ListView, UpdateView
from accounts.mixins import OrgaRequiredMixin, StaffRequiredMixin
from proposals.mixins import OnSiteFormMixin
from proposals.models import Talk
from .models import Room
from .forms import RoomForm
from .utils import Program
class RoomMixin(object):
def get_queryset(self):
@ -30,3 +32,10 @@ class RoomUpdate(OrgaRequiredMixin, RoomMixin, RoomFormMixin, UpdateView):
class RoomDetail(StaffRequiredMixin, RoomMixin, DetailView):
pass
def program(request):
program = Program(site=get_current_site(request))
return render(request, 'planning/program.html', {
'program': program,
})

View File

@ -71,6 +71,9 @@
<li role="presentation">
<a role="menuitem" tabindex="-1" href="{% url 'list-rooms' %}"><span class="glyphicon glyphicon-tent"></span>&nbsp;{% trans "Rooms" %}</a>
</li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="{% url 'program' %}"><span class="glyphicon glyphicon-list-alt"></span>&nbsp;{% trans "Program" %}</a>
</li>
</ul>
</li>
<li class="dropdown{% block admintab %}{% endblock %}">

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-10-16 21:21
from __future__ import unicode_literals
import colorful.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('proposals', '0020_auto_20161015_1201'),
]
operations = [
migrations.AddField(
model_name='event',
name='color',
field=colorful.fields.RGBColorField(default='#ffffff', verbose_name='Color on program'),
),
migrations.AlterField(
model_name='event',
name='duration',
field=models.IntegerField(default=0, verbose_name='Default duration (min)'),
),
]

View File

@ -10,6 +10,7 @@ from django.utils.translation import ugettext
from django.utils import timezone
from autoslug import AutoSlugField
from colorful.fields import RGBColorField
from accounts.models import Participation
from ponyconf.utils import PonyConfModel, enum_to_choices
@ -85,7 +86,8 @@ class Event(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=64)
duration = models.IntegerField(default=0, verbose_name=_('Duration (min)'))
duration = models.IntegerField(default=0, verbose_name=_('Default duration (min)'))
color = RGBColorField(default='#ffffff', verbose_name=_("Color on program"))
class Meta:
unique_together = ('site', 'name')
@ -123,6 +125,13 @@ class Talk(PonyConfModel):
def __str__(self):
return self.title
def get_speakers_str(self):
speakers = [str(speaker) for speaker in self.speakers.all()]
if len(speakers) == 1:
return speakers[0]
else:
return ', '.join(speakers[:-1]) + ' & ' + str(speakers[-1])
def estimated_duration(self):
return self.duration or self.event.duration

View File

@ -6,6 +6,7 @@ django-bower
django-registration-redux
django-select2
django-avatar
django-colorful
markdown
bleach