diff --git a/accounts/mixins.py b/accounts/mixins.py
index 89bf1aa..d1a5689 100644
--- a/accounts/mixins.py
+++ b/accounts/mixins.py
@@ -11,3 +11,8 @@ class OrgaRequiredMixin(UserPassesTestMixin):
class StaffRequiredMixin(UserPassesTestMixin):
def test_func(self):
return is_staff(self.request, self.request.user)
+
+
+class SuperuserRequiredMixin(UserPassesTestMixin):
+ def test_func(self):
+ return self.request.user.is_superuser
diff --git a/components b/components
index 35805d8..c74943c 160000
--- a/components
+++ b/components
@@ -1 +1 @@
-Subproject commit 35805d82545cdc2aea0acc6f480f866119a8bb52
+Subproject commit c74943cba752ff6d40a505321ac58bdacbc41857
diff --git a/doc/2016-06-15.md b/doc/2016-06-15.md
index 53ac0bf..d7f7779 100644
--- a/doc/2016-06-15.md
+++ b/doc/2016-06-15.md
@@ -23,3 +23,4 @@
- [x] note sur un speaker
- [ ] ouverture conf
- [ ] mail de bienvenu autre que activation si le mec est inscrit par le staff
+- [ ] do not notify speakers of modified talk
diff --git a/ponyconf/settings.py b/ponyconf/settings.py
index 4a6e2f0..a380a94 100644
--- a/ponyconf/settings.py
+++ b/ponyconf/settings.py
@@ -146,6 +146,7 @@ BOWER_COMPONENTS_ROOT = os.path.join(BASE_DIR, 'components')
BOWER_INSTALLED_APPS = (
'bootstrap',
'jquery',
+ 'jquery-ui',
)
LOGIN_REDIRECT_URL = 'home'
diff --git a/proposals/forms.py b/proposals/forms.py
index d42588f..dfeebf5 100644
--- a/proposals/forms.py
+++ b/proposals/forms.py
@@ -3,12 +3,10 @@ from django.forms.models import modelform_factory
from proposals.models import Talk, Topic
-__all__ = ['TalkForm', 'TopicForm', 'TopicOrgaForm']
+__all__ = ['TalkForm', 'TopicForm']
TalkForm = modelform_factory(Talk, fields=['title', 'description', 'topics', 'event', 'speakers'],
widgets={'topics': CheckboxSelectMultiple()})
TopicForm = modelform_factory(Topic, fields=['name'])
-
-TopicOrgaForm = modelform_factory(Topic, fields=['name', 'reviewers'])
diff --git a/proposals/migrations/0004_auto_20160706_1446.py b/proposals/migrations/0004_auto_20160706_1446.py
new file mode 100644
index 0000000..0ac5c29
--- /dev/null
+++ b/proposals/migrations/0004_auto_20160706_1446.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-07-06 14:46
+from __future__ import unicode_literals
+
+import django.contrib.sites.managers
+from django.db import migrations, models
+import django.db.models.deletion
+import django.db.models.manager
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sites', '0002_alter_domain_unique'),
+ ('proposals', '0003_talk_accepted'),
+ ]
+
+ operations = [
+ migrations.AlterModelManagers(
+ name='topic',
+ managers=[
+ ('objects', django.db.models.manager.Manager()),
+ ('on_site', django.contrib.sites.managers.CurrentSiteManager()),
+ ],
+ ),
+ migrations.AddField(
+ model_name='topic',
+ name='site',
+ field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='sites.Site'),
+ preserve_default=False,
+ ),
+ ]
diff --git a/proposals/models.py b/proposals/models.py
index 160ef5d..5d38801 100644
--- a/proposals/models.py
+++ b/proposals/models.py
@@ -20,16 +20,25 @@ __all__ = ['Topic', 'Talk']
class Topic(PonyConfModel):
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+
name = models.CharField(max_length=128, verbose_name='Name', unique=True)
slug = AutoSlugField(populate_from='name', unique=True)
reviewers = models.ManyToManyField(Participation, blank=True)
+ objects = models.Manager()
+ on_site = CurrentSiteManager()
+
+ @property
+ def talks(self):
+ return Talk.objects.filter(topics=self).all()
+
def __str__(self):
return self.name
def get_absolute_url(self):
- return reverse('list-talks-by-topic', kwargs={'topic': self.slug})
+ return reverse('show-topic', kwargs={'slug': self.slug})
class Talk(PonyConfModel):
diff --git a/proposals/templates/proposals/topic_detail.html b/proposals/templates/proposals/topic_detail.html
new file mode 100644
index 0000000..54be0ad
--- /dev/null
+++ b/proposals/templates/proposals/topic_detail.html
@@ -0,0 +1,59 @@
+{% extends 'base.html' %}
+
+{% load staticfiles accounts_tags %}
+
+{% block topictab %} class="active"{% endblock %}
+
+{% block css %}
+{{ block.super }}
+
+
+{% endblock %}
+
+{% block content %}
+
+
{{ topic }}
+
+{% if request|orga %}
+
+{% endif %}
+
+
+ {% for reviewer in topic.reviewers.all %}
+ -
+ {{ reviewer }}
+ {% if request|orga %} - remove{% endif %}
+
+ {% empty %}
+ - No reviewers.
+ {% endfor %}
+
+
+{% endblock %}
+
+{% block js_end %}
+{{ block.super }}
+
+
+
+
+
+
+{% endblock %}
diff --git a/proposals/templates/proposals/topic_list.html b/proposals/templates/proposals/topic_list.html
index a1494d1..96d1da7 100644
--- a/proposals/templates/proposals/topic_list.html
+++ b/proposals/templates/proposals/topic_list.html
@@ -11,18 +11,21 @@
{% for topic in topic_list %}
-
- {{ topic.get_link }}
+ {{ topic }}:
{% if request|staff %}
- {% if topic.reviewers.exists %} ({% for reviewer in topic.reviewers.all %} {{ reviewer.get_link }} {% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %}
- {% if request|orga %} - edit{% endif %}
+ {{ topic.reviewers.count }} reviewer{{ topic.reviewers.count|pluralize }}
+ and
+ {% comment %}{% if topic.reviewers.exists %} ({% for reviewer in topic.reviewers.all %} {{ reviewer.get_link }} {% if not forloop.last %}, {% endif %}{% endfor %}) {% endif %}{% endcomment %}
{% endif %}
+ {{ topic.talks.count }} talk{{ topic.talks.count|pluralize }}.
+ {% if request.user.is_superuser %} - edit{% endif %}
{% empty %}
- - No topic.
+ - No topics.
{% endfor %}
-{% if request|staff %}
+{% if request|orga %}
Add a topic
{% endif %}
diff --git a/proposals/urls.py b/proposals/urls.py
index 7b46030..dedae1e 100644
--- a/proposals/urls.py
+++ b/proposals/urls.py
@@ -12,7 +12,10 @@ urlpatterns = [
url(r'^talk/by-topic/(?P[-\w]+)$', views.talk_list_by_topic, name='list-talks-by-topic'),
url(r'^topic/$', views.TopicList.as_view(), name='list-topics'),
url(r'^topic/add/$', views.TopicCreate.as_view(), name='add-topic'),
- url(r'^topic/edit/(?P[-\w]+)/$', views.TopicUpdate.as_view(), name='edit-topic'),
+ url(r'^topic/(?P[-\w]+)/$', views.TopicDetail.as_view(), name='show-topic'),
+ url(r'^topic/(?P[-\w]+)/edit/$', views.TopicUpdate.as_view(), name='edit-topic'),
+ url(r'^topic/(?P[-\w]+)/add-reviewer/$', views.topic_add_reviewer, name='add-reviewer'),
+ url(r'^topic/(?P[-\w]+)/remove-reviewer/(?P[\w.@+-]+)/$', views.topic_remove_reviewer, name='remove-reviewer'),
url(r'^speakers/$', views.SpeakerList.as_view(), name='list-speakers'),
url(r'^speaker/(?P[\w.@+-]+)$', views.user_details, name='show-speaker'),
]
diff --git a/proposals/views.py b/proposals/views.py
index 171c897..5a2dddb 100644
--- a/proposals/views.py
+++ b/proposals/views.py
@@ -8,12 +8,14 @@ from django.core.urlresolvers import reverse
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.views.generic import CreateView, DetailView, ListView, UpdateView
+from django.http import JsonResponse
+from django.core.exceptions import ObjectDoesNotExist
-from accounts.mixins import OrgaRequiredMixin, StaffRequiredMixin
+from accounts.mixins import OrgaRequiredMixin, StaffRequiredMixin, SuperuserRequiredMixin
from accounts.models import Participation
from accounts.utils import is_orga
-from .forms import TalkForm, TopicForm, TopicOrgaForm
+from .forms import TalkForm, TopicForm
from .models import Talk, Topic, Vote
from .utils import allowed_talks
from .signals import *
@@ -81,25 +83,81 @@ class TalkDetail(LoginRequiredMixin, DetailView):
return super().get_context_data(**ctx)
-class TopicList(LoginRequiredMixin, ListView):
- model = Topic
-
-
class TopicMixin(object):
model = Topic
-
- def get_form_class(self):
- return TopicOrgaForm if is_orga(self.request, self.request.user) else TopicForm
+ queryset = Topic.on_site.all()
+ form_class = TopicForm
-class TopicCreate(StaffRequiredMixin, TopicMixin, CreateView):
+class TopicList(LoginRequiredMixin, TopicMixin, ListView):
pass
-class TopicUpdate(OrgaRequiredMixin, TopicMixin, UpdateView):
+class TopicCreate(OrgaRequiredMixin, TopicMixin, CreateView):
+ def form_valid(self, form):
+ form.instance.site = get_current_site(self.request)
+ return super(TopicCreate, self).form_valid(form)
+
+
+class TopicUpdate(SuperuserRequiredMixin, TopicMixin, UpdateView):
pass
+class TopicDetail(StaffRequiredMixin, TopicMixin, DetailView):
+ pass
+
+
+@login_required
+def topic_add_reviewer(request, slug):
+ if not Participation.objects.get(user=request.user).is_orga():
+ raise PermissionDenied()
+
+ topic = get_object_or_404(Topic, slug=slug)
+
+ if request.method == 'POST':
+ user = request.POST.get('user')
+ try:
+ user = User.objects.get(username=user)
+ except ObjectDoesNotExist:
+ messages.error(request, 'User not found.')
+ else:
+ participation, created = Participation.on_site.get_or_create(user=user, site=get_current_site(request))
+ if participation in topic.reviewers.all():
+ messages.info(request, 'User is already a reviewer of this topic.')
+ else:
+ topic.reviewers.add(participation)
+ topic.save()
+ messages.success(request, 'User add to reviewer of this topic successfully.')
+ return redirect(topic.get_absolute_url())
+ else:
+ term = request.GET.get('term')
+ if not term:
+ raise Http404()
+ query = Q(username__icontains=term) \
+ | Q(first_name__icontains=term) \
+ | Q(last_name__icontains=term)
+ users = User.objects \
+ .exclude(id__in=topic.reviewers.values('user__id')) \
+ .filter(query)[:10]
+ response = []
+ for user in users:
+ response += [{
+ 'label': str(user.profile),
+ 'value': user.username,
+ }]
+ return JsonResponse(response, safe=False)
+
+
+@login_required
+def topic_remove_reviewer(request, slug, username):
+ if not Participation.objects.get(user=request.user).is_orga():
+ raise PermissionDenied()
+ topic = get_object_or_404(Topic, slug=slug)
+ participation = get_object_or_404(Participation, user__username=username)
+ topic.reviewers.remove(participation)
+ return redirect(topic.get_absolute_url())
+
+
class SpeakerList(StaffRequiredMixin, ListView):
queryset = Participation.on_site.filter(user__talk__in=Talk.on_site.all()).distinct()
template_name = 'proposals/speaker_list.html'