From 4e7ae4eca9d20a622590e8a5b7ec31c16ea619a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Bouttier?= Date: Mon, 9 Oct 2017 19:48:33 +0200 Subject: [PATCH] talks can be tagged --- cfp/admin.py | 14 ++++---- cfp/forms.py | 14 ++++++-- cfp/migrations/0013_tags.py | 33 +++++++++++++++++++ cfp/models.py | 39 +++++++++++++++++++++++ cfp/templates/cfp/staff/talk_details.html | 5 ++- cfp/templates/cfp/staff/talk_list.html | 3 ++ cfp/views.py | 5 ++- 7 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 cfp/migrations/0013_tags.py diff --git a/cfp/admin.py b/cfp/admin.py index adadfab..73c512c 100644 --- a/cfp/admin.py +++ b/cfp/admin.py @@ -2,7 +2,8 @@ from django.contrib import admin from django.contrib.sites.models import Site 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): @@ -41,11 +42,7 @@ class VoteAdmin(admin.ModelAdmin): return super().get_queryset(request).filter(talk__site=request.conference.site) -class VolunteerAdmin(OnSiteAdminMixin, admin.ModelAdmin): - pass - - -class ActivityAdmin(OnSiteAdminMixin, admin.ModelAdmin): +class OnSiteModelAdmin(OnSiteAdminMixin, admin.ModelAdmin): pass @@ -54,5 +51,6 @@ admin.site.register(Participant, ParticipantAdmin) admin.site.register(Talk, TalkAdmin) admin.site.register(TalkCategory, TalkCategoryAdmin) admin.site.register(Vote, VoteAdmin) -admin.site.register(Volunteer, VolunteerAdmin) -admin.site.register(Activity, ActivityAdmin) +admin.site.register(Tag, OnSiteModelAdmin) +admin.site.register(Volunteer, OnSiteModelAdmin) +admin.site.register(Activity, OnSiteModelAdmin) diff --git a/cfp/forms.py b/cfp/forms.py index 2c8aafe..9c57117 100644 --- a/cfp/forms.py +++ b/cfp/forms.py @@ -9,7 +9,7 @@ from django.utils.crypto import get_random_string 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 = [ @@ -61,7 +61,10 @@ class TalkStaffForm(forms.ModelForm): self.fields['duration'].help_text = _('Default duration: %(duration)d min') % {'duration': self.instance.duration} 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 = { 'category': _('Category'), 'title': _('Title'), @@ -98,6 +101,12 @@ class TalkFilterForm(forms.Form): widget=forms.CheckboxSelectMultiple, choices=[], ) + tag = forms.MultipleChoiceField( + label=_('Tag'), + required=False, + widget=forms.CheckboxSelectMultiple, + choices=[], + ) vote = forms.NullBooleanField( label=_('Vote'), 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') tracks = Track.objects.filter(site=site) 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): diff --git a/cfp/migrations/0013_tags.py b/cfp/migrations/0013_tags.py new file mode 100644 index 0000000..084511b --- /dev/null +++ b/cfp/migrations/0013_tags.py @@ -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'), + ), + ] diff --git a/cfp/models.py b/cfp/models.py index 09863b3..948adcb 100644 --- a/cfp/models.py +++ b/cfp/models.py @@ -8,6 +8,8 @@ from django.db.models import Q, Count, Avg, Case, When from django.db.models.functions import Coalesce from django.utils import timezone 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 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() +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('{content}', **{ + 'url': reverse('talk-list'), + 'tag': self.slug, + 'content': self.label, + }) + + @property + def label(self): + return format_html('{name}', **{ + '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, …) site = models.ForeignKey(Site, on_delete=models.CASCADE) name = models.CharField(max_length=64) @@ -266,6 +301,7 @@ class Talk(PonyConfModel): description = models.TextField(verbose_name=_('Description of your talk'), help_text=_('This field is only visible by organizers.')) 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'), 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' @@ -330,6 +366,9 @@ class Talk(PonyConfModel): else: return 'warning' + def get_tags_html(self): + return mark_safe(' '.join(map(lambda tag: tag.link, self.tags.all()))) + @property def estimated_duration(self): return self.duration or self.category.duration diff --git a/cfp/templates/cfp/staff/talk_details.html b/cfp/templates/cfp/staff/talk_details.html index ee50641..8536498 100644 --- a/cfp/templates/cfp/staff/talk_details.html +++ b/cfp/templates/cfp/staff/talk_details.html @@ -22,9 +22,12 @@
{% if talk.track %} {{ talk.track }} {% else %} - {% trans "No assigned yet." context "session" %} + {% trans "not defined" context "session" %} {% endif %}
+
{% trans "Tags" %}
+
{% if talk.tags.exist %}{{ talk.get_tags_html }}{% else %}{% trans "none" context "tag" %}{% endif %}
+
{% trans "Timeslot" %}
{% if talk.start_date %} {{ talk.start_date|date:"l d b" }}, diff --git a/cfp/templates/cfp/staff/talk_list.html b/cfp/templates/cfp/staff/talk_list.html index e8f9355..9d1b26e 100644 --- a/cfp/templates/cfp/staff/talk_list.html +++ b/cfp/templates/cfp/staff/talk_list.html @@ -24,6 +24,7 @@ {% bootstrap_field filter_form.video layout="horizontal" %}
+ {% bootstrap_field filter_form.tag layout="horizontal" %} {% bootstrap_field filter_form.track layout="horizontal" %}
@@ -43,6 +44,7 @@ {% trans "Intervention kind" %} {% trans "Speakers" %} {% trans "Track" %} + {% trans "Tags" %} {% trans "Status" %} @@ -62,6 +64,7 @@ {% endfor %} {{ talk.track|default:"–" }} + {{ talk.get_tags_html }} {{ talk.get_status_str }} diff --git a/cfp/views.py b/cfp/views.py index 7124b95..31d3a42 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -247,6 +247,9 @@ def talk_list(request): if data['scheduled'] != None: show_filters = True 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']): show_filters = True q = Q() @@ -323,7 +326,7 @@ def talk_list(request): glyphicon = 'sort' sort_urls[c] = url.urlencode() 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', { 'show_filters': show_filters, 'talk_list': talks,