talks can be tagged

This commit is contained in:
Élie Bouttier 2017-10-09 19:48:33 +02:00
parent 5e276d9c1a
commit 4e7ae4eca9
7 changed files with 101 additions and 12 deletions

View File

@ -2,7 +2,8 @@ from django.contrib import admin
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from .mixins import OnSiteAdminMixin from .mixins import OnSiteAdminMixin
from .models import Conference, Participant, Talk, TalkCategory, Track, Vote, Volunteer, Activity from .models import Conference, Participant, Talk, TalkCategory, Track, \
Vote, Volunteer, Activity, Tag
class ConferenceAdmin(OnSiteAdminMixin, admin.ModelAdmin): class ConferenceAdmin(OnSiteAdminMixin, admin.ModelAdmin):
@ -41,11 +42,7 @@ class VoteAdmin(admin.ModelAdmin):
return super().get_queryset(request).filter(talk__site=request.conference.site) return super().get_queryset(request).filter(talk__site=request.conference.site)
class VolunteerAdmin(OnSiteAdminMixin, admin.ModelAdmin): class OnSiteModelAdmin(OnSiteAdminMixin, admin.ModelAdmin):
pass
class ActivityAdmin(OnSiteAdminMixin, admin.ModelAdmin):
pass pass
@ -54,5 +51,6 @@ admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Talk, TalkAdmin) admin.site.register(Talk, TalkAdmin)
admin.site.register(TalkCategory, TalkCategoryAdmin) admin.site.register(TalkCategory, TalkCategoryAdmin)
admin.site.register(Vote, VoteAdmin) admin.site.register(Vote, VoteAdmin)
admin.site.register(Volunteer, VolunteerAdmin) admin.site.register(Tag, OnSiteModelAdmin)
admin.site.register(Activity, ActivityAdmin) admin.site.register(Volunteer, OnSiteModelAdmin)
admin.site.register(Activity, OnSiteModelAdmin)

View File

@ -9,7 +9,7 @@ from django.utils.crypto import get_random_string
from django_select2.forms import ModelSelect2MultipleWidget from django_select2.forms import ModelSelect2MultipleWidget
from .models import Participant, Talk, TalkCategory, Track, Conference, Room, Volunteer from .models import Participant, Talk, TalkCategory, Track, Tag, Conference, Room, Volunteer
ACCEPTATION_CHOICES = [ ACCEPTATION_CHOICES = [
@ -61,7 +61,10 @@ class TalkStaffForm(forms.ModelForm):
self.fields['duration'].help_text = _('Default duration: %(duration)d min') % {'duration': self.instance.duration} self.fields['duration'].help_text = _('Default duration: %(duration)d min') % {'duration': self.instance.duration}
class Meta(TalkForm.Meta): class Meta(TalkForm.Meta):
fields = ('category', 'track', 'title', 'description', 'notes', 'start_date', 'duration', 'room', 'materials', 'video',) fields = ('category', 'track', 'title', 'description', 'notes', 'tags', 'start_date', 'duration', 'room', 'materials', 'video',)
widgets = {
'tags': forms.CheckboxSelectMultiple,
}
labels = { labels = {
'category': _('Category'), 'category': _('Category'),
'title': _('Title'), 'title': _('Title'),
@ -98,6 +101,12 @@ class TalkFilterForm(forms.Form):
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
choices=[], choices=[],
) )
tag = forms.MultipleChoiceField(
label=_('Tag'),
required=False,
widget=forms.CheckboxSelectMultiple,
choices=[],
)
vote = forms.NullBooleanField( vote = forms.NullBooleanField(
label=_('Vote'), label=_('Vote'),
help_text=_('Filter talks you already / not yet voted for'), help_text=_('Filter talks you already / not yet voted for'),
@ -126,6 +135,7 @@ class TalkFilterForm(forms.Form):
self.fields['category'].choices = categories.values_list('pk', 'name') self.fields['category'].choices = categories.values_list('pk', 'name')
tracks = Track.objects.filter(site=site) tracks = Track.objects.filter(site=site)
self.fields['track'].choices = [('none', _('Not assigned'))] + list(tracks.values_list('slug', 'name')) self.fields['track'].choices = [('none', _('Not assigned'))] + list(tracks.values_list('slug', 'name'))
self.fields['tag'].choices = Tag.objects.filter(site=site).values_list('slug', 'name')
class TalkActionForm(forms.Form): class TalkActionForm(forms.Form):

View File

@ -0,0 +1,33 @@
from __future__ import unicode_literals
import autoslug.fields
import colorful.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('sites', '0002_alter_domain_unique'),
('cfp', '0012_talk_confirmed'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, verbose_name='Name')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name')),
('color', colorful.fields.RGBColorField(default='#ffffff', verbose_name='Color')),
('inverted', models.BooleanField(default=False)),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
migrations.AddField(
model_name='talk',
name='tags',
field=models.ManyToManyField(to='cfp.Tag'),
),
]

View File

@ -8,6 +8,8 @@ from django.db.models import Q, Count, Avg, Case, When
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
from django.utils.safestring import mark_safe
from django.utils.html import escape, format_html
from autoslug import AutoSlugField from autoslug import AutoSlugField
from colorful.fields import RGBColorField from colorful.fields import RGBColorField
@ -183,6 +185,39 @@ class Room(models.Model):
return self.talks.filter(Q(start_date__isnull=True) | Q(duration=0, category__duration=0)).all() return self.talks.filter(Q(start_date__isnull=True) | Q(duration=0, category__duration=0)).all()
class Tag(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=256, verbose_name=_('Name'))
slug = AutoSlugField(populate_from='name')
color = RGBColorField(default='#ffffff', verbose_name=_("Color"))
inverted = models.BooleanField(default=False)
@property
def link(self):
return format_html('<a href="{url}?tag={tag}">{content}</a>', **{
'url': reverse('talk-list'),
'tag': self.slug,
'content': self.label,
})
@property
def label(self):
return format_html('<span class="label" style="{style}">{name}</span>', **{
'style': self.style,
'name': self.name,
})
@property
def style(self):
return mark_safe('background-color: {bg}; color: {fg}; vertical-align: middle;'.format(**{
'fg': '#fff' if self.inverted else '#000',
'bg': self.color,
}))
def __str__(self):
return self.name
class TalkCategory(models.Model): # type of talk (conf 30min, 1h, stand, …) class TalkCategory(models.Model): # type of talk (conf 30min, 1h, stand, …)
site = models.ForeignKey(Site, on_delete=models.CASCADE) site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
@ -266,6 +301,7 @@ class Talk(PonyConfModel):
description = models.TextField(verbose_name=_('Description of your talk'), description = models.TextField(verbose_name=_('Description of your talk'),
help_text=_('This field is only visible by organizers.')) help_text=_('This field is only visible by organizers.'))
track = models.ForeignKey(Track, blank=True, null=True, verbose_name=_('Track')) track = models.ForeignKey(Track, blank=True, null=True, verbose_name=_('Track'))
tags = models.ManyToManyField(Tag)
notes = models.TextField(blank=True, verbose_name=_('Message to organizers'), notes = models.TextField(blank=True, verbose_name=_('Message to organizers'),
help_text=_('If you have any constraint or if you have anything that may ' help_text=_('If you have any constraint or if you have anything that may '
'help you to select your talk, like a video or slides of your' 'help you to select your talk, like a video or slides of your'
@ -330,6 +366,9 @@ class Talk(PonyConfModel):
else: else:
return 'warning' return 'warning'
def get_tags_html(self):
return mark_safe(' '.join(map(lambda tag: tag.link, self.tags.all())))
@property @property
def estimated_duration(self): def estimated_duration(self):
return self.duration or self.category.duration return self.duration or self.category.duration

View File

@ -22,9 +22,12 @@
<dd>{% if talk.track %} <dd>{% if talk.track %}
<a href="{{ talk.track.get_absolute_url }}">{{ talk.track }}</a> <a href="{{ talk.track.get_absolute_url }}">{{ talk.track }}</a>
{% else %} {% else %}
<em>{% trans "No assigned yet." context "session" %}</em> <em>{% trans "not defined" context "session" %}</em>
{% endif %}</dd> {% endif %}</dd>
<dt>{% trans "Tags" %}</dt>
<dd>{% if talk.tags.exist %}{{ talk.get_tags_html }}{% else %}<em>{% trans "none" context "tag" %}</em>{% endif %}</dd>
<dt>{% trans "Timeslot" %}</dt> <dt>{% trans "Timeslot" %}</dt>
<dd>{% if talk.start_date %} <dd>{% if talk.start_date %}
<span class="date">{{ talk.start_date|date:"l d b" }}</span>, <span class="date">{{ talk.start_date|date:"l d b" }}</span>,

View File

@ -24,6 +24,7 @@
{% bootstrap_field filter_form.video layout="horizontal" %} {% bootstrap_field filter_form.video layout="horizontal" %}
</div> </div>
<div class="col-md-6 col-xs-6"> <div class="col-md-6 col-xs-6">
{% bootstrap_field filter_form.tag layout="horizontal" %}
{% bootstrap_field filter_form.track layout="horizontal" %} {% bootstrap_field filter_form.track layout="horizontal" %}
</div> </div>
</div> </div>
@ -43,6 +44,7 @@
<th class="text-center">{% trans "Intervention kind" %} <a href="?{{ sort_urls.category }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.category }} pull-right"></span></a></th> <th class="text-center">{% trans "Intervention kind" %} <a href="?{{ sort_urls.category }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.category }} pull-right"></span></a></th>
<th class="text-center">{% trans "Speakers" %}</th> <th class="text-center">{% trans "Speakers" %}</th>
<th class="text-center">{% trans "Track" %}</th> <th class="text-center">{% trans "Track" %}</th>
<th class="text-center">{% trans "Tags" %}</th>
<th class="text-center">{% trans "Status" %} <a href="?{{ sort_urls.status }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.status }} pull-right"></span></a></th> <th class="text-center">{% trans "Status" %} <a href="?{{ sort_urls.status }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.status }} pull-right"></span></a></th>
</tr> </tr>
</thead> </thead>
@ -62,6 +64,7 @@
{% endfor %} {% endfor %}
</td> </td>
<td>{{ talk.track|default:"" }}</td> <td>{{ talk.track|default:"" }}</td>
<td>{{ talk.get_tags_html }}</td>
<td> <td>
{{ talk.get_status_str }} {{ talk.get_status_str }}
</td> </td>

View File

@ -247,6 +247,9 @@ def talk_list(request):
if data['scheduled'] != None: if data['scheduled'] != None:
show_filters = True show_filters = True
talks = talks.filter(start_date__isnull=not data['scheduled']) talks = talks.filter(start_date__isnull=not data['scheduled'])
if len(data['tag']):
show_filters = True
talks = talks.filter(tags__slug__in=data['tag'])
if len(data['track']): if len(data['track']):
show_filters = True show_filters = True
q = Q() q = Q()
@ -323,7 +326,7 @@ def talk_list(request):
glyphicon = 'sort' glyphicon = 'sort'
sort_urls[c] = url.urlencode() sort_urls[c] = url.urlencode()
sort_glyphicons[c] = glyphicon sort_glyphicons[c] = glyphicon
talks = talks.prefetch_related('category', 'speakers', 'track') talks = talks.prefetch_related('category', 'speakers', 'track', 'tags')
return render(request, 'cfp/staff/talk_list.html', { return render(request, 'cfp/staff/talk_list.html', {
'show_filters': show_filters, 'show_filters': show_filters,
'talk_list': talks, 'talk_list': talks,