Commit 4ad923ff authored by Skia's avatar Skia 🤘
Browse files

Merge branch 'mailing' into 'master'

Mailing list system

See merge request !96
parents df42617c 4f6109e2
Pipeline #1159 passed with stage
in 5 minutes and 23 seconds
......@@ -48,5 +48,6 @@ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^login/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^markdown$', RenderMarkdown, name='api_markdown'),
url(r'^mailings$', FetchMailingLists, name='mailings_fetch')
]
......@@ -22,9 +22,15 @@
#
#
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import StaticHTMLRenderer
from club.models import Club
from django.conf import settings
from django.core.exceptions import PermissionDenied
from club.models import Club, Mailing
from api.views import RightModelViewSet
......@@ -43,3 +49,15 @@ class ClubViewSet(RightModelViewSet):
serializer_class = ClubSerializer
queryset = Club.objects.all()
@api_view(['GET'])
@renderer_classes((StaticHTMLRenderer,))
def FetchMailingLists(request):
key = request.GET.get('key', '')
if key != settings.SITH_MAILING_FETCH_KEY:
raise PermissionDenied
data = ''
for mailing in Mailing.objects.filter(is_moderated=True).all():
data += mailing.fetch_format()
return Response(data)
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
import re
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('club', '0008_auto_20170515_2214'),
]
operations = [
migrations.CreateModel(
name='Mailing',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)),
('email', models.CharField(max_length=256, unique=True, validators=[django.core.validators.RegexValidator(re.compile('(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*\\Z|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"\\Z)', 34), 'Enter a valid address. Only the root of the address is needed.')], verbose_name='Email address')),
('is_moderated', models.BooleanField(default=False, verbose_name='is moderated')),
('club', models.ForeignKey(verbose_name='Club', related_name='mailings', to='club.Club')),
('moderator', models.ForeignKey(null=True, verbose_name='moderator', related_name='moderated_mailings', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='MailingSubscription',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)),
('email', models.EmailField(max_length=254, verbose_name='Email address')),
('mailing', models.ForeignKey(verbose_name='Mailing', related_name='subscriptions', to='club.Mailing')),
('user', models.ForeignKey(null=True, verbose_name='User', related_name='mailing_subscriptions', blank=True, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='mailingsubscription',
unique_together=set([('user', 'email', 'mailing')]),
),
]
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -26,12 +27,13 @@ from django.db import models
from django.core import validators
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db import transaction
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.core.validators import RegexValidator, validate_email
from core.models import User, MetaGroup, Group, SithFile
from core.models import User, MetaGroup, Group, SithFile, RealGroup, Notification
# Create your models here.
......@@ -220,3 +222,99 @@ class Membership(models.Model):
def get_absolute_url(self):
return reverse('club:club_members', kwargs={'club_id': self.club.id})
class Mailing(models.Model):
"""
This class correspond to a mailing list
Remember that mailing lists should be validated by UTBM
"""
club = models.ForeignKey(Club, verbose_name=_('Club'), related_name="mailings", null=False, blank=False)
email = models.CharField(_('Email address'), unique=True, null=False, blank=False, max_length=256,
validators=[
RegexValidator(validate_email.user_regex,
_('Enter a valid address. Only the root of the address is needed.'))
])
is_moderated = models.BooleanField(_('is moderated'), default=False)
moderator = models.ForeignKey(User, related_name="moderated_mailings", verbose_name=_("moderator"), null=True)
def clean(self):
if self.can_moderate(self.moderator):
self.is_moderated = True
else:
self.moderator = None
super(Mailing, self).clean()
@property
def email_full(self):
return self.email + '@' + settings.SITH_MAILING_DOMAIN
def can_moderate(self, user):
return user.is_root or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
def is_owned_by(self, user):
return user.is_in_group(self) or user.is_root or user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
def can_view(self, user):
return self.club.has_rights_in_club(user)
def delete(self):
for sub in self.subscriptions.all():
sub.delete()
super(Mailing, self).delete()
def fetch_format(self):
resp = self.email + ': '
for sub in self.subscriptions.all():
resp += sub.fetch_format()
return resp
def save(self):
if not self.is_moderated:
for user in RealGroup.objects.filter(id=settings.SITH_GROUP_COM_ADMIN_ID).first().users.all():
if not user.notifications.filter(type="MAILING_MODERATION", viewed=False).exists():
Notification(user=user, url=reverse('com:mailing_admin'), type="MAILING_MODERATION").save()
super(Mailing, self).save()
def __str__(self):
return "%s - %s" % (self.club, self.email_full)
class MailingSubscription(models.Model):
"""
This class makes the link between user and mailing list
"""
mailing = models.ForeignKey(Mailing, verbose_name=_('Mailing'), related_name="subscriptions", null=False, blank=False)
user = models.ForeignKey(User, verbose_name=_('User'), related_name="mailing_subscriptions", null=True, blank=True)
email = models.EmailField(_('Email address'), blank=False, null=False)
class Meta:
unique_together = (('user', 'email', 'mailing'),)
def clean(self):
if not self.user and not self.email:
raise ValidationError(_("At least user or email is required"))
try:
if self.user and not self.email:
self.email = self.user.email
if MailingSubscription.objects.filter(mailing=self.mailing, email=self.email).exists():
raise ValidationError(_("This email is already suscribed in this mailing"))
except ObjectDoesNotExist:
pass
super(MailingSubscription, self).clean()
def is_owned_by(self, user):
return self.mailing.club.has_rights_in_club(user) or user.is_root or self.user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
def can_be_edited_by(self, user):
return (user is not None and user.id == self.user.id)
def fetch_format(self):
return self.email + ' '
def __str__(self):
if self.user:
user = str(self.user)
else:
user = _("Unregistered user")
return "(%s) - %s : %s" % (self.mailing, user, self.email)
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Mailing lists{% endtrans %}
{% endblock %}
{% block content %}
{% if has_objects %}
<b>{% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}</b>
{% for mailing in object_list %}
{% if mailing.is_moderated %}
<h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }}
{%- if user.is_owner(mailing) -%}
<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a>
{%- endif -%}
</h2>
<hr>
<table>
<tr>
<th>{% trans %}User{% endtrans %}</th>
<th colspan="2">{% trans %}Email{%endtrans%}</th>
</tr>
{% for subscriber in mailing.subscriptions.all() %}
<tr>
{% if subscriber.user %}
<td>{{ subscriber.user }}</td>
{% else %}
<td>{% trans %}Unregistered user{% endtrans %}</td>
{% endif %}
<td>{{ subscriber.email }}</td>
<td><a href="{{ url('club:mailing_subscription_delete', mailing_subscription_id=subscriber.id) }}">{% trans %}Delete{% endtrans %}</a></td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endfor %}
{% else %}
<p>{% trans %}No mailing list existing for this club{% endtrans %}</p>
{% endif %}
{% if has_objects %}
<h2>{% trans %}New member{% endtrans %}</h2>
<form action="{{ url('club:mailing_subscription_create', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ add_member.as_p() }}
<p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p>
</form>
{% endif %}
<h2>{% trans %}New mailing{% endtrans %}</h2>
<form action="{{ url('club:mailing_create', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ add_mailing.as_p() }}
<p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p>
</form>
{% endblock %}
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -38,5 +39,10 @@ urlpatterns = [
url(r'^(?P<club_id>[0-9]+)/sellings/csv$', ClubSellingCSVView.as_view(), name='sellings_csv'),
url(r'^(?P<club_id>[0-9]+)/prop$', ClubEditPropView.as_view(), name='club_prop'),
url(r'^(?P<club_id>[0-9]+)/tools$', ClubToolsView.as_view(), name='tools'),
url(r'^(?P<club_id>[0-9]+)/mailing$', ClubMailingView.as_view(action="display"), name='mailing'),
url(r'^(?P<club_id>[0-9]+)/mailing/new/mailing$', ClubMailingView.as_view(action="add_mailing"), name='mailing_create'),
url(r'^(?P<club_id>[0-9]+)/mailing/new/subscription$', ClubMailingView.as_view(action="add_member"), name='mailing_subscription_create'),
url(r'^(?P<mailing_id>[0-9]+)/mailing/delete$', MailingDeleteView.as_view(), name='mailing_delete'),
url(r'^(?P<mailing_subscription_id>[0-9]+)/mailing/delete/subscription$', MailingSubscriptionDeleteView.as_view(), name='mailing_subscription_delete'),
url(r'^membership/(?P<membership_id>[0-9]+)/set_old$', MembershipSetOldView.as_view(), name='membership_set_old'),
]
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -24,19 +25,63 @@
from django import forms
from django.views.generic import ListView, DetailView, TemplateView
from django.views.generic.edit import DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView, CreateView
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _t
from ajax_select.fields import AutoCompleteSelectField
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, TabedViewMixin
from core.views.forms import SelectDate, SelectDateTime
from club.models import Club, Membership
from club.models import Club, Membership, Mailing, MailingSubscription
from sith.settings import SITH_MAXIMUM_FREE_ROLE
from counter.models import Selling, Counter
from core.models import User
from django.conf import settings
# Custom forms
class MailingForm(forms.ModelForm):
class Meta:
model = Mailing
fields = ('email', 'club', 'moderator')
def __init__(self, *args, **kwargs):
club_id = kwargs.pop('club_id', None)
user_id = kwargs.pop('user_id', -1) # Remember 0 is treated as None
super(MailingForm, self).__init__(*args, **kwargs)
if club_id:
self.fields['club'].queryset = Club.objects.filter(id=club_id)
self.fields['club'].initial = club_id
self.fields['club'].widget = forms.HiddenInput()
if user_id >= 0:
self.fields['moderator'].queryset = User.objects.filter(id=user_id)
self.fields['moderator'].initial = user_id
self.fields['moderator'].widget = forms.HiddenInput()
class MailingSubscriptionForm(forms.ModelForm):
class Meta:
model = MailingSubscription
fields = ('mailing', 'user', 'email')
def __init__(self, *args, **kwargs):
kwargs.pop('user_id', None) # For standart interface
club_id = kwargs.pop('club_id', None)
super(MailingSubscriptionForm, self).__init__(*args, **kwargs)
self.fields['email'].required = False
if club_id:
self.fields['mailing'].queryset = Mailing.objects.filter(club__id=club_id, is_moderated=True)
user = AutoCompleteSelectField('users', label=_('User'), help_text=None, required=False)
class ClubTabsMixin(TabedViewMixin):
......@@ -77,6 +122,11 @@ class ClubTabsMixin(TabedViewMixin):
'slug': 'sellings',
'name': _("Sellings"),
})
tab_list.append({
'url': reverse('club:mailing', kwargs={'club_id': self.object.id}),
'slug': 'mailing',
'name': _("Mailing list"),
})
if self.request.user.is_owner(self.object):
tab_list.append({
'url': reverse('club:club_prop', kwargs={'club_id': self.object.id}),
......@@ -338,3 +388,112 @@ class ClubStatView(TemplateView):
kwargs = super(ClubStatView, self).get_context_data(**kwargs)
kwargs['club_list'] = Club.objects.all()
return kwargs
class ClubMailingView(ClubTabsMixin, ListView):
"""
A list of mailing for a given club
"""
action = None
model = Mailing
template_name = "club/mailing.jinja"
current_tab = 'mailing'
def authorized(self):
return self.club.has_rights_in_club(self.user) or self.user.is_root or self.user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID)
def dispatch(self, request, *args, **kwargs):
self.club = get_object_or_404(Club, pk=kwargs['club_id'])
self.user = request.user
self.object = self.club
if not self.authorized():
raise PermissionDenied
self.member_form = MailingSubscriptionForm(club_id=self.club.id)
self.mailing_form = MailingForm(club_id=self.club.id, user_id=self.user.id)
return super(ClubMailingView, self).dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
res = super(ClubMailingView, self).get(request, *args, **kwargs)
if self.action != "display":
if self.action == "add_mailing":
form = MailingForm
model = Mailing
elif self.action == "add_member":
form = MailingSubscriptionForm
model = MailingSubscription
return MailingGenericCreateView.as_view(model=model, list_view=self, form_class=form)(request, *args, **kwargs)
return res
def get_queryset(self):
return Mailing.objects.filter(club_id=self.club.id).all()
def get_context_data(self, **kwargs):
kwargs = super(ClubMailingView, self).get_context_data(**kwargs)
kwargs['add_member'] = self.member_form
kwargs['add_mailing'] = self.mailing_form
kwargs['club'] = self.club
kwargs['user'] = self.user
kwargs['has_objects'] = len(kwargs['object_list']) > 0
return kwargs
class MailingGenericCreateView(CreateView, SingleObjectMixin):
"""
Create a new mailing list
"""
list_view = None
form_class = None
def get_context_data(self, **kwargs):
view_kwargs = self.list_view.get_context_data(**kwargs)
for key, data in super(MailingGenericCreateView, self).get_context_data(**kwargs).items():
view_kwargs[key] = data
view_kwargs[self.list_view.action] = view_kwargs['form']
return view_kwargs
def get_form_kwargs(self):
kwargs = super(MailingGenericCreateView, self).get_form_kwargs()
kwargs['club_id'] = self.list_view.club.id
kwargs['user_id'] = self.list_view.user.id
return kwargs
def dispatch(self, request, *args, **kwargs):
if not self.list_view.authorized():
raise PermissionDenied
self.template_name = self.list_view.template_name
return super(MailingGenericCreateView, self).dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse_lazy('club:mailing', kwargs={'club_id': self.list_view.club.id})
class MailingDeleteView(CanEditMixin, DeleteView):
model = Mailing
template_name = 'core/delete_confirm.jinja'
pk_url_kwarg = "mailing_id"
redirect_page = None
def dispatch(self, request, *args, **kwargs):
self.club_id = self.get_object().club.id
return super(MailingDeleteView, self).dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs):
if self.redirect_page:
return reverse_lazy(self.redirect_page)
else:
return reverse_lazy('club:mailing', kwargs={'club_id': self.club_id})
class MailingSubscriptionDeleteView(CanEditMixin, DeleteView):
model = MailingSubscription
template_name = 'core/delete_confirm.jinja'
pk_url_kwarg = "mailing_subscription_id"
def dispatch(self, request, *args, **kwargs):
self.club_id = self.get_object().mailing.club.id
return super(MailingSubscriptionDeleteView, self).dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse_lazy('club:mailing', kwargs={'club_id': self.club_id})
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Mailing lists administration{% endtrans %}
{% endblock %}
{% macro display_mailings(list) %}
<table>
<tr>
<th>{% trans %}Email{% endtrans %}</th>
<th>{% trans %}Club{%endtrans%}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</tr>
{% for mailing in list %}
<tr>
<td>{{ mailing.email_full }}</td>
<td><a href="{{ url('club:mailing', club_id=mailing.club.id) }}">{{ mailing.club }}</a></td>
<td>
<a href="{{ url('com:mailing_delete', mailing_id=mailing.id) }}">{% trans %}Delete{% endtrans %}</a> - {% if not mailing.is_moderated %}<a href="{{ url('com:mailing_moderate', mailing_id=mailing.id) }}">{% trans %}Moderate{% endtrans %}</a>{% else %}{% trans user=mailing.moderator %}Moderated by {{ user }}{% endtrans %}{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endmacro %}
{% block content %}
<h1>{% trans %}This page lists all mailing lists{% endtrans %}</h1>
{% if has_unmoderated %}
<h2>{% trans %}Not moderated mailing lists{% endtrans %}</h2>
{{ display_mailings(unmoderated) }}
{% endif %}
<h2>{% trans %}Moderated mailing lists{% endtrans %}</h2>
{% if has_moderated %}
{{ display_mailings(moderated) }}
{% else %}
<p>{% trans %}No mailing list existing{% endtrans %}</p>
{% endif %}
{% endblock %}
......@@ -25,6 +25,7 @@
from django.conf.urls import url
from com.views import *
from club.views import MailingDeleteView
urlpatterns = [
url(r'^sith/edit/alert$', AlertMsgEditView.as_view(), name='alert_edit'),
......@@ -42,5 +43,8 @@ urlpatterns = [
url(r'^news/(?P<news_id>[0-9]+)/moderate$', NewsModerateView.as_view(), name='news_moderate'),
url(r'^news/(?P<news_id>[0-9]+)/edit$', NewsEditView.as_view(), name='news_edit'),
url(r'^news/(?P<news_id>[0-9]+)$', NewsDetailView.as_view(), name='news_detail'),
url(r'^mailings$', MailingListAdminView.as_view(), name='mailing_admin'),
url(r'^mailings/(?P<mailing_id>[0-9]+)/moderate$', MailingModerateView.as_view(), name='mailing_moderate'),
url(r'^mailings/(?P<mailing_id>[0-9]+)/delete$', MailingDeleteView.as_view(redirect_page='com:mailing_admin'), name='mailing_delete'),
]
......@@ -2,6 +2,7 @@
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
......@@ -24,7 +25,7 @@
from django.shortcuts import redirect, get_object_or_404
from django.http import HttpResponseRedirect
from django.views.generic import ListView, DetailView
from django.views.generic import ListView, DetailView, View
from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
......@@ -34,6 +35,7 @@ from django.utils import timezone
from django.conf import settings
from django.db.models import Max
from django.forms.models import modelform_factory
from django.core.exceptions import PermissionDenied
from django import forms
from datetime import timedelta
......@@ -42,7 +44,7 @@ from com.models import Sith, News, NewsDate, Weekmail, WeekmailArticle
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, TabedViewMixin, CanCreateMixin, QuickNotifMixin
from core.views.forms import SelectDateTime
from core.models import Notification, RealGroup
from club.models import Club
from club.models import Club, Mailing
# Sith object
......@@ -81,6 +83,11 @@ class ComTabsMixin(TabedViewMixin):
'slug': 'alert',
'name': _("Alert message"),
})
tab_list.append({
'url': reverse('com:mailing_admin'),
'slug': 'mailings',
'name': _("Mailing lists administration"),
})
return tab_list
......@@ -414,3 +421,35 @@ class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView):
template_name = 'core/delete_confirm.jinja'
success_url = reverse_lazy('com:weekmail')
pk_url_kwarg = "article_id"
class MailingListAdminView(ComTabsMixin, ListView):
template_name = "com/mailing_admin.jinja"
model = Mailing