talks can be tagged
This commit is contained in:
parent
5e276d9c1a
commit
4e7ae4eca9
14
cfp/admin.py
14
cfp/admin.py
|
@ -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)
|
||||||
|
|
14
cfp/forms.py
14
cfp/forms.py
|
@ -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):
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue