From 577ad07a2b6b0af04dfbf7c829d4f71028f14cd8 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Thu, 18 Oct 2018 01:16:26 +0200 Subject: [PATCH 1/9] Can identify user on counter with student card UID --- counter/migrations/0017_studentcard.py | 41 ++++++++++++++++++++++++++ counter/models.py | 20 +++++++++++++ counter/views.py | 12 ++++++-- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 counter/migrations/0017_studentcard.py diff --git a/counter/migrations/0017_studentcard.py b/counter/migrations/0017_studentcard.py new file mode 100644 index 00000000..963f77db --- /dev/null +++ b/counter/migrations/0017_studentcard.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-10-17 23:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [("counter", "0016_producttype_comment")] + + operations = [ + migrations.CreateModel( + name="StudentCard", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "uid", + models.CharField(max_length=14, unique=True, verbose_name="uid"), + ), + ( + "customer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="student_cards", + to="counter.Customer", + verbose_name="student cards", + ), + ), + ], + ) + ] diff --git a/counter/models.py b/counter/models.py index 9767cfd3..196b474d 100644 --- a/counter/models.py +++ b/counter/models.py @@ -732,3 +732,23 @@ class Eticket(models.Model): return hmac.new( bytes(self.secret, "utf-8"), bytes(string, "utf-8"), hashlib.sha1 ).hexdigest() + + +class StudentCard(models.Model): + """ + Alternative way to connect a customer into a counter + We are using Mifare DESFire EV1 specs since it's used for izly cards + https://www.nxp.com/docs/en/application-note/AN10927.pdf + UID is 7 byte long that means 14 hexa characters + """ + + UID_SIZE = 14 + + uid = models.CharField(_("uid"), max_length=14, unique=True) + customer = models.ForeignKey( + Customer, + related_name="student_cards", + verbose_name=_("student cards"), + null=False, + blank=False, + ) diff --git a/counter/views.py b/counter/views.py index 941cbc80..fd51b20f 100644 --- a/counter/views.py +++ b/counter/views.py @@ -57,6 +57,7 @@ from subscription.models import Subscription from counter.models import ( Counter, Customer, + StudentCard, Product, Selling, Refilling, @@ -121,9 +122,14 @@ class GetUserForm(forms.Form): cleaned_data = super(GetUserForm, self).clean() cus = None if cleaned_data["code"] != "": - cus = Customer.objects.filter( - account_id__iexact=cleaned_data["code"] - ).first() + if len(cleaned_data["code"]) == StudentCard.UID_SIZE: + card = StudentCard.objects.filter(uid=cleaned_data["code"]) + if card.exists(): + cus = card.first().customer + if cus is None: + cus = Customer.objects.filter( + account_id__iexact=cleaned_data["code"] + ).first() elif cleaned_data["id"] is not None: cus = Customer.objects.filter(user=cleaned_data["id"]).first() if cus is None or not cus.can_buy: From 14d9fc04d190bdc0f56b8ff034bad445a147e489 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Fri, 19 Oct 2018 01:21:40 +0200 Subject: [PATCH 2/9] Stronger bdd validation for studentcards --- counter/migrations/0017_studentcard.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/counter/migrations/0017_studentcard.py b/counter/migrations/0017_studentcard.py index 963f77db..8304a104 100644 --- a/counter/migrations/0017_studentcard.py +++ b/counter/migrations/0017_studentcard.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.13 on 2018-10-17 23:02 +# Generated by Django 1.11.13 on 2018-10-18 23:15 from __future__ import unicode_literals +import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -25,7 +26,12 @@ class Migration(migrations.Migration): ), ( "uid", - models.CharField(max_length=14, unique=True, verbose_name="uid"), + models.CharField( + max_length=14, + unique=True, + validators=[django.core.validators.MinLengthValidator(4)], + verbose_name="uid", + ), ), ( "customer", From 4669e5a4e9aacbb4fb09497a221e616c5609bcfb Mon Sep 17 00:00:00 2001 From: klmp200 Date: Fri, 19 Oct 2018 01:21:57 +0200 Subject: [PATCH 3/9] Gui for studentcards in counters --- counter/models.py | 29 ++++++++++++++++++- counter/templates/counter/counter_click.jinja | 10 +++++++ counter/views.py | 17 +++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/counter/models.py b/counter/models.py index 196b474d..0d373d36 100644 --- a/counter/models.py +++ b/counter/models.py @@ -27,8 +27,10 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.conf import settings from django.core.urlresolvers import reverse +from django.core.validators import MinLengthValidator from django.forms import ValidationError from django.utils.functional import cached_property +from django.core.exceptions import PermissionDenied from datetime import timedelta, date import random @@ -85,6 +87,29 @@ class Customer(models.Model): letter = random.choice(string.ascii_lowercase) return number + letter + def add_student_card(self, uid, request, counter=None): + """ + Add a new student card on the customer account + """ + # If you are comming from a counter, only your connection to the counter is checked, not your right on the user to avoid wierd conflicts + if counter != None and ( + counter.type != "BAR" + or not ( + "counter_token" in request.session.keys() + and request.session["counter_token"] == counter.token + ) + or len(counter.get_barmen_list()) < 1 + ): + raise PermissionDenied + # If you are not comming from a counter, your permissions are checked + if not ( + request.user.id == self.user.id + or request.user.is_board_member + or request.user.is_root + ): + raise PermissionDenied + StudentCard(customer=self, uid=uid).save() + def save(self, allow_negative=False, is_selling=False, *args, **kwargs): """ is_selling : tell if the current action is a selling @@ -744,7 +769,9 @@ class StudentCard(models.Model): UID_SIZE = 14 - uid = models.CharField(_("uid"), max_length=14, unique=True) + uid = models.CharField( + _("uid"), max_length=14, unique=True, validators=[MinLengthValidator(4)] + ) customer = models.ForeignKey( Customer, related_name="student_cards", diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index aee9c309..521ec4c9 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -30,6 +30,16 @@ {{ user_mini_profile(customer.user) }} {{ user_subscription(customer.user) }}

{% trans %}Amount: {% endtrans %}{{ customer.amount }} €

+
+ {% csrf_token %} + + {% trans %}Add a student card{% endtrans %} + + {% if request.session['not_valid_student_card_uid'] %} +

{% trans %}This is not a valid student card UID{% endtrans %}

+ {% endif %} + +
{% trans %}Selling{% endtrans %}
diff --git a/counter/views.py b/counter/views.py index fd51b20f..439eb87d 100644 --- a/counter/views.py +++ b/counter/views.py @@ -380,6 +380,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session["too_young"] = False request.session["not_allowed"] = False request.session["no_age"] = False + request.session["not_valid_student_card_uid"] = False if self.object.type != "BAR": self.operator = request.user elif self.is_barman_price(): @@ -389,6 +390,8 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): if "add_product" in request.POST["action"]: self.add_product(request) + elif "add_student_card" in request.POST["action"]: + self.add_student_card(request) elif "del_product" in request.POST["action"]: self.del_product(request) elif "refill" in request.POST["action"]: @@ -525,6 +528,19 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session.modified = True return True + def add_student_card(self, request): + """ + Add a new student card on the customer account + """ + uid = request.POST["student_card_uid"] + uid = str(uid) + if len(uid) != StudentCard.UID_SIZE: + request.session["not_valid_student_card_uid"] = True + return False + + self.customer.add_student_card(uid, request, self.object) + return True + def del_product(self, request): """ Delete a product from the basket """ pid = str(request.POST["product_id"]) @@ -648,6 +664,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): kwargs["basket_total"] = self.sum_basket(self.request) kwargs["refill_form"] = self.refill_form or RefillForm() kwargs["categories"] = ProductType.objects.all() + kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE return kwargs From 616b7ccfc859dc5a9a75bc281d496f26870721cf Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Fri, 19 Oct 2018 19:25:55 +0200 Subject: [PATCH 4/9] Nice user interface and permission rework --- core/templates/core/user_preferences.jinja | 18 +++++ core/views/user.py | 3 + counter/models.py | 49 ++++++++----- counter/templates/counter/counter_click.jinja | 10 +++ counter/urls.py | 10 +++ counter/views.py | 72 ++++++++++++++++++- 6 files changed, 143 insertions(+), 19 deletions(-) diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 9525de20..97416f47 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -22,6 +22,24 @@

{% trans trombi=user.trombi_user.trombi %}You already choose to be in that Trombi: {{ trombi }}.{% endtrans %} {% trans %}Go to my Trombi tools{% endtrans %}

{% endif %} +{% if profile.customer %} +

{% trans %}Student cards{% endtrans %}

+

{% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}

+
+ {% csrf_token %} + {{ student_card_form.as_p() }} +

+
+{% if profile.customer.student_cards.exists() %} + +{% else %} +

{% trans %}No student cards registered.{% endtrans %}

+{% endif %} +{% endif %} {% endblock %} diff --git a/core/views/user.py b/core/views/user.py index fb9e6455..019cda0a 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -64,6 +64,7 @@ from core.views.forms import ( ) from core.models import User, SithFile, Preferences, Gift from subscription.models import Subscription +from counter.views import StudentCardForm from trombi.views import UserTrombiForm @@ -741,6 +742,8 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView): kwargs = super(UserPreferencesView, self).get_context_data(**kwargs) if not hasattr(self.object, "trombi_user"): kwargs["trombi_form"] = UserTrombiForm() + if self.object.customer: + kwargs["student_card_form"] = StudentCardForm() return kwargs diff --git a/counter/models.py b/counter/models.py index 0d373d36..693b277b 100644 --- a/counter/models.py +++ b/counter/models.py @@ -91,22 +91,7 @@ class Customer(models.Model): """ Add a new student card on the customer account """ - # If you are comming from a counter, only your connection to the counter is checked, not your right on the user to avoid wierd conflicts - if counter != None and ( - counter.type != "BAR" - or not ( - "counter_token" in request.session.keys() - and request.session["counter_token"] == counter.token - ) - or len(counter.get_barmen_list()) < 1 - ): - raise PermissionDenied - # If you are not comming from a counter, your permissions are checked - if not ( - request.user.id == self.user.id - or request.user.is_board_member - or request.user.is_root - ): + if not StudentCard.check_creation_permission(request, self, counter): raise PermissionDenied StudentCard(customer=self, uid=uid).save() @@ -769,6 +754,38 @@ class StudentCard(models.Model): UID_SIZE = 14 + @staticmethod + def is_valid(uid): + return len(uid) == StudentCard.UID_SIZE + + @staticmethod + def __comming_from_right_counter(request, counter): + return ( + counter.type == "BAR" + and "counter_token" in request.session.keys() + and request.session["counter_token"] == counter.token + and len(counter.get_barmen_list()) > 0 + ) + + @staticmethod + def __user_has_rights(customer, user): + return user.pk == customer.user.pk or user.is_board_member or user.is_root + + @staticmethod + def check_creation_permission(request, customer, counter=None): + """ + If you are comming from a counter, only your connection to the counter is checked, not your right on the user to avoid wierd conflicts + If you are not comming from a counter, your permissions are checked + """ + if counter: + return StudentCard.__comming_from_right_counter(request, counter) + return StudentCard.__user_has_rights(customer, request.user) + + def can_edit(self, obj): + if isinstance(obj, User): + return StudentCard.__user_has_rights(self.customer, obj) + return False + uid = models.CharField( _("uid"), max_length=14, unique=True, validators=[MinLengthValidator(4)] ) diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 521ec4c9..ed40e584 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -40,6 +40,16 @@ {% endif %} +
{% trans %}Registered cards{% endtrans %}
+ {% if customer.student_cards.exists() %} +
    + {% for card in customer.student_cards.all() %} +
  • {{ card.uid }}
  • + {% endfor %} +
+ {% else %} + {% trans %}No card registered{% endtrans %} + {% endif %}
{% trans %}Selling{% endtrans %}
diff --git a/counter/urls.py b/counter/urls.py index 9b99b604..fe3d90fa 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -56,6 +56,16 @@ urlpatterns = [ EticketPDFView.as_view(), name="eticket_pdf", ), + url( + r"^customer/(?P[0-9]+)/card/add$", + StudentCardFormView.as_view(), + name="add_student_card", + ), + url( + r"^customer/(?P[0-9]+)/card/delete/(?P[0-9]+)/$", + StudentCardDeleteView.as_view(), + name="delete_student_card", + ), url(r"^admin/(?P[0-9]+)$", CounterEditView.as_view(), name="admin"), url( r"^admin/(?P[0-9]+)/prop$", diff --git a/counter/views.py b/counter/views.py index 439eb87d..10a12eec 100644 --- a/counter/views.py +++ b/counter/views.py @@ -33,6 +33,7 @@ from django.views.generic.edit import ( DeleteView, ProcessFormView, FormMixin, + FormView, ) from django.forms.models import modelform_factory from django.forms import CheckboxSelectMultiple @@ -50,7 +51,7 @@ from datetime import date, timedelta, datetime from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from ajax_select import make_ajax_field -from core.views import CanViewMixin, TabedViewMixin +from core.views import CanViewMixin, TabedViewMixin, CanEditMixin from core.views.forms import LoginForm, SelectDate, SelectDateTime from core.models import User from subscription.models import Subscription @@ -100,6 +101,45 @@ class CounterAdminMixin(View): return super(CounterAdminMixin, self).dispatch(request, *args, **kwargs) +class StudentCardForm(forms.ModelForm): + """ + Form for adding student cards + Only used for user profile since CounterClick is to complicated + """ + + class Meta: + model = StudentCard + fields = ["uid"] + + def clean(self): + cleaned_data = super(StudentCardForm, self).clean() + uid = cleaned_data.get("uid") + if not StudentCard.is_valid(uid): + raise forms.ValidationError(_("This uid is invalid"), code="invalid") + return cleaned_data + + +class StudentCardDeleteView(DeleteView): + """ + View used to delete a card from a user + """ + + model = StudentCard + template_name = "core/delete_confirm.jinja" + pk_url_kwarg = "card_id" + + def dispatch(self, request, *args, **kwargs): + self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) + if not self.get_object().can_edit(self.customer.user): + raise PermissionDenied + return super(StudentCardDeleteView, self).dispatch(request, *args, **kwargs) + + def get_success_url(self, **kwargs): + return reverse_lazy( + "core:user_prefs", kwargs={"user_id": self.customer.user.pk} + ) + + class GetUserForm(forms.Form): """ The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, @@ -109,7 +149,9 @@ class GetUserForm(forms.Form): some nickname, first name, or last name (TODO) """ - code = forms.CharField(label="Code", max_length=10, required=False) + code = forms.CharField( + label="Code", max_length=StudentCard.UID_SIZE, required=False + ) id = AutoCompleteSelectField( "users", required=False, label=_("Select user"), help_text=None ) @@ -534,7 +576,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): """ uid = request.POST["student_card_uid"] uid = str(uid) - if len(uid) != StudentCard.UID_SIZE: + if not StudentCard.is_valid(uid): request.session["not_valid_student_card_uid"] = True return False @@ -1788,3 +1830,27 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie kwargs = super(CounterRefillingListView, self).get_context_data(**kwargs) kwargs["counter"] = self.counter return kwargs + + +class StudentCardFormView(FormView): + """ + Add a new student card + """ + + form_class = StudentCardForm + template_name = "core/create.jinja" + + def dispatch(self, request, *args, **kwargs): + self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) + return super(StudentCardFormView, self).dispatch(request, *args, **kwargs) + + def form_valid(self, form): + data = form.clean() + res = super(FormView, self).form_valid(form) + self.customer.add_student_card(data["uid"], self.request) + return res + + def get_success_url(self, **kwargs): + return reverse_lazy( + "core:user_prefs", kwargs={"user_id": self.customer.user.pk} + ) From 0ba0df0f294da8cceaf8315082fad5cc94cf19c8 Mon Sep 17 00:00:00 2001 From: klmp200 Date: Sun, 11 Nov 2018 22:56:51 +0100 Subject: [PATCH 5/9] Better handling of user rights for studentcards --- counter/models.py | 32 ++++---------------------------- counter/views.py | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/counter/models.py b/counter/models.py index 693b277b..bcc9388e 100644 --- a/counter/models.py +++ b/counter/models.py @@ -87,14 +87,6 @@ class Customer(models.Model): letter = random.choice(string.ascii_lowercase) return number + letter - def add_student_card(self, uid, request, counter=None): - """ - Add a new student card on the customer account - """ - if not StudentCard.check_creation_permission(request, self, counter): - raise PermissionDenied - StudentCard(customer=self, uid=uid).save() - def save(self, allow_negative=False, is_selling=False, *args, **kwargs): """ is_selling : tell if the current action is a selling @@ -756,34 +748,18 @@ class StudentCard(models.Model): @staticmethod def is_valid(uid): - return len(uid) == StudentCard.UID_SIZE - - @staticmethod - def __comming_from_right_counter(request, counter): return ( - counter.type == "BAR" - and "counter_token" in request.session.keys() - and request.session["counter_token"] == counter.token - and len(counter.get_barmen_list()) > 0 + len(uid) == StudentCard.UID_SIZE + and not StudentCard.objects.filter(uid=uid).exists() ) @staticmethod - def __user_has_rights(customer, user): + def can_create(customer, user): return user.pk == customer.user.pk or user.is_board_member or user.is_root - @staticmethod - def check_creation_permission(request, customer, counter=None): - """ - If you are comming from a counter, only your connection to the counter is checked, not your right on the user to avoid wierd conflicts - If you are not comming from a counter, your permissions are checked - """ - if counter: - return StudentCard.__comming_from_right_counter(request, counter) - return StudentCard.__user_has_rights(customer, request.user) - def can_edit(self, obj): if isinstance(obj, User): - return StudentCard.__user_has_rights(self.customer, obj) + return StudentCard.can_create(self.customer, obj) return False uid = models.CharField( diff --git a/counter/views.py b/counter/views.py index 10a12eec..2f3a653d 100644 --- a/counter/views.py +++ b/counter/views.py @@ -119,7 +119,7 @@ class StudentCardForm(forms.ModelForm): return cleaned_data -class StudentCardDeleteView(DeleteView): +class StudentCardDeleteView(DeleteView, CanEditMixin): """ View used to delete a card from a user """ @@ -130,8 +130,6 @@ class StudentCardDeleteView(DeleteView): def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) - if not self.get_object().can_edit(self.customer.user): - raise PermissionDenied return super(StudentCardDeleteView, self).dispatch(request, *args, **kwargs) def get_success_url(self, **kwargs): @@ -580,7 +578,15 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): request.session["not_valid_student_card_uid"] = True return False - self.customer.add_student_card(uid, request, self.object) + if not ( + self.object.type == "BAR" + and "counter_token" in request.session.keys() + and request.session["counter_token"] == self.object.token + and len(self.object.get_barmen_list()) > 0 + ): + raise PermissionDenied + + StudentCard(customer=self.customer, uid=uid).save() return True def del_product(self, request): @@ -1842,12 +1848,14 @@ class StudentCardFormView(FormView): def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) + if not StudentCard.can_create(self.customer, request.user): + raise PermissionDenied return super(StudentCardFormView, self).dispatch(request, *args, **kwargs) def form_valid(self, form): data = form.clean() res = super(FormView, self).form_valid(form) - self.customer.add_student_card(data["uid"], self.request) + StudentCard(customer=self.customer, uid=data["uid"]).save() return res def get_success_url(self, **kwargs): From e1ffdbe3f9c3f99e2fe53cf24e2ba63bc0a0cbf3 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Tue, 14 May 2019 13:18:49 +0200 Subject: [PATCH 6/9] Translations on student card feature --- locale/fr/LC_MESSAGES/django.po | 591 +++++++++++++++++--------------- 1 file changed, 321 insertions(+), 270 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 8eab8c71..f32db813 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-05-09 11:33+0200\n" +"POT-Creation-Date: 2019-05-20 17:55+0200\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Skia \n" "Language-Team: AE info \n" @@ -18,8 +18,8 @@ msgstr "" #: accounting/models.py:61 accounting/models.py:110 accounting/models.py:138 #: accounting/models.py:203 club/models.py:48 com/models.py:231 -#: com/models.py:248 counter/models.py:119 counter/models.py:147 -#: counter/models.py:209 forum/models.py:58 launderette/models.py:38 +#: com/models.py:248 counter/models.py:121 counter/models.py:149 +#: counter/models.py:211 forum/models.py:58 launderette/models.py:38 #: launderette/models.py:90 launderette/models.py:125 stock/models.py:40 #: stock/models.py:60 stock/models.py:100 stock/models.py:127 msgid "name" @@ -66,8 +66,8 @@ msgid "account number" msgstr "numero de compte" #: accounting/models.py:113 accounting/models.py:139 club/models.py:266 -#: com/models.py:75 com/models.py:219 com/models.py:252 counter/models.py:164 -#: counter/models.py:210 trombi/models.py:205 +#: com/models.py:75 com/models.py:219 com/models.py:252 counter/models.py:166 +#: counter/models.py:212 trombi/models.py:205 msgid "club" msgstr "club" @@ -88,12 +88,12 @@ msgstr "Compte club" msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:201 club/models.py:268 counter/models.py:589 +#: accounting/models.py:201 club/models.py:268 counter/models.py:591 #: election/models.py:18 launderette/models.py:180 msgid "start date" msgstr "date de début" -#: accounting/models.py:202 club/models.py:269 counter/models.py:590 +#: accounting/models.py:202 club/models.py:269 counter/models.py:592 #: election/models.py:19 msgid "end date" msgstr "date de fin" @@ -106,8 +106,8 @@ msgstr "est fermé" msgid "club account" msgstr "compte club" -#: accounting/models.py:208 accounting/models.py:268 counter/models.py:54 -#: counter/models.py:346 +#: accounting/models.py:208 accounting/models.py:268 counter/models.py:56 +#: counter/models.py:348 msgid "amount" msgstr "montant" @@ -128,19 +128,19 @@ msgid "journal" msgstr "classeur" #: accounting/models.py:269 core/models.py:809 core/models.py:1345 -#: core/models.py:1391 core/models.py:1420 counter/models.py:351 -#: counter/models.py:444 counter/models.py:613 eboutic/models.py:42 +#: core/models.py:1391 core/models.py:1420 counter/models.py:353 +#: counter/models.py:446 counter/models.py:615 eboutic/models.py:42 #: eboutic/models.py:85 forum/models.py:298 forum/models.py:391 #: stock/models.py:99 msgid "date" msgstr "date" -#: accounting/models.py:270 counter/models.py:121 counter/models.py:614 +#: accounting/models.py:270 counter/models.py:123 counter/models.py:616 #: stock/models.py:102 msgid "comment" msgstr "commentaire" -#: accounting/models.py:272 counter/models.py:353 counter/models.py:446 +#: accounting/models.py:272 counter/models.py:355 counter/models.py:448 #: subscription/models.py:64 msgid "payment method" msgstr "méthode de paiement" @@ -166,7 +166,7 @@ msgid "accounting type" msgstr "type comptable" #: accounting/models.py:304 accounting/models.py:450 accounting/models.py:485 -#: accounting/models.py:519 core/models.py:1419 counter/models.py:410 +#: accounting/models.py:519 core/models.py:1419 counter/models.py:412 msgid "label" msgstr "étiquette" @@ -210,7 +210,7 @@ msgstr "Utilisateur" msgid "Club" msgstr "Club" -#: accounting/models.py:315 core/views/user.py:295 +#: accounting/models.py:315 core/views/user.py:296 msgid "Account" msgstr "Compte" @@ -218,7 +218,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:317 sith/settings.py:378 +#: accounting/models.py:317 sith/settings.py:375 #: stock/templates/stock/shopping_list_items.jinja:37 msgid "Other" msgstr "Autre" @@ -265,7 +265,7 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:442 counter/models.py:157 +#: accounting/models.py:442 counter/models.py:159 msgid "code" msgstr "code" @@ -345,7 +345,8 @@ msgstr "Compte en banque : " #: accounting/templates/accounting/club_account_details.jinja:60 #: accounting/templates/accounting/label_list.jinja:26 #: club/templates/club/club_sellings.jinja:50 -#: club/templates/club/mailing.jinja:25 club/templates/club/mailing.jinja:43 +#: club/templates/club/mailing.jinja:16 club/templates/club/mailing.jinja:25 +#: club/templates/club/mailing.jinja:43 #: com/templates/com/mailing_admin.jinja:19 #: com/templates/com/news_admin_list.jinja:41 #: com/templates/com/news_admin_list.jinja:70 @@ -367,6 +368,7 @@ msgstr "Compte en banque : " #: core/templates/core/user_account_detail.jinja:38 #: core/templates/core/user_detail.jinja:178 #: core/templates/core/user_edit.jinja:19 +#: core/templates/core/user_preferences.jinja:36 #: counter/templates/counter/last_ops.jinja:29 #: counter/templates/counter/last_ops.jinja:59 #: election/templates/election/election_detail.jinja:271 @@ -386,7 +388,7 @@ msgid "Delete" msgstr "Supprimer" #: accounting/templates/accounting/bank_account_details.jinja:18 -#: club/views.py:78 core/views/user.py:205 sas/templates/sas/picture.jinja:86 +#: club/views.py:78 core/views/user.py:206 sas/templates/sas/picture.jinja:86 msgid "Infos" msgstr "Infos" @@ -420,7 +422,7 @@ msgstr "Nouveau compte club" #: com/templates/com/weekmail.jinja:61 core/templates/core/file.jinja:38 #: core/templates/core/group_list.jinja:24 core/templates/core/page.jinja:35 #: core/templates/core/poster_list.jinja:40 -#: core/templates/core/user_tools.jinja:42 core/views/user.py:237 +#: core/templates/core/user_tools.jinja:42 core/views/user.py:238 #: counter/templates/counter/cash_summary_list.jinja:53 #: counter/templates/counter/counter_list.jinja:17 #: counter/templates/counter/counter_list.jinja:33 @@ -658,7 +660,7 @@ msgid "Done" msgstr "Effectuées" #: accounting/templates/accounting/journal_details.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:1091 +#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:1162 #: trombi/templates/trombi/comment.jinja:4 #: trombi/templates/trombi/comment.jinja:8 #: trombi/templates/trombi/user_tools.jinja:50 @@ -719,7 +721,7 @@ msgstr "Nature de l'opération" #: accounting/templates/accounting/journal_statement_nature.jinja:26 #: accounting/templates/accounting/journal_statement_nature.jinja:45 #: club/templates/club/club_sellings.jinja:14 -#: counter/templates/counter/counter_click.jinja:70 +#: counter/templates/counter/counter_click.jinja:90 #: counter/templates/counter/counter_main.jinja:28 #: eboutic/templates/eboutic/eboutic_main.jinja:34 msgid "Total: " @@ -777,6 +779,7 @@ msgstr "Opération liée : " #: core/templates/core/user_godfathers.jinja:41 #: core/templates/core/user_preferences.jinja:12 #: core/templates/core/user_preferences.jinja:19 +#: core/templates/core/user_preferences.jinja:31 #: counter/templates/counter/cash_register_summary.jinja:22 #: forum/templates/forum/reply.jinja:33 #: subscription/templates/subscription/subscription.jinja:25 @@ -893,11 +896,11 @@ msgstr "Opérations sans étiquette" msgid "Refound this account" msgstr "Rembourser ce compte" -#: club/forms.py:60 club/forms.py:197 +#: club/forms.py:60 club/forms.py:188 msgid "Users to add" msgstr "Utilisateurs à ajouter" -#: club/forms.py:61 club/forms.py:198 core/views/group.py:63 +#: club/forms.py:61 club/forms.py:189 core/views/group.py:63 msgid "Search users to add (one or more)." msgstr "Recherche les utilisateurs à ajouter (un ou plus)." @@ -924,67 +927,67 @@ msgstr "Retirer" msgid "Action" msgstr "Action" -#: club/forms.py:122 +#: club/forms.py:115 club/tests.py:575 #, fuzzy #| msgid "This field is required." msgid "This field is required" msgstr "Ce champ est obligatoire." -#: club/forms.py:134 club/forms.py:259 +#: club/forms.py:127 club/forms.py:250 club/tests.py:587 msgid "One of the selected users doesn't exist" msgstr "Un des utilisateurs sélectionné n'existe pas" -#: club/forms.py:138 +#: club/forms.py:131 club/tests.py:605 #, 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 +#: club/forms.py:142 #, fuzzy #| msgid "This field is required." msgid "An action is required" msgstr "Ce champ est obligatoire." -#: club/forms.py:162 +#: club/forms.py:153 club/tests.py:564 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 +#: club/forms.py:163 counter/views.py:1552 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:178 com/views.py:85 com/views.py:221 counter/views.py:1487 +#: club/forms.py:169 com/views.py:85 com/views.py:221 counter/views.py:1558 #: 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 +#: club/forms.py:174 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 +#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:214 msgid "Counter" msgstr "Comptoir" -#: club/forms.py:241 club/templates/club/club_members.jinja:21 +#: club/forms.py:232 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 +#: club/forms.py:254 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 +#: club/forms.py:258 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 +#: club/forms.py:279 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 +#: club/forms.py:290 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" @@ -1032,7 +1035,7 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs" msgid "A club with that unix_name already exists" msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:260 counter/models.py:585 counter/models.py:608 +#: club/models.py:260 counter/models.py:587 counter/models.py:610 #: eboutic/models.py:40 eboutic/models.py:83 election/models.py:180 #: launderette/models.py:134 launderette/models.py:190 sas/models.py:239 #: trombi/models.py:203 @@ -1044,8 +1047,8 @@ msgstr "nom d'utilisateur" msgid "role" msgstr "rôle" -#: club/models.py:276 core/models.py:73 counter/models.py:120 -#: counter/models.py:148 election/models.py:15 election/models.py:117 +#: club/models.py:276 core/models.py:73 counter/models.py:122 +#: counter/models.py:150 election/models.py:15 election/models.py:117 #: election/models.py:182 forum/models.py:59 forum/models.py:231 msgid "description" msgstr "description" @@ -1083,7 +1086,7 @@ msgstr "Liste de diffusion" msgid "At least user or email is required" msgstr "Au moins un utilisateur ou un email est nécessaire" -#: club/models.py:435 +#: club/models.py:435 club/tests.py:633 msgid "This email is already suscribed in this mailing" msgstr "Cet email est déjà abonné à cette mailing" @@ -1325,7 +1328,7 @@ msgstr "Ajouter à la mailing liste" msgid "New mailing" msgstr "Nouvelle liste de diffusion" -#: club/templates/club/mailing.jinja:109 +#: club/templates/club/mailing.jinja:105 msgid "Create mailing list" msgstr "Créer une liste de diffusion" @@ -1349,7 +1352,7 @@ msgstr "Anciens membres" msgid "History" msgstr "Historique" -#: club/views.py:115 core/templates/core/base.jinja:121 core/views/user.py:228 +#: club/views.py:115 core/templates/core/base.jinja:117 core/views/user.py:229 #: sas/templates/sas/picture.jinja:95 trombi/views.py:60 msgid "Tools" msgstr "Outils" @@ -1373,7 +1376,7 @@ msgid "Props" msgstr "Propriétés" #: club/views.py:335 core/templates/core/user_stats.jinja:27 -#: counter/views.py:1635 +#: counter/views.py:1706 msgid "Product" msgstr "Produit" @@ -1569,7 +1572,7 @@ msgstr "Type" #: com/templates/com/news_admin_list.jinja:286 #: com/templates/com/weekmail.jinja:19 com/templates/com/weekmail.jinja:48 #: forum/templates/forum/forum.jinja:24 forum/templates/forum/forum.jinja:43 -#: forum/templates/forum/main.jinja:27 forum/views.py:240 +#: forum/templates/forum/main.jinja:27 forum/views.py:243 msgid "Title" msgstr "Titre" @@ -1639,7 +1642,7 @@ msgid "Calls to moderate" msgstr "Appels à modérer" #: com/templates/com/news_admin_list.jinja:242 -#: core/templates/core/base.jinja:173 +#: core/templates/core/base.jinja:169 msgid "Events" msgstr "Événements" @@ -2350,7 +2353,7 @@ msgstr "403, Non autorisé" msgid "404, Not Found" msgstr "404. Non trouvé" -#: core/templates/core/500.jinja:12 +#: core/templates/core/500.jinja:8 msgid "500, Server Error" msgstr "500, Erreur Serveur" @@ -2358,24 +2361,24 @@ msgstr "500, Erreur Serveur" msgid "Welcome!" msgstr "Bienvenue!" -#: core/templates/core/base.jinja:53 +#: core/templates/core/base.jinja:49 msgid "Username" msgstr "Nom d'utilisateur" -#: core/templates/core/base.jinja:55 +#: core/templates/core/base.jinja:51 msgid "Password" msgstr "Mot de passe" -#: core/templates/core/base.jinja:57 core/templates/core/login.jinja:4 +#: core/templates/core/base.jinja:53 core/templates/core/login.jinja:4 #: core/templates/core/password_reset_complete.jinja:5 msgid "Login" msgstr "Connexion" -#: core/templates/core/base.jinja:59 core/templates/core/register.jinja:18 +#: core/templates/core/base.jinja:55 core/templates/core/register.jinja:18 msgid "Register" msgstr "S'enregister" -#: core/templates/core/base.jinja:83 core/templates/core/base.jinja:84 +#: core/templates/core/base.jinja:79 core/templates/core/base.jinja:80 #: forum/templates/forum/macros.jinja:171 #: forum/templates/forum/macros.jinja:175 #: matmat/templates/matmat/search_form.jinja:37 @@ -2384,64 +2387,64 @@ msgstr "S'enregister" msgid "Search" msgstr "Recherche" -#: core/templates/core/base.jinja:110 +#: core/templates/core/base.jinja:106 msgid "View more" msgstr "Voir plus" -#: core/templates/core/base.jinja:114 +#: core/templates/core/base.jinja:110 #: forum/templates/forum/last_unread.jinja:17 msgid "Mark all as read" msgstr "Marquer tout commme lu" -#: core/templates/core/base.jinja:124 +#: core/templates/core/base.jinja:120 msgid "Logout" msgstr "Déconnexion" -#: core/templates/core/base.jinja:157 +#: core/templates/core/base.jinja:153 msgid "Main" msgstr "Accueil" -#: core/templates/core/base.jinja:159 +#: core/templates/core/base.jinja:155 msgid "Associations & Clubs" msgstr "Associations & Clubs" -#: core/templates/core/base.jinja:163 +#: core/templates/core/base.jinja:159 msgid "AE" msgstr "L'AE" -#: core/templates/core/base.jinja:164 +#: core/templates/core/base.jinja:160 msgid "AE's clubs" msgstr "Les clubs de L'AE" -#: core/templates/core/base.jinja:165 +#: core/templates/core/base.jinja:161 msgid "BdF" msgstr "Le BdF" -#: core/templates/core/base.jinja:166 +#: core/templates/core/base.jinja:162 msgid "BDS" msgstr "Le BDS" -#: core/templates/core/base.jinja:167 +#: core/templates/core/base.jinja:163 msgid "CETU" msgstr "Le CETU" -#: core/templates/core/base.jinja:168 +#: core/templates/core/base.jinja:164 msgid "Doceo" msgstr "Doceo" -#: core/templates/core/base.jinja:169 +#: core/templates/core/base.jinja:165 msgid "Positions" msgstr "Postes à pourvoir" -#: core/templates/core/base.jinja:177 +#: core/templates/core/base.jinja:173 msgid "Calendar" msgstr "Calendrier" -#: core/templates/core/base.jinja:178 +#: core/templates/core/base.jinja:174 msgid "Big event" msgstr "Grandes Activités" -#: core/templates/core/base.jinja:181 +#: core/templates/core/base.jinja:177 #: forum/templates/forum/favorite_topics.jinja:14 #: forum/templates/forum/last_unread.jinja:14 #: forum/templates/forum/macros.jinja:90 forum/templates/forum/main.jinja:6 @@ -2450,85 +2453,85 @@ msgstr "Grandes Activités" msgid "Forum" msgstr "Forum" -#: core/templates/core/base.jinja:182 +#: core/templates/core/base.jinja:178 msgid "Gallery" msgstr "Photos" -#: core/templates/core/base.jinja:183 counter/models.py:217 +#: core/templates/core/base.jinja:179 counter/models.py:219 #: counter/templates/counter/counter_list.jinja:11 #: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:24 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:8 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:377 sith/settings.py:385 +#: sith/settings.py:374 sith/settings.py:382 msgid "Eboutic" msgstr "Eboutic" -#: core/templates/core/base.jinja:185 +#: core/templates/core/base.jinja:181 msgid "Services" msgstr "Services" -#: core/templates/core/base.jinja:189 +#: core/templates/core/base.jinja:185 msgid "Matmatronch" msgstr "Matmatronch" -#: core/templates/core/base.jinja:190 launderette/models.py:44 +#: core/templates/core/base.jinja:186 launderette/models.py:44 #: launderette/templates/launderette/launderette_book.jinja:5 #: launderette/templates/launderette/launderette_book_choose.jinja:4 #: launderette/templates/launderette/launderette_main.jinja:4 msgid "Launderette" msgstr "Laverie" -#: core/templates/core/base.jinja:191 core/templates/core/file.jinja:20 +#: core/templates/core/base.jinja:187 core/templates/core/file.jinja:20 #: core/views/files.py:86 msgid "Files" msgstr "Fichiers" -#: core/templates/core/base.jinja:196 +#: core/templates/core/base.jinja:192 msgid "My Benefits" msgstr "Mes Avantages" -#: core/templates/core/base.jinja:200 +#: core/templates/core/base.jinja:196 msgid "Sponsors" msgstr "Partenaires" -#: core/templates/core/base.jinja:201 +#: core/templates/core/base.jinja:197 msgid "Subscriber benefits" msgstr "Les avantages cotisants" -#: core/templates/core/base.jinja:205 +#: core/templates/core/base.jinja:201 msgid "Help" msgstr "Aide" -#: core/templates/core/base.jinja:209 +#: core/templates/core/base.jinja:205 msgid "FAQ" msgstr "FAQ" -#: core/templates/core/base.jinja:210 core/templates/core/base.jinja:252 +#: core/templates/core/base.jinja:206 core/templates/core/base.jinja:248 msgid "Contacts" msgstr "Contacts" -#: core/templates/core/base.jinja:211 +#: core/templates/core/base.jinja:207 msgid "Wiki" msgstr "Wiki" -#: core/templates/core/base.jinja:253 +#: core/templates/core/base.jinja:249 msgid "Legal notices" msgstr "Mentions légales" -#: core/templates/core/base.jinja:254 +#: core/templates/core/base.jinja:250 msgid "Intellectual property" msgstr "Propriété intellectuelle" -#: core/templates/core/base.jinja:255 +#: core/templates/core/base.jinja:251 msgid "Help & Documentation" msgstr "Aide & Documentation" -#: core/templates/core/base.jinja:256 +#: core/templates/core/base.jinja:252 msgid "R&D" msgstr "R&D" -#: core/templates/core/base.jinja:258 +#: core/templates/core/base.jinja:254 msgid "Site made by good people" msgstr "Site réalisé par des gens bons" @@ -2557,7 +2560,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:14 #: core/templates/core/file_delete_confirm.jinja:14 -#: counter/templates/counter/counter_click.jinja:93 +#: counter/templates/counter/counter_click.jinja:113 msgid "Cancel" msgstr "Annuler" @@ -2961,7 +2964,7 @@ msgstr "Résultat de la recherche" msgid "Users" msgstr "Utilisateurs" -#: core/templates/core/search.jinja:18 core/views/user.py:256 +#: core/templates/core/search.jinja:18 core/views/user.py:257 #: counter/templates/counter/stats.jinja:17 msgid "Clubs" msgstr "Clubs" @@ -3019,7 +3022,7 @@ msgid "Eboutic invoices" msgstr "Facture eboutic" #: core/templates/core/user_account.jinja:57 -#: core/templates/core/user_tools.jinja:36 counter/views.py:745 +#: core/templates/core/user_tools.jinja:36 counter/views.py:816 msgid "Etickets" msgstr "Etickets" @@ -3192,8 +3195,8 @@ msgstr "Parrains de %(user_name)s" msgid "Show family picture" msgstr "Voir une image de la famille" -#: core/templates/core/user_godfathers.jinja:12 core/views/user.py:214 -#: core/views/user.py:486 +#: core/templates/core/user_godfathers.jinja:12 core/views/user.py:215 +#: core/views/user.py:487 msgid "Godfathers" msgstr "Parrains" @@ -3206,7 +3209,7 @@ msgstr "Voir l'arbre des ancêtres" msgid "No godfathers" msgstr "Pas de parrains" -#: core/templates/core/user_godfathers.jinja:25 core/views/user.py:484 +#: core/templates/core/user_godfathers.jinja:25 core/views/user.py:485 msgid "Godchildren" msgstr "Fillots" @@ -3262,7 +3265,7 @@ msgid "%(user_name)s's pictures" msgstr "Photos de %(user_name)s" #: core/templates/core/user_preferences.jinja:4 -#: core/templates/core/user_preferences.jinja:8 core/views/user.py:246 +#: core/templates/core/user_preferences.jinja:8 core/views/user.py:247 msgid "Preferences" msgstr "Préférences" @@ -3279,6 +3282,25 @@ msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s." msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" +#: core/templates/core/user_preferences.jinja:26 +msgid "Student cards" +msgstr "Cartes étudiante" + +#: core/templates/core/user_preferences.jinja:27 +msgid "" +"You can add a card by asking at a counter or add it yourself here. If you " +"want to manually add a student card yourself, you'll need a NFC reader. We " +"store the UID of the card which is 14 characters long." +msgstr "" +"Vous pouvez ajouter une carte en demandant à un comptoir ou en l'ajoutant " +"vous même ici. Si vous voulez l'ajouter manuellement par vous même, vous " +"aurez besoin d'un lecteur NFC. Nous enregistrons l'UID de la carte qui fait " +"14 caractères de long." + +#: core/templates/core/user_preferences.jinja:40 +msgid "No student cards registered." +msgstr "Aucune cartes étudiante enregistré." + #: core/templates/core/user_stats.jinja:4 #, python-format msgid "%(user_name)s's stats" @@ -3309,7 +3331,7 @@ msgstr "Outils utilisateurs" msgid "Sith management" msgstr "Gestion de Sith" -#: core/templates/core/user_tools.jinja:14 core/views/user.py:266 +#: core/templates/core/user_tools.jinja:14 core/views/user.py:267 msgid "Groups" msgstr "Groupes" @@ -3332,8 +3354,8 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:28 counter/views.py:715 -#: counter/views.py:923 +#: core/templates/core/user_tools.jinja:28 counter/views.py:786 +#: counter/views.py:994 msgid "Counters" msgstr "Comptoirs" @@ -3350,16 +3372,16 @@ msgid "Product types management" msgstr "Gestion des types de produit" #: core/templates/core/user_tools.jinja:34 -#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:735 +#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:806 msgid "Cash register summaries" msgstr "Relevés de caisse" #: core/templates/core/user_tools.jinja:35 -#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:740 +#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:811 msgid "Invoices call" msgstr "Appels à facture" -#: core/templates/core/user_tools.jinja:43 core/views/user.py:286 +#: core/templates/core/user_tools.jinja:43 core/views/user.py:287 #: counter/templates/counter/counter_list.jinja:18 #: counter/templates/counter/counter_list.jinja:34 #: counter/templates/counter/counter_list.jinja:56 @@ -3581,7 +3603,7 @@ msgstr "Parrain" msgid "Godchild" msgstr "Fillot" -#: core/views/forms.py:358 counter/views.py:113 trombi/views.py:141 +#: core/views/forms.py:358 counter/views.py:154 trombi/views.py:141 msgid "Select user" msgstr "Choisir un utilisateur" @@ -3603,16 +3625,16 @@ msgstr "Utilisateurs à retirer du groupe" msgid "Users to add to group" msgstr "Utilisateurs à ajouter au groupe" -#: core/views/user.py:223 trombi/templates/trombi/export.jinja:25 +#: core/views/user.py:224 trombi/templates/trombi/export.jinja:25 #: trombi/templates/trombi/user_profile.jinja:11 msgid "Pictures" msgstr "Photos" -#: core/views/user.py:488 +#: core/views/user.py:489 msgid "Family" msgstr "Famille" -#: core/views/user.py:630 +#: core/views/user.py:631 msgid "User already has a profile picture" msgstr "L'utilisateur a déjà une photo de profil" @@ -3620,143 +3642,143 @@ msgstr "L'utilisateur a déjà une photo de profil" msgid "Ecocup regularization" msgstr "Régularization des ecocups" -#: counter/models.py:53 +#: counter/models.py:55 msgid "account id" msgstr "numéro de compte" -#: counter/models.py:55 +#: counter/models.py:57 msgid "recorded product" msgstr "produits consignés" -#: counter/models.py:58 +#: counter/models.py:60 msgid "customer" msgstr "client" -#: counter/models.py:59 +#: counter/models.py:61 msgid "customers" msgstr "clients" -#: counter/models.py:95 counter/templates/counter/counter_click.jinja:48 -#: counter/templates/counter/counter_click.jinja:82 +#: counter/models.py:97 counter/templates/counter/counter_click.jinja:68 +#: counter/templates/counter/counter_click.jinja:102 msgid "Not enough money" msgstr "Solde insuffisant" -#: counter/models.py:125 counter/models.py:152 +#: counter/models.py:127 counter/models.py:154 msgid "product type" msgstr "type du produit" -#: counter/models.py:158 +#: counter/models.py:160 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:159 +#: counter/models.py:161 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:160 +#: counter/models.py:162 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:162 +#: counter/models.py:164 msgid "icon" msgstr "icône" -#: counter/models.py:165 +#: counter/models.py:167 msgid "limit age" msgstr "âge limite" -#: counter/models.py:166 +#: counter/models.py:168 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:170 +#: counter/models.py:172 msgid "parent product" msgstr "produit parent" -#: counter/models.py:176 +#: counter/models.py:178 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:178 election/models.py:52 +#: counter/models.py:180 election/models.py:52 msgid "archived" msgstr "archivé" -#: counter/models.py:181 counter/models.py:700 +#: counter/models.py:183 counter/models.py:702 msgid "product" msgstr "produit" -#: counter/models.py:212 +#: counter/models.py:214 msgid "products" msgstr "produits" -#: counter/models.py:215 +#: counter/models.py:217 msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:217 +#: counter/models.py:219 msgid "Bar" msgstr "Bar" -#: counter/models.py:217 +#: counter/models.py:219 msgid "Office" msgstr "Bureau" -#: counter/models.py:220 +#: counter/models.py:222 msgid "sellers" msgstr "vendeurs" -#: counter/models.py:228 launderette/models.py:188 +#: counter/models.py:230 launderette/models.py:188 msgid "token" msgstr "jeton" -#: counter/models.py:231 counter/models.py:587 counter/models.py:611 +#: counter/models.py:233 counter/models.py:589 counter/models.py:613 #: launderette/models.py:40 stock/models.py:42 msgid "counter" msgstr "comptoir" -#: counter/models.py:359 +#: counter/models.py:361 msgid "bank" msgstr "banque" -#: counter/models.py:361 counter/models.py:451 +#: counter/models.py:363 counter/models.py:453 msgid "is validated" msgstr "est validé" -#: counter/models.py:364 +#: counter/models.py:366 msgid "refilling" msgstr "rechargement" -#: counter/models.py:428 eboutic/models.py:146 +#: counter/models.py:430 eboutic/models.py:146 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:429 counter/models.py:687 eboutic/models.py:147 +#: counter/models.py:431 counter/models.py:689 eboutic/models.py:147 msgid "quantity" msgstr "quantité" -#: counter/models.py:448 +#: counter/models.py:450 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:448 sith/settings.py:370 sith/settings.py:375 -#: sith/settings.py:393 +#: counter/models.py:450 sith/settings.py:367 sith/settings.py:372 +#: sith/settings.py:390 msgid "Credit card" msgstr "Carte bancaire" -#: counter/models.py:454 +#: counter/models.py:456 msgid "selling" msgstr "vente" -#: counter/models.py:477 +#: counter/models.py:479 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:478 +#: counter/models.py:480 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:480 counter/models.py:494 +#: counter/models.py:482 counter/models.py:496 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3765,54 +3787,62 @@ msgstr "" "Vous avez acheté un Eticket pour l'événement %(event)s.\n" "Vous pouvez le télécharger sur cette page: %(url)s" -#: counter/models.py:591 +#: counter/models.py:593 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:594 +#: counter/models.py:596 msgid "permanency" msgstr "permanence" -#: counter/models.py:615 +#: counter/models.py:617 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:618 +#: counter/models.py:620 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:684 +#: counter/models.py:686 msgid "cash summary" msgstr "relevé" -#: counter/models.py:686 +#: counter/models.py:688 msgid "value" msgstr "valeur" -#: counter/models.py:688 +#: counter/models.py:690 msgid "check" msgstr "chèque" -#: counter/models.py:691 +#: counter/models.py:693 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:703 +#: counter/models.py:705 msgid "banner" msgstr "bannière" -#: counter/models.py:705 +#: counter/models.py:707 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:707 +#: counter/models.py:709 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:709 +#: counter/models.py:711 msgid "secret" msgstr "secret" +#: counter/models.py:766 +msgid "uid" +msgstr "uid" + +#: counter/models.py:771 +msgid "student cards" +msgstr "cartes étudiante" + #: counter/templates/counter/activity.jinja:5 #: counter/templates/counter/activity.jinja:9 #, python-format @@ -3861,7 +3891,7 @@ msgstr "Liste des relevés de caisse" msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:1092 +#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:1163 msgid "Emptied" msgstr "Coffre vidé" @@ -3873,28 +3903,17 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:35 -#: launderette/templates/launderette/launderette_admin.jinja:8 -msgid "Selling" -msgstr "Vente" +#: counter/templates/counter/counter_click.jinja:36 +msgid "Add a student card" +msgstr "Ajouter une carte étudiante" #: counter/templates/counter/counter_click.jinja:39 -#: counter/templates/counter/counter_click.jinja:73 -msgid "Too young for that product" -msgstr "Trop jeune pour ce produit" +msgid "This is not a valid student card UID" +msgstr "Ce n'est pas un UID de carte étudiante valide" -#: counter/templates/counter/counter_click.jinja:42 -#: counter/templates/counter/counter_click.jinja:76 -msgid "Not allowed for that product" -msgstr "Non autorisé pour ce produit" - -#: counter/templates/counter/counter_click.jinja:45 -#: counter/templates/counter/counter_click.jinja:79 -msgid "No date of birth provided" -msgstr "Pas de date de naissance renseignée" - -#: counter/templates/counter/counter_click.jinja:55 -#: counter/templates/counter/counter_click.jinja:103 +#: counter/templates/counter/counter_click.jinja:41 +#: counter/templates/counter/counter_click.jinja:75 +#: counter/templates/counter/counter_click.jinja:123 #: counter/templates/counter/invoices_call.jinja:16 #: launderette/templates/launderette/launderette_admin.jinja:35 #: launderette/templates/launderette/launderette_click.jinja:13 @@ -3903,17 +3922,45 @@ msgstr "Pas de date de naissance renseignée" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:57 +#: counter/templates/counter/counter_click.jinja:43 +msgid "Registered cards" +msgstr "Cartes enregistrées" + +#: counter/templates/counter/counter_click.jinja:51 +msgid "No card registered" +msgstr "Aucune carte enregistrée" + +#: counter/templates/counter/counter_click.jinja:55 +#: launderette/templates/launderette/launderette_admin.jinja:8 +msgid "Selling" +msgstr "Vente" + +#: counter/templates/counter/counter_click.jinja:59 +#: counter/templates/counter/counter_click.jinja:93 +msgid "Too young for that product" +msgstr "Trop jeune pour ce produit" + +#: counter/templates/counter/counter_click.jinja:62 +#: counter/templates/counter/counter_click.jinja:96 +msgid "Not allowed for that product" +msgstr "Non autorisé pour ce produit" + +#: counter/templates/counter/counter_click.jinja:65 +#: counter/templates/counter/counter_click.jinja:99 +msgid "No date of birth provided" +msgstr "Pas de date de naissance renseignée" + +#: counter/templates/counter/counter_click.jinja:77 #: eboutic/templates/eboutic/eboutic_main.jinja:27 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:11 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:88 +#: counter/templates/counter/counter_click.jinja:108 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:97 +#: counter/templates/counter/counter_click.jinja:117 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" @@ -4080,125 +4127,129 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:130 +#: counter/views.py:118 +msgid "This uid is invalid" +msgstr "Cet UID est invalide" + +#: counter/views.py:176 msgid "User not found" msgstr "Utilisateur non trouvé" -#: counter/views.py:187 +#: counter/views.py:233 msgid "Cash summary" msgstr "Relevé de caisse" -#: counter/views.py:201 +#: counter/views.py:247 msgid "Last operations" msgstr "Dernières opérations" -#: counter/views.py:216 +#: counter/views.py:262 msgid "Take items from stock" msgstr "Prendre des éléments du stock" -#: counter/views.py:269 +#: counter/views.py:315 msgid "Bad credentials" msgstr "Mauvais identifiants" -#: counter/views.py:271 +#: counter/views.py:317 msgid "User is not barman" msgstr "L'utilisateur n'est pas barman." -#: counter/views.py:276 +#: counter/views.py:322 msgid "Bad location, someone is already logged in somewhere else" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" -#: counter/views.py:544 +#: counter/views.py:614 msgid "END" msgstr "FIN" -#: counter/views.py:546 +#: counter/views.py:616 msgid "CAN" msgstr "ANN" -#: counter/views.py:569 +#: counter/views.py:639 msgid "You have not enough money to buy all the basket" msgstr "Vous n'avez pas assez d'argent pour acheter le panier" -#: counter/views.py:709 +#: counter/views.py:780 msgid "Counter administration" msgstr "Administration des comptoirs" -#: counter/views.py:711 +#: counter/views.py:782 msgid "Stocks" msgstr "Stocks" -#: counter/views.py:720 +#: counter/views.py:791 msgid "Products" msgstr "Produits" -#: counter/views.py:725 +#: counter/views.py:796 msgid "Archived products" msgstr "Produits archivés" -#: counter/views.py:730 +#: counter/views.py:801 msgid "Product types" msgstr "Types de produit" -#: counter/views.py:909 +#: counter/views.py:980 msgid "Parent product" msgstr "Produit parent" -#: counter/views.py:915 +#: counter/views.py:986 msgid "Buying groups" msgstr "Groupes d'achat" -#: counter/views.py:1049 +#: counter/views.py:1120 msgid "10 cents" msgstr "10 centimes" -#: counter/views.py:1050 +#: counter/views.py:1121 msgid "20 cents" msgstr "20 centimes" -#: counter/views.py:1051 +#: counter/views.py:1122 msgid "50 cents" msgstr "50 centimes" -#: counter/views.py:1052 +#: counter/views.py:1123 msgid "1 euro" msgstr "1 €" -#: counter/views.py:1053 +#: counter/views.py:1124 msgid "2 euros" msgstr "2 €" -#: counter/views.py:1054 +#: counter/views.py:1125 msgid "5 euros" msgstr "5 €" -#: counter/views.py:1055 +#: counter/views.py:1126 msgid "10 euros" msgstr "10 €" -#: counter/views.py:1056 +#: counter/views.py:1127 msgid "20 euros" msgstr "20 €" -#: counter/views.py:1057 +#: counter/views.py:1128 msgid "50 euros" msgstr "50 €" -#: counter/views.py:1059 +#: counter/views.py:1130 msgid "100 euros" msgstr "100 €" -#: counter/views.py:1062 counter/views.py:1068 counter/views.py:1074 -#: counter/views.py:1080 counter/views.py:1086 +#: counter/views.py:1133 counter/views.py:1139 counter/views.py:1145 +#: counter/views.py:1151 counter/views.py:1157 msgid "Check amount" msgstr "Montant du chèque" -#: counter/views.py:1065 counter/views.py:1071 counter/views.py:1077 -#: counter/views.py:1083 counter/views.py:1089 +#: counter/views.py:1136 counter/views.py:1142 counter/views.py:1148 +#: counter/views.py:1154 counter/views.py:1160 msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views.py:1725 +#: counter/views.py:1796 msgid "people(s)" msgstr "personne(s)" @@ -4574,11 +4625,11 @@ msgstr "Enlever des favoris" msgid "Mark as favorite" msgstr "Ajouter aux favoris" -#: forum/views.py:186 +#: forum/views.py:189 msgid "Apply rights and club owner recursively" msgstr "Appliquer les droits et le club propriétaire récursivement" -#: forum/views.py:404 +#: forum/views.py:407 #, python-format msgid "%(author)s said" msgstr "Citation de %(author)s" @@ -4632,12 +4683,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:521 +#: sith/settings.py:518 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:521 +#: sith/settings.py:518 msgid "Drying" msgstr "Séchage" @@ -4842,243 +4893,243 @@ msgstr "Anglais" msgid "French" msgstr "Français" -#: sith/settings.py:351 +#: sith/settings.py:348 msgid "TC" msgstr "TC" -#: sith/settings.py:352 +#: sith/settings.py:349 msgid "IMSI" msgstr "IMSI" -#: sith/settings.py:353 +#: sith/settings.py:350 msgid "IMAP" msgstr "IMAP" -#: sith/settings.py:354 +#: sith/settings.py:351 msgid "INFO" msgstr "INFO" -#: sith/settings.py:355 +#: sith/settings.py:352 msgid "GI" msgstr "GI" -#: sith/settings.py:356 +#: sith/settings.py:353 msgid "E" msgstr "E" -#: sith/settings.py:357 +#: sith/settings.py:354 msgid "EE" msgstr "EE" -#: sith/settings.py:358 +#: sith/settings.py:355 msgid "GESC" msgstr "GESC" -#: sith/settings.py:359 +#: sith/settings.py:356 msgid "GMC" msgstr "GMC" -#: sith/settings.py:360 +#: sith/settings.py:357 msgid "MC" msgstr "MC" -#: sith/settings.py:361 +#: sith/settings.py:358 msgid "EDIM" msgstr "EDIM" -#: sith/settings.py:362 +#: sith/settings.py:359 msgid "Humanities" msgstr "Humanités" -#: sith/settings.py:363 +#: sith/settings.py:360 msgid "N/A" msgstr "N/A" -#: sith/settings.py:367 sith/settings.py:374 sith/settings.py:391 +#: sith/settings.py:364 sith/settings.py:371 sith/settings.py:388 msgid "Check" msgstr "Chèque" -#: sith/settings.py:368 sith/settings.py:376 sith/settings.py:392 +#: sith/settings.py:365 sith/settings.py:373 sith/settings.py:389 msgid "Cash" msgstr "Espèces" -#: sith/settings.py:369 +#: sith/settings.py:366 msgid "Transfert" msgstr "Virement" -#: sith/settings.py:382 +#: sith/settings.py:379 msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:383 +#: sith/settings.py:380 msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:384 +#: sith/settings.py:381 msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:435 +#: sith/settings.py:432 msgid "One semester" msgstr "Un semestre, 15 €" -#: sith/settings.py:436 +#: sith/settings.py:433 msgid "Two semesters" msgstr "Deux semestres, 28 €" -#: sith/settings.py:438 +#: sith/settings.py:435 msgid "Common core cursus" msgstr "Cursus tronc commun, 45 €" -#: sith/settings.py:442 +#: sith/settings.py:439 msgid "Branch cursus" msgstr "Cursus branche, 45 €" -#: sith/settings.py:443 +#: sith/settings.py:440 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:444 +#: sith/settings.py:441 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:445 +#: sith/settings.py:442 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:446 +#: sith/settings.py:443 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:447 +#: sith/settings.py:444 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:448 +#: sith/settings.py:445 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:449 +#: sith/settings.py:446 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 15 €" -#: sith/settings.py:451 +#: sith/settings.py:448 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:455 +#: sith/settings.py:452 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:456 +#: sith/settings.py:453 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:458 +#: sith/settings.py:455 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:462 +#: sith/settings.py:459 msgid "One day" msgstr "Un jour" -#: sith/settings.py:481 +#: sith/settings.py:478 msgid "President" msgstr "Président" -#: sith/settings.py:482 +#: sith/settings.py:479 msgid "Vice-President" msgstr "Vice-Président" -#: sith/settings.py:483 +#: sith/settings.py:480 msgid "Treasurer" msgstr "Trésorier" -#: sith/settings.py:484 +#: sith/settings.py:481 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:485 +#: sith/settings.py:482 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:486 +#: sith/settings.py:483 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:487 +#: sith/settings.py:484 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:488 +#: sith/settings.py:485 msgid "Active member" msgstr "Membre actif" -#: sith/settings.py:489 +#: sith/settings.py:486 msgid "Curious" msgstr "Curieux" -#: sith/settings.py:525 +#: sith/settings.py:522 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:526 +#: sith/settings.py:523 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:527 +#: sith/settings.py:524 #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:528 +#: sith/settings.py:525 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:529 +#: sith/settings.py:526 #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:530 +#: sith/settings.py:527 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:531 +#: sith/settings.py:528 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:532 +#: sith/settings.py:529 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:533 +#: sith/settings.py:530 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:545 +#: sith/settings.py:542 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:546 +#: sith/settings.py:543 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:547 +#: sith/settings.py:544 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:548 +#: sith/settings.py:545 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:549 +#: sith/settings.py:546 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:557 +#: sith/settings.py:554 msgid "AE tee-shirt" msgstr "Tee-shirt AE" From 5ae7d10e845a258a15863a8caa679c1f0c2f0f18 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Tue, 14 May 2019 15:13:14 +0200 Subject: [PATCH 7/9] Add unit tests for student cards and fix edge cases --- core/management/commands/populate.py | 3 +- counter/models.py | 2 +- counter/tests.py | 192 +++++++++++++++++++++++++++ counter/views.py | 4 +- 4 files changed, 197 insertions(+), 4 deletions(-) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 02bd3cd3..312744c6 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -48,7 +48,7 @@ from accounting.models import ( from core.utils import resize_image from club.models import Club, Membership from subscription.models import Subscription -from counter.models import Customer, ProductType, Product, Counter, Selling +from counter.models import Customer, ProductType, Product, Counter, Selling, StudentCard from com.models import Sith, Weekmail, News, NewsDate from election.models import Election, Role, Candidature, ElectionList from forum.models import Forum, ForumTopic @@ -870,6 +870,7 @@ Welcome to the wiki page! start=s.subscription_start, ) s.save() + StudentCard(uid="9A89B82018B0A0", customer=sli.customer).save() # Adding subscription for Krophil s = Subscription( member=User.objects.filter(pk=krophil.pk).first(), diff --git a/counter/models.py b/counter/models.py index bcc9388e..3306a550 100644 --- a/counter/models.py +++ b/counter/models.py @@ -757,7 +757,7 @@ class StudentCard(models.Model): def can_create(customer, user): return user.pk == customer.user.pk or user.is_board_member or user.is_root - def can_edit(self, obj): + def can_be_edited_by(self, obj): if isinstance(obj, User): return StudentCard.can_create(self.customer, obj) return False diff --git a/counter/tests.py b/counter/tests.py index 7e037826..e001b13f 100644 --- a/counter/tests.py +++ b/counter/tests.py @@ -142,3 +142,195 @@ class BarmanConnectionTest(TestCase): self.assertFalse( '
  • S' Kia
  • ' in str(response_get.content) ) + + +class StudentCardTest(TestCase): + """ + Tests for adding and deleting Stundent Cards + Test that an user can be found with it's student card + """ + + def setUp(self): + call_command("populate") + self.krophil = User.objects.get(username="krophil") + self.sli = User.objects.get(username="sli") + + self.counter = Counter.objects.filter(id=2).first() + + # Auto login on counter + self.client.post( + reverse("counter:login", args=[self.counter.id]), + {"username": "krophil", "password": "plop"}, + ) + + def test_search_user_with_student_card(self): + response = self.client.post( + reverse("counter:details", args=[self.counter.id]), + {"code": "9A89B82018B0A0"}, + ) + + self.assertEqual( + response.url, + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + ) + + def test_add_student_card_from_counter(self): + response = self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": "8B90734A802A8F", "action": "add_student_card"}, + ) + self.assertContains(response, text="8B90734A802A8F") + + def test_add_student_card_from_counter_fail(self): + response = self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": "8B90734A802A8", "action": "add_student_card"}, + ) + self.assertContains( + response, text="Ce n'est pas un UID de carte étudiante valide" + ) + + response = self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": "8B90734A802A8FA", "action": "add_student_card"}, + ) + self.assertContains( + response, text="Ce n'est pas un UID de carte étudiante valide" + ) + + def test_delete_student_card_with_owner(self): + self.client.login(username="sli", password="plop") + self.client.post( + reverse( + "counter:delete_student_card", + kwargs={ + "customer_id": self.sli.customer.pk, + "card_id": self.sli.customer.student_cards.first().id, + }, + ) + ) + self.assertFalse(self.sli.customer.student_cards.exists()) + + def test_delete_student_card_with_board_member(self): + self.client.login(username="skia", password="plop") + self.client.post( + reverse( + "counter:delete_student_card", + kwargs={ + "customer_id": self.sli.customer.pk, + "card_id": self.sli.customer.student_cards.first().id, + }, + ) + ) + self.assertFalse(self.sli.customer.student_cards.exists()) + + def test_delete_student_card_with_root(self): + self.client.login(username="root", password="plop") + self.client.post( + reverse( + "counter:delete_student_card", + kwargs={ + "customer_id": self.sli.customer.pk, + "card_id": self.sli.customer.student_cards.first().id, + }, + ) + ) + self.assertFalse(self.sli.customer.student_cards.exists()) + + def test_delete_student_card_fail(self): + self.client.login(username="krophil", password="plop") + response = self.client.post( + reverse( + "counter:delete_student_card", + kwargs={ + "customer_id": self.sli.customer.pk, + "card_id": self.sli.customer.student_cards.first().id, + }, + ) + ) + self.assertEqual(response.status_code, 403) + self.assertTrue(self.sli.customer.student_cards.exists()) + + def test_add_student_card_from_user_preferences(self): + # Test with owner of the card + self.client.login(username="sli", password="plop") + self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8B90734A802A8F"}, + ) + + response = self.client.get( + reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) + ) + self.assertContains(response, text="8B90734A802A8F") + + # Test with board member + self.client.login(username="skia", password="plop") + self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8B90734A802A8A"}, + ) + + response = self.client.get( + reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) + ) + self.assertContains(response, text="8B90734A802A8A") + + # Test with root + self.client.login(username="root", password="plop") + self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8B90734A802A8B"}, + ) + + response = self.client.get( + reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) + ) + self.assertContains(response, text="8B90734A802A8B") + + def test_add_student_card_from_user_preferences_fail(self): + self.client.login(username="sli", password="plop") + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8B90734A802A8"}, + ) + + self.assertContains(response, text="Cet UID est invalide") + + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8B90734A802A8FA"}, + ) + self.assertContains(response, text="Cet UID est invalide") + + # Test with unauthorized user + self.client.login(username="krophil", password="plop") + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8B90734A802A8F"}, + ) + self.assertEqual(response.status_code, 403) diff --git a/counter/views.py b/counter/views.py index 2f3a653d..8296aa2b 100644 --- a/counter/views.py +++ b/counter/views.py @@ -113,8 +113,8 @@ class StudentCardForm(forms.ModelForm): def clean(self): cleaned_data = super(StudentCardForm, self).clean() - uid = cleaned_data.get("uid") - if not StudentCard.is_valid(uid): + uid = cleaned_data.get("uid", None) + if not uid or not StudentCard.is_valid(uid): raise forms.ValidationError(_("This uid is invalid"), code="invalid") return cleaned_data From 19e353970d99931bb89e3ed9b4ebd4ac9ab73db1 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Tue, 14 May 2019 15:51:14 +0200 Subject: [PATCH 8/9] Enforce uid with uppercase for Studentcard and test more edge cases --- counter/models.py | 3 ++- counter/tests.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/counter/models.py b/counter/models.py index 3306a550..93105908 100644 --- a/counter/models.py +++ b/counter/models.py @@ -749,7 +749,8 @@ class StudentCard(models.Model): @staticmethod def is_valid(uid): return ( - len(uid) == StudentCard.UID_SIZE + uid.isupper() + and len(uid) == StudentCard.UID_SIZE and not StudentCard.objects.filter(uid=uid).exists() ) diff --git a/counter/tests.py b/counter/tests.py index e001b13f..1547eea8 100644 --- a/counter/tests.py +++ b/counter/tests.py @@ -188,6 +188,7 @@ class StudentCardTest(TestCase): self.assertContains(response, text="8B90734A802A8F") def test_add_student_card_from_counter_fail(self): + # UID too short response = self.client.post( reverse( "counter:click", @@ -199,6 +200,7 @@ class StudentCardTest(TestCase): response, text="Ce n'est pas un UID de carte étudiante valide" ) + # UID too long response = self.client.post( reverse( "counter:click", @@ -210,6 +212,30 @@ class StudentCardTest(TestCase): response, text="Ce n'est pas un UID de carte étudiante valide" ) + # Test with already existing card + response = self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": "9A89B82018B0A0", "action": "add_student_card"}, + ) + self.assertContains( + response, text="Ce n'est pas un UID de carte étudiante valide" + ) + + # Test with lowercase + response = self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": "8b90734a802a9f", "action": "add_student_card"}, + ) + self.assertContains( + response, text="Ce n'est pas un UID de carte étudiante valide" + ) + def test_delete_student_card_with_owner(self): self.client.login(username="sli", password="plop") self.client.post( @@ -308,6 +334,7 @@ class StudentCardTest(TestCase): def test_add_student_card_from_user_preferences_fail(self): self.client.login(username="sli", password="plop") + # UID too short response = self.client.post( reverse( "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} @@ -317,6 +344,7 @@ class StudentCardTest(TestCase): self.assertContains(response, text="Cet UID est invalide") + # UID too long response = self.client.post( reverse( "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} @@ -325,6 +353,26 @@ class StudentCardTest(TestCase): ) self.assertContains(response, text="Cet UID est invalide") + # Test with already existing card + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "9A89B82018B0A0"}, + ) + self.assertContains( + response, text="Un objet Student card avec ce champ Uid existe déjà." + ) + + # Test with lowercase + response = self.client.post( + reverse( + "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} + ), + {"uid": "8b90734a802a9f"}, + ) + self.assertContains(response, text="Cet UID est invalide") + # Test with unauthorized user self.client.login(username="krophil", password="plop") response = self.client.post( From 3bddf176d8235191203cc257a73646cf34d3fa71 Mon Sep 17 00:00:00 2001 From: Bartuccio Antoine Date: Mon, 20 May 2019 19:12:53 +0200 Subject: [PATCH 9/9] Fix typo for NFC cards --- counter/views.py | 2 +- locale/fr/LC_MESSAGES/django.po | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/counter/views.py b/counter/views.py index 8296aa2b..e3f92058 100644 --- a/counter/views.py +++ b/counter/views.py @@ -115,7 +115,7 @@ class StudentCardForm(forms.ModelForm): cleaned_data = super(StudentCardForm, self).clean() uid = cleaned_data.get("uid", None) if not uid or not StudentCard.is_valid(uid): - raise forms.ValidationError(_("This uid is invalid"), code="invalid") + raise forms.ValidationError(_("This UID is invalid"), code="invalid") return cleaned_data diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index f32db813..4041b770 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -4128,7 +4128,7 @@ msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" #: counter/views.py:118 -msgid "This uid is invalid" +msgid "This UID is invalid" msgstr "Cet UID est invalide" #: counter/views.py:176