Merge branch 'mailing' into 'master'

Enhance mailing list edition for clubs with brand new form

See merge request ae/Sith!200
This commit is contained in:
Antoine Bartuccio 2019-05-20 17:39:43 +02:00
commit 9f2a0deeb9
9 changed files with 983 additions and 430 deletions

291
club/forms.py Normal file
View File

@ -0,0 +1,291 @@
# -*- coding:utf-8 -*
#
# 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.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.conf import settings
from django import forms
from django.utils.translation import ugettext_lazy as _
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from club.models import Mailing, MailingSubscription, Club, Membership
from core.models import User
from core.views.forms import SelectDate, SelectDateTime
from counter.models import Counter
class ClubEditForm(forms.ModelForm):
class Meta:
model = Club
fields = ["address", "logo", "short_description"]
def __init__(self, *args, **kwargs):
super(ClubEditForm, self).__init__(*args, **kwargs)
self.fields["short_description"].widget = forms.Textarea()
class MailingForm(forms.Form):
"""
Form handling mailing lists right
"""
ACTION_NEW_MAILING = 1
ACTION_NEW_SUBSCRIPTION = 2
ACTION_REMOVE_SUBSCRIPTION = 3
subscription_users = AutoCompleteSelectMultipleField(
"users",
label=_("Users to add"),
help_text=_("Search users to add (one or more)."),
required=False,
)
def __init__(self, club_id, user_id, mailings, *args, **kwargs):
super(MailingForm, self).__init__(*args, **kwargs)
self.fields["action"] = forms.TypedChoiceField(
(
(self.ACTION_NEW_MAILING, _("New Mailing")),
(self.ACTION_NEW_SUBSCRIPTION, _("Subscribe")),
(self.ACTION_REMOVE_SUBSCRIPTION, _("Remove")),
),
coerce=int,
label=_("Action"),
initial=1,
required=True,
widget=forms.HiddenInput(),
)
# Generate bulk removal forms, they are never required
for mailing in mailings:
self.fields["removal_" + str(mailing.id)] = forms.ModelMultipleChoiceField(
mailing.subscriptions.all(),
label=_("Remove"),
required=False,
widget=forms.CheckboxSelectMultiple,
)
# Include fields for handling mailing creation
mailing_fields = ("email",)
self.fields.update(forms.fields_for_model(Mailing, fields=mailing_fields))
for field in mailing_fields:
self.fields["mailing_" + field] = self.fields.pop(field)
self.fields["mailing_" + field].required = False
# Include fields for handling subscription creation
subscription_fields = ("mailing", "email")
self.fields.update(
forms.fields_for_model(MailingSubscription, fields=subscription_fields)
)
for field in subscription_fields:
self.fields["subscription_" + field] = self.fields.pop(field)
self.fields["subscription_" + field].required = False
self.fields["subscription_mailing"].queryset = Mailing.objects.filter(
club__id=club_id, is_moderated=True
)
def check_required(self, cleaned_data, field):
"""
If the given field doesn't exist or has no value, add a required error on it
"""
if not cleaned_data.get(field, None):
self.add_error(field, _("This field is required"))
def clean_subscription_users(self):
"""
Convert given users into real users and check their validity
"""
cleaned_data = super(MailingForm, self).clean()
users = []
for user in cleaned_data["subscription_users"]:
user = User.objects.filter(id=user).first()
if not user:
raise forms.ValidationError(
_("One of the selected users doesn't exist"), code="invalid"
)
if not user.email:
raise forms.ValidationError(
_("One of the selected users doesn't have an email address"),
code="invalid",
)
users.append(user)
return users
def clean(self):
cleaned_data = super(MailingForm, self).clean()
if not "action" in cleaned_data:
# If there is no action provided, we can stop here
raise forms.ValidationError(_("An action is required"), code="invalid")
if cleaned_data["action"] == self.ACTION_NEW_MAILING:
self.check_required(cleaned_data, "mailing_email")
if cleaned_data["action"] == self.ACTION_NEW_SUBSCRIPTION:
self.check_required(cleaned_data, "subscription_mailing")
if not cleaned_data.get(
"subscription_users", None
) and not cleaned_data.get("subscription_email", None):
raise forms.ValidationError(
_("You must specify at least an user or an email address"),
code="invalid",
)
return cleaned_data
class SellingsFormBase(forms.Form):
begin_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
counter = forms.ModelChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
)
class ClubMemberForm(forms.Form):
"""
Form handling the members of a club
"""
error_css_class = "error"
required_css_class = "required"
users = AutoCompleteSelectMultipleField(
"users",
label=_("Users to add"),
help_text=_("Search users to add (one or more)."),
required=False,
)
def __init__(self, *args, **kwargs):
self.club = kwargs.pop("club")
self.request_user = kwargs.pop("request_user")
self.club_members = kwargs.pop("club_members", None)
if not self.club_members:
self.club_members = (
self.club.members.filter(end_date=None).order_by("-role").all()
)
self.request_user_membership = self.club.get_membership_for(self.request_user)
super(ClubMemberForm, self).__init__(*args, **kwargs)
# Using a ModelForm binds too much the form with the model and we don't want that
# We want the view to process the model creation since they are multiple users
# We also want the form to handle bulk deletion
self.fields.update(
forms.fields_for_model(
Membership,
fields=("role", "start_date", "description"),
widgets={"start_date": SelectDate},
)
)
# Role is required only if users is specified
self.fields["role"].required = False
# Start date and description are never really required
self.fields["start_date"].required = False
self.fields["description"].required = False
self.fields["users_old"] = forms.ModelMultipleChoiceField(
User.objects.filter(
id__in=[
ms.user.id
for ms in self.club_members
if ms.can_be_edited_by(
self.request_user, self.request_user_membership
)
]
).all(),
label=_("Mark as old"),
required=False,
widget=forms.CheckboxSelectMultiple,
)
if not self.request_user.is_root:
self.fields.pop("start_date")
def clean_users(self):
"""
Check that the user is not trying to add an user already in the club
Also check that the user is valid and has a valid subscription
"""
cleaned_data = super(ClubMemberForm, self).clean()
users = []
for user_id in cleaned_data["users"]:
user = User.objects.filter(id=user_id).first()
if not user:
raise forms.ValidationError(
_("One of the selected users doesn't exist"), code="invalid"
)
if not user.is_subscribed:
raise forms.ValidationError(
_("User must be subscriber to take part to a club"), code="invalid"
)
if self.club.get_membership_for(user):
raise forms.ValidationError(
_("You can not add the same user twice"), code="invalid"
)
users.append(user)
return users
def clean(self):
"""
Check user rights for adding an user
"""
cleaned_data = super(ClubMemberForm, self).clean()
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
# Drop start_date if allowed to edition but not specified
cleaned_data.pop("start_date")
if not cleaned_data.get("users"):
# No user to add equals no check needed
return cleaned_data
if cleaned_data.get("role", "") == "":
# Role is required if users exists
self.add_error("role", _("You should specify a role"))
return cleaned_data
request_user = self.request_user
membership = self.request_user_membership
if not (
cleaned_data["role"] <= settings.SITH_MAXIMUM_FREE_ROLE
or (membership is not None and membership.role >= cleaned_data["role"])
or request_user.is_board_member
or request_user.is_root
):
raise forms.ValidationError(_("You do not have the permission to do that"))
return cleaned_data

View File

@ -338,6 +338,8 @@ class Mailing(models.Model):
) )
def clean(self): def clean(self):
if Mailing.objects.filter(email=self.email).exists():
raise ValidationError(_("This mailing list already exists."))
if self.can_moderate(self.moderator): if self.can_moderate(self.moderator):
self.is_moderated = True self.is_moderated = True
else: else:
@ -446,18 +448,20 @@ class MailingSubscription(models.Model):
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
return self.user is not None and user.id == self.user.id return self.user is not None and user.id == self.user.id
@property @cached_property
def get_email(self): def get_email(self):
if self.user and not self.email: if self.user and not self.email:
return self.user.email return self.user.email
return self.email return self.email
@cached_property
def get_username(self):
if self.user:
return str(self.user)
return _("Unregistered user")
def fetch_format(self): def fetch_format(self):
return self.get_email + " " return self.get_email + " "
def __str__(self): def __str__(self):
if self.user: return "(%s) - %s : %s" % (self.mailing, self.get_username, self.email)
user = str(self.user)
else:
user = _("Unregistered user")
return "(%s) - %s : %s" % (self.mailing, user, self.email)

View File

@ -1,17 +1,26 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import select_all_checkbox %}
{% block title %} {% block title %}
{% trans %}Mailing lists{% endtrans %} {% trans %}Mailing lists{% endtrans %}
{% endblock %} {% endblock %}
{% block content %} {% 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> <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 mailings_not_moderated %}
{% if mailing.is_moderated %} <p>{% trans %}Mailing lists waiting for moderation{% endtrans %}</p>
<h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }} <ul>
{% for mailing in mailings_not_moderated %}
<li>{{ mailing.email_full }}<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if mailings_moderated %}
{% for mailing in mailings_moderated %}
<h2>{% trans %}Mailing{% endtrans %} {{ mailing.email_full }}
{%- if user.is_owner(mailing) -%} {%- if user.is_owner(mailing) -%}
<a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a> <a href="{{ url('club:mailing_delete', mailing_id=mailing.id) }}"> - {% trans %}Delete{% endtrans %}</a>
{%- endif -%} {%- endif -%}
@ -19,27 +28,38 @@
<form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;"> <form method="GET" action="{{ url('club:mailing_generate', mailing_id=mailing.id) }}" style="display:inline-block;">
<input type="submit" name="generateMalingList" value="{% trans %}Generate mailing list{% endtrans %}"> <input type="submit" name="generateMalingList" value="{% trans %}Generate mailing list{% endtrans %}">
</form> </form>
<form method="GET" action="{{ url('club:mailing_clean', mailing_id=mailing.id) }}" style="display:inline-block;"> {% set form_mailing_removal = form["removal_" + mailing.id|string] %}
<input type="submit" name="cleanMailingList" value="{% trans %}Clean mailing list{% endtrans %}"> {% if form_mailing_removal.field.choices %}
{% set ms = dict(mailing.subscriptions.all() | groupby('id')) %}
<form action="{{ url('club:mailing', club_id=club.id) }}" id="{{ form_mailing_removal.auto_id }}" method="post" enctype="multipart/form-data">
<p style="margin-bottom: 1em;">{{ select_all_checkbox(form_mailing_removal.auto_id) }}</p>
{% csrf_token %}
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.REMOVE_SUBSCRIPTION }}" />
<table>
<thead>
<tr>
<td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Email{% endtrans %}</td>
<td>{% trans %}Delete{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for widget in form_mailing_removal.subwidgets %}
{% set user = ms[widget.data.value][0] %}
<tr>
<td>{{ user.get_username }}</td>
<td>{{ user.get_email }}</td>
<td>{{ widget.tag() }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ form_mailing_removal.errors }}
<p><input type="submit" value="{% trans %}Remove from mailing list{% endtrans %}" /></p>
</form> </form>
<hr>
<table> {% else %}
<tr> <p><b>{% trans %}There is no subscriber for this mailing list{% endtrans %}</b></p>
<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.get_email }}</td>
<td><a href="{{ url('club:mailing_subscription_delete', mailing_subscription_id=subscriber.id) }}">{% trans %}Delete{% endtrans %}</a></td>
</tr>
{% endfor %}
</table>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@ -47,19 +67,41 @@
<p>{% trans %}No mailing list existing for this club{% endtrans %}</p> <p>{% trans %}No mailing list existing for this club{% endtrans %}</p>
{% endif %} {% endif %}
{% if has_objects %} <p>{{ form.non_field_errors() }}</p>
{% if mailings_moderated %}
<h2>{% trans %}New member{% endtrans %}</h2> <h2>{% trans %}New member{% endtrans %}</h2>
<form action="{{ url('club:mailing_subscription_create', club_id=club.id) }}" method="post" enctype="multipart/form-data"> <form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ add_member.as_p() }} <p>
{{ form.subscription_mailing.errors }}
<label for="{{ form.subscription_mailing.id_for_label }}">{{ form.subscription_mailing.label }}</label>
{{ form.subscription_mailing }}
</p>
<p>
{{ form.subscription_users.errors }}
<label for="{{ form.subscription_users.id_for_label }}">{{ form.subscription_users.label }}</label>
{{ form.subscription_users }}
<span class="helptext">{{ form.subscription_users.help_text }}</span>
</p>
<p>
{{ form.subscription_email.errors }}
<label for="{{ form.subscription_email.id_for_label }}">{{ form.subscription_email.label }}</label>
{{ form.subscription_email }}
</p>
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_SUBSCRIPTION }}" />
<p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Add to mailing list{% endtrans %}" /></p>
</form> </form>
{% endif %} {% endif %}
<h2>{% trans %}New mailing{% endtrans %}</h2> <h2>{% trans %}New mailing{% endtrans %}</h2>
<form action="{{ url('club:mailing_create', club_id=club.id) }}" method="post" enctype="multipart/form-data"> <form action="{{ url('club:mailing', club_id=club.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ add_mailing.as_p() }} <p>
{{ form.mailing_email.errors }}
<label for="{{ form.mailing_email.id_for_label }}">{{ form.mailing_email.label }}</label>
{{ form.mailing_email }}
</p>
<input hidden type="number" name="{{ form.action.name }}" value="{{ form_actions.NEW_MALING }}" />
<p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Create mailing list{% endtrans %}" /></p>
</form> </form>

View File

@ -22,12 +22,17 @@
# #
# #
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.utils import timezone, html
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.management import call_command from django.core.management import call_command
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from core.models import User from core.models import User
from club.models import Club from club.models import Club, Membership, Mailing
from club.forms import MailingForm
# Create your tests here. # Create your tests here.
@ -373,3 +378,316 @@ class ClubTest(TestCase):
"S&#39; Kia</a></td>\\n <td>Responsable info</td>" "S&#39; Kia</a></td>\\n <td>Responsable info</td>"
in content in content
) )
class MailingFormTest(TestCase):
"""Perform validation tests for MailingForm"""
def setUp(self):
call_command("populate")
self.skia = User.objects.filter(username="skia").first()
self.rbatsbak = User.objects.filter(username="rbatsbak").first()
self.krophil = User.objects.filter(username="krophil").first()
self.comunity = User.objects.filter(username="comunity").first()
self.bdf = Club.objects.filter(unix_name="bdf").first()
Membership(
user=self.rbatsbak,
club=self.bdf,
start_date=timezone.now(),
role=settings.SITH_CLUB_ROLES_ID["Board member"],
).save()
def test_mailing_list_add_no_moderation(self):
# Test with Communication admin
self.client.login(username="comunity", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "foyer"},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, text="Liste de diffusion foyer@utbm.fr")
# Test with Root
self.client.login(username="root", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, text="Liste de diffusion mde@utbm.fr")
def test_mailing_list_add_moderation(self):
self.client.login(username="rbatsbak", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertNotContains(response, text="Liste de diffusion mde@utbm.fr")
self.assertContains(
response, text="<p>Listes de diffusions en attente de modération</p>"
)
self.assertContains(response, "<li>mde@utbm.fr")
def test_mailing_list_forbidden(self):
# With anonymous user
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "", status_code=403)
# With user not in club
self.client.login(username="krophil", password="plop")
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "", status_code=403)
def test_add_new_subscription_fail_not_moderated(self):
self.client.login(username="rbatsbak", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": self.skia.id,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertNotContains(response, "skia@git.an")
def test_add_new_subscription_success(self):
# Prepare mailing list
self.client.login(username="comunity", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
# Add single user
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": self.skia.id,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "skia@git.an")
# Add multiple users
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": "|%s|%s|" % (self.comunity.id, self.rbatsbak.id),
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "richard@git.an")
self.assertContains(response, "comunity@git.an")
self.assertContains(response, "skia@git.an")
# Add arbitrary email
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_email": "arbitrary@git.an",
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "richard@git.an")
self.assertContains(response, "comunity@git.an")
self.assertContains(response, "skia@git.an")
self.assertContains(response, "arbitrary@git.an")
# Add user and arbitrary email
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_email": "more.arbitrary@git.an",
"subscription_users": self.krophil.id,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "richard@git.an")
self.assertContains(response, "comunity@git.an")
self.assertContains(response, "skia@git.an")
self.assertContains(response, "arbitrary@git.an")
self.assertContains(response, "more.arbitrary@git.an")
self.assertContains(response, "krophil@git.an")
def test_add_new_subscription_fail_form_errors(self):
# Prepare mailing list
self.client.login(username="comunity", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
# Neither email or email is specified
response = self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
self.assertContains(
response, text=_("You must specify at least an user or an email address")
)
# No mailing specified
response = self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": self.krophil.id,
},
)
self.assertContains(response, text=_("This field is required"))
# One of the selected users doesn't exist
response = self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": "|789|",
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
self.assertContains(
response, text=html.escape(_("One of the selected users doesn't exist"))
)
# An user has no email adress
self.krophil.email = ""
self.krophil.save()
response = self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": self.krophil.id,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
self.assertContains(
response,
text=html.escape(
_("One of the selected users doesn't have an email address")
),
)
self.krophil.email = "krophil@git.an"
self.krophil.save()
# An user is added twice
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": self.krophil.id,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
response = self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": self.krophil.id,
"subscription_mailing": Mailing.objects.get(email="mde").id,
},
)
self.assertContains(
response,
text=html.escape(_("This email is already suscribed in this mailing")),
)
def test_remove_subscription_success(self):
# Prepare mailing list
self.client.login(username="comunity", password="plop")
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{"action": MailingForm.ACTION_NEW_MAILING, "mailing_email": "mde"},
)
mde = Mailing.objects.get(email="mde")
response = self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_NEW_SUBSCRIPTION,
"subscription_users": "|%s|%s|%s|"
% (self.comunity.id, self.rbatsbak.id, self.krophil.id),
"subscription_mailing": mde.id,
},
)
self.assertContains(response, "comunity@git.an")
self.assertContains(response, "richard@git.an")
self.assertContains(response, "krophil@git.an")
# Delete one user
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_REMOVE_SUBSCRIPTION,
"removal_%d" % mde.id: mde.subscriptions.get(user=self.krophil).id,
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertContains(response, "comunity@git.an")
self.assertContains(response, "richard@git.an")
self.assertNotContains(response, "krophil@git.an")
# Delete multiple users
self.client.post(
reverse("club:mailing", kwargs={"club_id": self.bdf.id}),
{
"action": MailingForm.ACTION_REMOVE_SUBSCRIPTION,
"removal_%d"
% mde.id: [
user.id
for user in mde.subscriptions.filter(
user__in=[self.rbatsbak, self.comunity]
).all()
],
},
)
response = self.client.get(
reverse("club:mailing", kwargs={"club_id": self.bdf.id})
)
self.assertNotContains(response, "comunity@git.an")
self.assertNotContains(response, "richard@git.an")
self.assertNotContains(response, "krophil@git.an")

View File

@ -64,31 +64,12 @@ urlpatterns = [
), ),
url(r"^(?P<club_id>[0-9]+)/prop$", ClubEditPropView.as_view(), name="club_prop"), 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]+)/tools$", ClubToolsView.as_view(), name="tools"),
url( url(r"^(?P<club_id>[0-9]+)/mailing$", ClubMailingView.as_view(), name="mailing"),
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( url(
r"^(?P<mailing_id>[0-9]+)/mailing/generate$", r"^(?P<mailing_id>[0-9]+)/mailing/generate$",
MailingAutoGenerationView.as_view(), MailingAutoGenerationView.as_view(),
name="mailing_generate", name="mailing_generate",
), ),
url(
r"^(?P<mailing_id>[0-9]+)/mailing/clean$",
MailingAutoCleanView.as_view(),
name="mailing_clean",
),
url( url(
r"^(?P<mailing_id>[0-9]+)/mailing/delete$", r"^(?P<mailing_id>[0-9]+)/mailing/delete$",
MailingDeleteView.as_view(), MailingDeleteView.as_view(),

View File

@ -23,6 +23,8 @@
# #
# #
from django.conf import settings
from django import forms from django import forms
from django.views.generic import ListView, DetailView, TemplateView, View from django.views.generic import ListView, DetailView, TemplateView, View
from django.views.generic.edit import DeleteView from django.views.generic.edit import DeleteView
@ -33,8 +35,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext as _t from django.utils.translation import ugettext as _t
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from core.views import ( from core.views import (
@ -46,71 +47,19 @@ from core.views import (
PageEditViewBase, PageEditViewBase,
DetailFormView, DetailFormView,
) )
from core.views.forms import SelectDate, SelectDateTime from core.models import PageRev
from club.models import Club, Membership, Mailing, MailingSubscription
from sith.settings import SITH_MAXIMUM_FREE_ROLE from counter.models import Selling
from counter.models import Selling, Counter
from core.models import User, PageRev
from com.views import ( from com.views import (
PosterListBaseView, PosterListBaseView,
PosterCreateBaseView, PosterCreateBaseView,
PosterEditBaseView, PosterEditBaseView,
PosterDeleteBaseView, PosterDeleteBaseView,
) )
from com.models import Poster
from django.conf import settings from club.models import Club, Membership, Mailing, MailingSubscription
from club.forms import MailingForm, ClubEditForm, ClubMemberForm
# Custom forms
class ClubEditForm(forms.ModelForm):
class Meta:
model = Club
fields = ["address", "logo", "short_description"]
def __init__(self, *args, **kwargs):
super(ClubEditForm, self).__init__(*args, **kwargs)
self.fields["short_description"].widget = forms.Textarea()
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): class ClubTabsMixin(TabedViewMixin):
@ -306,122 +255,6 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
current_tab = "tools" current_tab = "tools"
class ClubMemberForm(forms.Form):
"""
Form handling the members of a club
"""
error_css_class = "error"
required_css_class = "required"
users = AutoCompleteSelectMultipleField(
"users",
label=_("Users to add"),
help_text=_("Search users to add (one or more)."),
required=False,
)
def __init__(self, *args, **kwargs):
self.club = kwargs.pop("club")
self.request_user = kwargs.pop("request_user")
self.club_members = kwargs.pop("club_members", None)
if not self.club_members:
self.club_members = (
self.club.members.filter(end_date=None).order_by("-role").all()
)
self.request_user_membership = self.club.get_membership_for(self.request_user)
super(ClubMemberForm, self).__init__(*args, **kwargs)
# Using a ModelForm binds too much the form with the model and we don't want that
# We want the view to process the model creation since they are multiple users
# We also want the form to handle bulk deletion
self.fields.update(
forms.fields_for_model(
Membership,
fields=("role", "start_date", "description"),
widgets={"start_date": SelectDate},
)
)
# Role is required only if users is specified
self.fields["role"].required = False
# Start date and description are never really required
self.fields["start_date"].required = False
self.fields["description"].required = False
self.fields["users_old"] = forms.ModelMultipleChoiceField(
User.objects.filter(
id__in=[
ms.user.id
for ms in self.club_members
if ms.can_be_edited_by(
self.request_user, self.request_user_membership
)
]
).all(),
label=_("Mark as old"),
required=False,
widget=forms.CheckboxSelectMultiple,
)
if not self.request_user.is_root:
self.fields.pop("start_date")
def clean_users(self):
"""
Check that the user is not trying to add an user already in the club
Also check that the user is valid and has a valid subscription
"""
cleaned_data = super(ClubMemberForm, self).clean()
users = []
for user_id in cleaned_data["users"]:
user = User.objects.filter(id=user_id).first()
if not user:
raise forms.ValidationError(
_("One of the selected users doesn't exist"), code="invalid"
)
if not user.is_subscribed:
raise forms.ValidationError(
_("User must be subscriber to take part to a club"), code="invalid"
)
if self.club.get_membership_for(user):
raise forms.ValidationError(
_("You can not add the same user twice"), code="invalid"
)
users.append(user)
return users
def clean(self):
"""
Check user rights for adding an user
"""
cleaned_data = super(ClubMemberForm, self).clean()
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
# Drop start_date if allowed to edition but not specified
cleaned_data.pop("start_date")
if not cleaned_data.get("users"):
# No user to add equals no check needed
return cleaned_data
if cleaned_data.get("role", "") == "":
# Role is required if users exists
self.add_error("role", _("You should specify a role"))
return cleaned_data
request_user = self.request_user
membership = self.request_user_membership
if not (
cleaned_data["role"] <= SITH_MAXIMUM_FREE_ROLE
or (membership is not None and membership.role >= cleaned_data["role"])
or request_user.is_board_member
or request_user.is_root
):
raise forms.ValidationError(_("You do not have the permission to do that"))
return cleaned_data
class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
""" """
View of a club's members View of a club's members
@ -485,24 +318,6 @@ class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
current_tab = "elderlies" current_tab = "elderlies"
class SellingsFormBase(forms.Form):
begin_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
label=_("Begin date"),
required=False,
widget=SelectDateTime,
)
end_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"],
label=_("End date"),
required=False,
widget=SelectDateTime,
)
counter = forms.ModelChoiceField(
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
)
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView): class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailView):
""" """
Sellings of a club Sellings of a club
@ -687,94 +502,134 @@ class ClubStatView(TemplateView):
return kwargs return kwargs
class ClubMailingView(ClubTabsMixin, ListView): class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):
""" """
A list of mailing for a given club A list of mailing for a given club
""" """
action = None model = Club
model = Mailing form_class = MailingForm
pk_url_kwarg = "club_id"
template_name = "club/mailing.jinja" template_name = "club/mailing.jinja"
current_tab = "mailing" current_tab = "mailing"
def authorized(self): def get_form_kwargs(self):
return ( kwargs = super(ClubMailingView, self).get_form_kwargs()
self.club.has_rights_in_club(self.user) kwargs["club_id"] = self.get_object().id
or self.user.is_root kwargs["user_id"] = self.request.user.id
or self.user.is_in_group(settings.SITH_GROUP_COM_ADMIN_ID) kwargs["mailings"] = self.mailings
) return kwargs
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.club = get_object_or_404(Club, pk=kwargs["club_id"]) self.mailings = Mailing.objects.filter(club_id=self.get_object().id).all()
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) 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): def get_context_data(self, **kwargs):
kwargs = super(ClubMailingView, self).get_context_data(**kwargs) kwargs = super(ClubMailingView, self).get_context_data(**kwargs)
kwargs["add_member"] = self.member_form kwargs["club"] = self.get_object()
kwargs["add_mailing"] = self.mailing_form kwargs["user"] = self.request.user
kwargs["club"] = self.club kwargs["mailings"] = self.mailings
kwargs["user"] = self.user kwargs["mailings_moderated"] = (
kwargs["has_objects"] = len(kwargs["object_list"]) > 0 kwargs["mailings"].exclude(is_moderated=False).all()
)
kwargs["mailings_not_moderated"] = (
kwargs["mailings"].exclude(is_moderated=True).all()
)
kwargs["form_actions"] = {
"NEW_MALING": self.form_class.ACTION_NEW_MAILING,
"NEW_SUBSCRIPTION": self.form_class.ACTION_NEW_SUBSCRIPTION,
"REMOVE_SUBSCRIPTION": self.form_class.ACTION_REMOVE_SUBSCRIPTION,
}
return kwargs return kwargs
def get_object(self): def add_new_mailing(self, cleaned_data) -> ValidationError:
return self.club """
Create a new mailing list from the form
"""
mailing = Mailing(
club=self.get_object(),
email=cleaned_data["mailing_email"],
moderator=self.request.user,
is_moderated=False,
)
try:
mailing.clean()
except ValidationError as validation_error:
return validation_error
mailing.save()
return None
def add_new_subscription(self, cleaned_data) -> ValidationError:
"""
Add mailing subscriptions for each user given and/or for the specified email in form
"""
users_to_save = []
class MailingGenericCreateView(CreateView, SingleObjectMixin): for user in cleaned_data["subscription_users"]:
""" sub = MailingSubscription(
Create a new mailing list mailing=cleaned_data["subscription_mailing"], user=user
""" )
try:
sub.clean()
except ValidationError as validation_error:
return validation_error
list_view = None users_to_save.append(sub.save())
form_class = None
def get_context_data(self, **kwargs): if cleaned_data["subscription_email"]:
view_kwargs = self.list_view.get_context_data(**kwargs) sub = MailingSubscription(
for key, data in ( mailing=cleaned_data["subscription_mailing"],
super(MailingGenericCreateView, self).get_context_data(**kwargs).items() email=cleaned_data["subscription_email"],
): )
view_kwargs[key] = data
view_kwargs[self.list_view.action] = view_kwargs["form"]
return view_kwargs
def get_form_kwargs(self): try:
kwargs = super(MailingGenericCreateView, self).get_form_kwargs() sub.clean()
kwargs["club_id"] = self.list_view.club.id except ValidationError as validation_error:
kwargs["user_id"] = self.list_view.user.id return validation_error
return kwargs sub.save()
def dispatch(self, request, *args, **kwargs): # Save users after we are sure there is no error
if not self.list_view.authorized(): for user in users_to_save:
raise PermissionDenied user.save()
self.template_name = self.list_view.template_name
return super(MailingGenericCreateView, self).dispatch(request, *args, **kwargs) return None
def remove_subscription(self, cleaned_data):
"""
Remove specified users from a mailing list
"""
fields = [
cleaned_data[key]
for key in cleaned_data.keys()
if key.startswith("removal_")
]
for field in fields:
for sub in field:
sub.delete()
def form_valid(self, form):
resp = super(ClubMailingView, self).form_valid(form)
cleaned_data = form.clean()
error = None
if cleaned_data["action"] == self.form_class.ACTION_NEW_MAILING:
error = self.add_new_mailing(cleaned_data)
if cleaned_data["action"] == self.form_class.ACTION_NEW_SUBSCRIPTION:
error = self.add_new_subscription(cleaned_data)
if cleaned_data["action"] == self.form_class.ACTION_REMOVE_SUBSCRIPTION:
self.remove_subscription(cleaned_data)
if error:
form.add_error(NON_FIELD_ERRORS, error)
return self.form_invalid(form)
return resp
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy("club:mailing", kwargs={"club_id": self.list_view.club.id}) return reverse_lazy("club:mailing", kwargs={"club_id": self.get_object().id})
class MailingDeleteView(CanEditMixin, DeleteView): class MailingDeleteView(CanEditMixin, DeleteView):
@ -829,18 +684,6 @@ class MailingAutoGenerationView(View):
return redirect("club:mailing", club_id=club.id) return redirect("club:mailing", club_id=club.id)
class MailingAutoCleanView(View):
def dispatch(self, request, *args, **kwargs):
self.mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"])
if not request.user.can_edit(self.mailing):
raise PermissionDenied
return super(MailingAutoCleanView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
self.mailing.subscriptions.all().delete()
return redirect("club:mailing", club_id=self.mailing.club.id)
class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
"""List communication posters""" """List communication posters"""

View File

@ -837,6 +837,27 @@ Welcome to the wiki page!
krophil_profile.save() krophil_profile.save()
krophil.profile_pict = krophil_profile krophil.profile_pict = krophil_profile
krophil.save() krophil.save()
# Adding user Com Unity
comunity = User(
username="comunity",
last_name="Unity",
first_name="Com",
email="comunity@git.an",
date_of_birth="1942-06-12",
)
comunity.set_password("plop")
comunity.save()
comunity.groups = [
Group.objects.filter(name="Communication admin").first().id
]
comunity.save()
Membership(
user=comunity,
club=bar_club,
start_date=timezone.now(),
role=settings.SITH_CLUB_ROLES_ID["Board member"],
).save()
# Adding subscription for sli # Adding subscription for sli
s = Subscription( s = Subscription(
member=User.objects.filter(pk=sli.pk).first(), member=User.objects.filter(pk=sli.pk).first(),
@ -861,6 +882,18 @@ Welcome to the wiki page!
start=s.subscription_start, start=s.subscription_start,
) )
s.save() s.save()
# Com Unity
s = Subscription(
member=comunity,
subscription_type=default_subscription,
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
)
s.subscription_start = s.compute_start()
s.subscription_end = s.compute_end(
duration=settings.SITH_SUBSCRIPTIONS[s.subscription_type]["duration"],
start=s.subscription_start,
)
s.save()
Selling( Selling(
label=dcons.name, label=dcons.name,

View File

@ -296,11 +296,6 @@ class DetailFormView(SingleObjectMixin, FormView):
""" """
return super(DetailFormView, self).get_object() return super(DetailFormView, self).get_object()
def get_context_data(self, *args, **kwargs):
kwargs = super(DetailFormView, self).get_context_data()
kwargs["object"] = self.get_object()
return kwargs
from .user import * from .user import *
from .page import * from .page import *

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-28 15:00+0200\n" "POT-Creation-Date: 2019-05-09 11:33+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n" "Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@ -174,10 +174,10 @@ msgstr "étiquette"
msgid "target type" msgid "target type"
msgstr "type de cible" msgstr "type de cible"
#: accounting/models.py:313 club/models.py:413 #: accounting/models.py:313 club/models.py:415
#: club/templates/club/club_members.jinja:16 #: club/templates/club/club_members.jinja:16
#: club/templates/club/club_old_members.jinja:8 #: club/templates/club/club_old_members.jinja:8
#: club/templates/club/mailing.jinja:28 club/views.py:112 #: club/templates/club/mailing.jinja:41
#: counter/templates/counter/cash_summary_list.jinja:32 #: counter/templates/counter/cash_summary_list.jinja:32
#: counter/templates/counter/stats.jinja:15 #: counter/templates/counter/stats.jinja:15
#: counter/templates/counter/stats.jinja:52 #: counter/templates/counter/stats.jinja:52
@ -345,7 +345,7 @@ msgstr "Compte en banque : "
#: accounting/templates/accounting/club_account_details.jinja:60 #: accounting/templates/accounting/club_account_details.jinja:60
#: accounting/templates/accounting/label_list.jinja:26 #: accounting/templates/accounting/label_list.jinja:26
#: club/templates/club/club_sellings.jinja:50 #: club/templates/club/club_sellings.jinja:50
#: club/templates/club/mailing.jinja:16 club/templates/club/mailing.jinja:39 #: club/templates/club/mailing.jinja:25 club/templates/club/mailing.jinja:43
#: com/templates/com/mailing_admin.jinja:19 #: com/templates/com/mailing_admin.jinja:19
#: com/templates/com/news_admin_list.jinja:41 #: com/templates/com/news_admin_list.jinja:41
#: com/templates/com/news_admin_list.jinja:70 #: com/templates/com/news_admin_list.jinja:70
@ -386,7 +386,7 @@ msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
#: accounting/templates/accounting/bank_account_details.jinja:18 #: accounting/templates/accounting/bank_account_details.jinja:18
#: club/views.py:129 core/views/user.py:205 sas/templates/sas/picture.jinja:86 #: club/views.py:78 core/views/user.py:205 sas/templates/sas/picture.jinja:86
msgid "Infos" msgid "Infos"
msgstr "Infos" msgstr "Infos"
@ -405,7 +405,7 @@ msgstr "Nouveau compte club"
#: accounting/templates/accounting/bank_account_details.jinja:27 #: accounting/templates/accounting/bank_account_details.jinja:27
#: accounting/templates/accounting/bank_account_list.jinja:22 #: accounting/templates/accounting/bank_account_list.jinja:22
#: accounting/templates/accounting/club_account_details.jinja:58 #: accounting/templates/accounting/club_account_details.jinja:58
#: accounting/templates/accounting/journal_details.jinja:89 club/views.py:175 #: accounting/templates/accounting/journal_details.jinja:89 club/views.py:124
#: com/templates/com/news_admin_list.jinja:39 #: com/templates/com/news_admin_list.jinja:39
#: com/templates/com/news_admin_list.jinja:68 #: com/templates/com/news_admin_list.jinja:68
#: com/templates/com/news_admin_list.jinja:115 #: com/templates/com/news_admin_list.jinja:115
@ -893,6 +893,101 @@ msgstr "Opérations sans étiquette"
msgid "Refound this account" msgid "Refound this account"
msgstr "Rembourser ce compte" msgstr "Rembourser ce compte"
#: club/forms.py:60 club/forms.py:197
msgid "Users to add"
msgstr "Utilisateurs à ajouter"
#: club/forms.py:61 club/forms.py:198 core/views/group.py:63
msgid "Search users to add (one or more)."
msgstr "Recherche les utilisateurs à ajouter (un ou plus)."
#: club/forms.py:70
#, fuzzy
#| msgid "New mailing"
msgid "New Mailing"
msgstr "Nouvelle mailing liste"
#: club/forms.py:71
#, fuzzy
#| msgid "Unsubscribe"
msgid "Subscribe"
msgstr "Se désabonner"
#: club/forms.py:72 club/forms.py:85 com/templates/com/news_admin_list.jinja:40
#: com/templates/com/news_admin_list.jinja:116
#: com/templates/com/news_admin_list.jinja:198
#: com/templates/com/news_admin_list.jinja:274
msgid "Remove"
msgstr "Retirer"
#: club/forms.py:75 launderette/views.py:228
msgid "Action"
msgstr "Action"
#: club/forms.py:122
#, fuzzy
#| msgid "This field is required."
msgid "This field is required"
msgstr "Ce champ est obligatoire."
#: club/forms.py:134 club/forms.py:259
msgid "One of the selected users doesn't exist"
msgstr "Un des utilisateurs sélectionné n'existe pas"
#: club/forms.py:138
#, fuzzy
#| msgid "One of the selected users doesn't exist"
msgid "One of the selected users doesn't have an email address"
msgstr "Un des utilisateurs sélectionné n'existe pas"
#: club/forms.py:149
#, fuzzy
#| msgid "This field is required."
msgid "An action is required"
msgstr "Ce champ est obligatoire."
#: club/forms.py:162
msgid "You must specify at least an user or an email address"
msgstr "vous devez spécifier au moins un utilisateur ou une adresse email"
#: club/forms.py:172 counter/views.py:1481
msgid "Begin date"
msgstr "Date de début"
#: club/forms.py:178 com/views.py:85 com/views.py:221 counter/views.py:1487
#: election/views.py:190 subscription/views.py:52
msgid "End date"
msgstr "Date de fin"
#: club/forms.py:183 club/templates/club/club_sellings.jinja:21
#: core/templates/core/user_account_detail.jinja:18
#: core/templates/core/user_account_detail.jinja:51
#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:168
msgid "Counter"
msgstr "Comptoir"
#: club/forms.py:241 club/templates/club/club_members.jinja:21
#: club/templates/club/club_members.jinja:46
#: core/templates/core/user_clubs.jinja:29
msgid "Mark as old"
msgstr "Marquer comme ancien"
#: club/forms.py:263
msgid "User must be subscriber to take part to a club"
msgstr "L'utilisateur doit être cotisant pour faire partie d'un club"
#: club/forms.py:267 core/views/group.py:82
msgid "You can not add the same user twice"
msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur"
#: club/forms.py:288
msgid "You should specify a role"
msgstr "Vous devez choisir un rôle"
#: club/forms.py:299 sas/views.py:129 sas/views.py:195 sas/views.py:286
msgid "You do not have the permission to do that"
msgstr "Vous n'avez pas la permission de faire cela"
#: club/models.py:51 #: club/models.py:51
msgid "unix name" msgid "unix name"
msgstr "nom unix" msgstr "nom unix"
@ -959,7 +1054,7 @@ msgstr "description"
msgid "past member" msgid "past member"
msgstr "Anciens membres" msgstr "Anciens membres"
#: club/models.py:323 club/models.py:418 #: club/models.py:323 club/models.py:420
msgid "Email address" msgid "Email address"
msgstr "Adresse email" msgstr "Adresse email"
@ -976,21 +1071,25 @@ msgstr "est modéré"
msgid "moderator" msgid "moderator"
msgstr "modérateur" msgstr "modérateur"
#: club/models.py:406 club/templates/club/mailing.jinja:14 #: club/models.py:342
msgid "This mailing list already exists."
msgstr "Cette liste de diffusion existe déjà."
#: club/models.py:408 club/templates/club/mailing.jinja:23
msgid "Mailing" msgid "Mailing"
msgstr "Liste de diffusion" msgstr "Liste de diffusion"
#: club/models.py:425 #: club/models.py:427
msgid "At least user or email is required" msgid "At least user or email is required"
msgstr "Au moins un utilisateur ou un email est nécessaire" msgstr "Au moins un utilisateur ou un email est nécessaire"
#: club/models.py:433 #: club/models.py:435
msgid "This email is already suscribed in this mailing" msgid "This email is already suscribed in this mailing"
msgstr "Cet email est déjà abonné à cette mailing" msgstr "Cet email est déjà abonné à cette mailing"
#: club/models.py:462 club/templates/club/mailing.jinja:36 #: club/models.py:461
msgid "Unregistered user" msgid "Unregistered user"
msgstr "Désabonner un utilisateur" msgstr "Utilisateur non enregistré"
#: club/templates/club/club_list.jinja:4 club/templates/club/club_list.jinja:37 #: club/templates/club/club_list.jinja:4 club/templates/club/club_list.jinja:37
msgid "Club list" msgid "Club list"
@ -1037,12 +1136,6 @@ msgstr "Description"
msgid "Since" msgid "Since"
msgstr "Depuis" msgstr "Depuis"
#: club/templates/club/club_members.jinja:21
#: club/templates/club/club_members.jinja:46 club/views.py:363
#: core/templates/core/user_clubs.jinja:29
msgid "Mark as old"
msgstr "Marquer comme ancien"
#: club/templates/club/club_members.jinja:50 #: club/templates/club/club_members.jinja:50
msgid "There are no members in this club." msgid "There are no members in this club."
msgstr "Il n'y a pas de membres dans ce club." msgstr "Il n'y a pas de membres dans ce club."
@ -1067,8 +1160,8 @@ msgstr "Du"
msgid "To" msgid "To"
msgstr "Au" msgstr "Au"
#: club/templates/club/club_sellings.jinja:5 club/views.py:195 #: club/templates/club/club_sellings.jinja:5 club/views.py:144
#: club/views.py:563 counter/templates/counter/counter_main.jinja:19 #: club/views.py:378 counter/templates/counter/counter_main.jinja:19
#: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:35
msgid "Sellings" msgid "Sellings"
msgstr "Ventes" msgstr "Ventes"
@ -1094,13 +1187,6 @@ msgstr "unités"
msgid "Benefit: " msgid "Benefit: "
msgstr "Bénéfice : " msgstr "Bénéfice : "
#: club/templates/club/club_sellings.jinja:21 club/views.py:502
#: core/templates/core/user_account_detail.jinja:18
#: core/templates/core/user_account_detail.jinja:51
#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:168
msgid "Counter"
msgstr "Comptoir"
#: club/templates/club/club_sellings.jinja:22 #: club/templates/club/club_sellings.jinja:22
#: core/templates/core/user_account_detail.jinja:19 #: core/templates/core/user_account_detail.jinja:19
#: core/templates/core/user_account_detail.jinja:52 #: core/templates/core/user_account_detail.jinja:52
@ -1189,7 +1275,7 @@ msgstr "Comptabilité : "
msgid "Manage launderettes" msgid "Manage launderettes"
msgstr "Gestion des laveries" msgstr "Gestion des laveries"
#: club/templates/club/mailing.jinja:4 #: club/templates/club/mailing.jinja:5
msgid "Mailing lists" msgid "Mailing lists"
msgstr "Mailing listes" msgstr "Mailing listes"
@ -1202,38 +1288,46 @@ msgstr ""
"nouvellement créee n'est pas affichée, attendez jusqu'à qu'un modérateur " "nouvellement créee n'est pas affichée, attendez jusqu'à qu'un modérateur "
"entre en action" "entre en action"
#: club/templates/club/mailing.jinja:20 #: club/templates/club/mailing.jinja:13
msgid "Mailing lists waiting for moderation"
msgstr "Listes de diffusions en attente de modération"
#: club/templates/club/mailing.jinja:29
msgid "Generate mailing list" msgid "Generate mailing list"
msgstr "Générer la liste de diffusion" msgstr "Générer la liste de diffusion"
#: club/templates/club/mailing.jinja:23 #: club/templates/club/mailing.jinja:42
msgid "Clean mailing list"
msgstr "Néttoyer la liste de diffusion"
#: club/templates/club/mailing.jinja:29
#: com/templates/com/mailing_admin.jinja:10 #: com/templates/com/mailing_admin.jinja:10
msgid "Email" msgid "Email"
msgstr "Email" msgstr "Email"
#: club/templates/club/mailing.jinja:47 #: club/templates/club/mailing.jinja:58
msgid "Remove from mailing list"
msgstr "Supprimer de la liste de diffusion"
#: club/templates/club/mailing.jinja:62
msgid "There is no subscriber for this mailing list"
msgstr "Il n'y a pas d'abonnés dans cette liste de diffusion"
#: club/templates/club/mailing.jinja:67
msgid "No mailing list existing for this club" msgid "No mailing list existing for this club"
msgstr "Aucune mailing liste n'existe pour ce club" msgstr "Aucune mailing liste n'existe pour ce club"
#: club/templates/club/mailing.jinja:51 #: club/templates/club/mailing.jinja:72
msgid "New member" msgid "New member"
msgstr "Nouveau membre" msgstr "Nouveau membre"
#: club/templates/club/mailing.jinja:55 #: club/templates/club/mailing.jinja:92
msgid "Add to mailing list" msgid "Add to mailing list"
msgstr "Ajouter à la mailing liste" msgstr "Ajouter à la mailing liste"
#: club/templates/club/mailing.jinja:59 #: club/templates/club/mailing.jinja:96
msgid "New mailing" msgid "New mailing"
msgstr "Nouvelle mailing liste" msgstr "Nouvelle liste de diffusion"
#: club/templates/club/mailing.jinja:63 #: club/templates/club/mailing.jinja:109
msgid "Create mailing list" msgid "Create mailing list"
msgstr "Créer une mailing liste" msgstr "Créer une liste de diffusion"
#: club/templates/club/page_history.jinja:8 #: club/templates/club/page_history.jinja:8
msgid "No page existing for this club" msgid "No page existing for this club"
@ -1243,79 +1337,42 @@ msgstr "Aucune page n'existe pour ce club"
msgid "Club stats" msgid "Club stats"
msgstr "Statistiques du club" msgstr "Statistiques du club"
#: club/views.py:139 #: club/views.py:88
msgid "Members" msgid "Members"
msgstr "Membres" msgstr "Membres"
#: club/views.py:148 #: club/views.py:97
msgid "Old members" msgid "Old members"
msgstr "Anciens membres" msgstr "Anciens membres"
#: club/views.py:158 core/templates/core/page.jinja:33 #: club/views.py:107 core/templates/core/page.jinja:33
msgid "History" msgid "History"
msgstr "Historique" msgstr "Historique"
#: club/views.py:166 core/templates/core/base.jinja:121 core/views/user.py:228 #: club/views.py:115 core/templates/core/base.jinja:121 core/views/user.py:228
#: sas/templates/sas/picture.jinja:95 trombi/views.py:60 #: sas/templates/sas/picture.jinja:95 trombi/views.py:60
msgid "Tools" msgid "Tools"
msgstr "Outils" msgstr "Outils"
#: club/views.py:186 #: club/views.py:135
msgid "Edit club page" msgid "Edit club page"
msgstr "Éditer la page de club" msgstr "Éditer la page de club"
#: club/views.py:202 #: club/views.py:151
msgid "Mailing list" msgid "Mailing list"
msgstr "Listes de diffusion" msgstr "Listes de diffusion"
#: club/views.py:211 com/views.py:141 #: club/views.py:160 com/views.py:141
msgid "Posters list" msgid "Posters list"
msgstr "Liste d'affiches" msgstr "Liste d'affiches"
#: club/views.py:221 counter/templates/counter/counter_list.jinja:21 #: club/views.py:170 counter/templates/counter/counter_list.jinja:21
#: counter/templates/counter/counter_list.jinja:43 #: counter/templates/counter/counter_list.jinja:43
#: counter/templates/counter/counter_list.jinja:59 #: counter/templates/counter/counter_list.jinja:59
msgid "Props" msgid "Props"
msgstr "Propriétés" msgstr "Propriétés"
#: club/views.py:319 #: club/views.py:335 core/templates/core/user_stats.jinja:27
msgid "Users to add"
msgstr "Utilisateurs à ajouter"
#: club/views.py:320 core/views/group.py:63
msgid "Search users to add (one or more)."
msgstr "Recherche les utilisateurs à ajouter (un ou plus)."
#: club/views.py:381
msgid "One of the selected users doesn't exist"
msgstr "Un des utilisateurs sélectionné n'existe pas"
#: club/views.py:385
msgid "User must be subscriber to take part to a club"
msgstr "L'utilisateur doit être cotisant pour faire partie d'un club"
#: club/views.py:389 core/views/group.py:82
msgid "You can not add the same user twice"
msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur"
#: club/views.py:410
msgid "You should specify a role"
msgstr "Vous devez choisir un rôle"
#: club/views.py:421 sas/views.py:129 sas/views.py:195 sas/views.py:286
msgid "You do not have the permission to do that"
msgstr "Vous n'avez pas la permission de faire cela"
#: club/views.py:491 counter/views.py:1481
msgid "Begin date"
msgstr "Date de début"
#: club/views.py:497 com/views.py:85 com/views.py:221 counter/views.py:1487
#: election/views.py:190 subscription/views.py:52
msgid "End date"
msgstr "Date de fin"
#: club/views.py:520 core/templates/core/user_stats.jinja:27
#: counter/views.py:1635 #: counter/views.py:1635
msgid "Product" msgid "Product"
msgstr "Produit" msgstr "Produit"
@ -1547,13 +1604,6 @@ msgstr "Auteur"
msgid "Moderator" msgid "Moderator"
msgstr "Modérateur" msgstr "Modérateur"
#: com/templates/com/news_admin_list.jinja:40
#: com/templates/com/news_admin_list.jinja:116
#: com/templates/com/news_admin_list.jinja:198
#: com/templates/com/news_admin_list.jinja:274
msgid "Remove"
msgstr "Retirer"
#: com/templates/com/news_admin_list.jinja:47 #: com/templates/com/news_admin_list.jinja:47
msgid "Notices to moderate" msgid "Notices to moderate"
msgstr "Informations à modérer" msgstr "Informations à modérer"
@ -4612,10 +4662,6 @@ msgstr "Éditer la page de présentation"
msgid "Book launderette slot" msgid "Book launderette slot"
msgstr "Réserver un créneau de laverie" msgstr "Réserver un créneau de laverie"
#: launderette/views.py:228
msgid "Action"
msgstr "Action"
#: launderette/views.py:240 #: launderette/views.py:240
msgid "Tokens, separated by spaces" msgid "Tokens, separated by spaces"
msgstr "Jetons, séparés par des espaces" msgstr "Jetons, séparés par des espaces"