mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 07:41:14 +00:00
feat: make student card unique per user
This commit is contained in:
parent
3b7e338808
commit
466fe58763
@ -69,7 +69,7 @@ class Command(BaseCommand):
|
|||||||
# sqlite doesn't support this operation
|
# sqlite doesn't support this operation
|
||||||
return
|
return
|
||||||
sqlcmd = StringIO()
|
sqlcmd = StringIO()
|
||||||
call_command("sqlsequencereset", *args, stdout=sqlcmd)
|
call_command("sqlsequencereset", "--no-color", *args, stdout=sqlcmd)
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute(sqlcmd.getvalue())
|
cursor.execute(sqlcmd.getvalue())
|
||||||
|
|
||||||
@ -137,11 +137,10 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.reset_index("club")
|
self.reset_index("club")
|
||||||
|
for bar_id, bar_name in settings.SITH_COUNTER_BARS:
|
||||||
|
Counter(id=bar_id, name=bar_name, club=bar_club, type="BAR").save()
|
||||||
|
self.reset_index("counter")
|
||||||
counters = [
|
counters = [
|
||||||
*[
|
|
||||||
Counter(id=bar_id, name=bar_name, club=bar_club, type="BAR")
|
|
||||||
for bar_id, bar_name in settings.SITH_COUNTER_BARS
|
|
||||||
],
|
|
||||||
Counter(name="Eboutic", club=main_club, type="EBOUTIC"),
|
Counter(name="Eboutic", club=main_club, type="EBOUTIC"),
|
||||||
Counter(name="AE", club=main_club, type="OFFICE"),
|
Counter(name="AE", club=main_club, type="OFFICE"),
|
||||||
Counter(name="Vidage comptes AE", club=main_club, type="OFFICE"),
|
Counter(name="Vidage comptes AE", club=main_club, type="OFFICE"),
|
||||||
|
@ -35,8 +35,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if student_card %}
|
{% if student_card_fragment %}
|
||||||
{{ student_card }}
|
{{ student_card_fragment }}
|
||||||
<p class="justify">
|
<p class="justify">
|
||||||
{% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually
|
{% 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 %}
|
add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}
|
||||||
|
@ -571,7 +571,7 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
|
|||||||
if not hasattr(self.object, "trombi_user"):
|
if not hasattr(self.object, "trombi_user"):
|
||||||
kwargs["trombi_form"] = UserTrombiForm()
|
kwargs["trombi_form"] = UserTrombiForm()
|
||||||
if hasattr(self.object, "customer"):
|
if hasattr(self.object, "customer"):
|
||||||
kwargs["student_card"] = StudentCardFormView.get_template_data(
|
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
|
||||||
self.object.customer
|
self.object.customer
|
||||||
).render(self.request)
|
).render(self.request)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -50,9 +50,7 @@ class StudentCardForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = StudentCard
|
model = StudentCard
|
||||||
fields = ["uid"]
|
fields = ["uid"]
|
||||||
widgets = {
|
widgets = {"uid": NFCTextInput}
|
||||||
"uid": NFCTextInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
53
counter/migrations/0026_alter_studentcard_customer.py
Normal file
53
counter/migrations/0026_alter_studentcard_customer.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 4.2.17 on 2024-12-08 13:30
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.migrations.state import StateApps
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
|
||||||
|
def delete_duplicates(apps: StateApps, schema_editor):
|
||||||
|
"""Delete cards of users with more than one student cards.
|
||||||
|
|
||||||
|
For all users who have more than one registered student card, all
|
||||||
|
the cards except the last one are deleted.
|
||||||
|
"""
|
||||||
|
Customer = apps.get_model("counter", "Customer")
|
||||||
|
StudentCard = apps.get_model("counter", "StudentCard")
|
||||||
|
customers = (
|
||||||
|
Customer.objects.annotate(nb_cards=Count("student_cards"))
|
||||||
|
.filter(nb_cards__gt=1)
|
||||||
|
.prefetch_related("student_cards")
|
||||||
|
)
|
||||||
|
to_delete = [
|
||||||
|
card.id
|
||||||
|
for customer in customers
|
||||||
|
for card in sorted(customer.student_cards.all(), key=attrgetter("id"))[:-1]
|
||||||
|
]
|
||||||
|
StudentCard.objects.filter(id__in=to_delete).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [("counter", "0025_remove_product_parent_product_and_more")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(delete_duplicates, migrations.RunPython.noop),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="studentcard",
|
||||||
|
name="customer",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="student_card",
|
||||||
|
to="counter.customer",
|
||||||
|
verbose_name="student card",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="studentcard",
|
||||||
|
options={
|
||||||
|
"verbose_name": "student card",
|
||||||
|
"verbose_name_plural": "student cards",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1138,20 +1138,22 @@ class StudentCard(models.Model):
|
|||||||
uid = models.CharField(
|
uid = models.CharField(
|
||||||
_("uid"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)]
|
_("uid"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)]
|
||||||
)
|
)
|
||||||
customer = models.ForeignKey(
|
customer = models.OneToOneField(
|
||||||
Customer,
|
Customer,
|
||||||
related_name="student_cards",
|
related_name="student_card",
|
||||||
verbose_name=_("student cards"),
|
verbose_name=_("student card"),
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("student card")
|
||||||
|
verbose_name_plural = _("student cards")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.uid
|
return self.uid
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_valid(uid):
|
def is_valid(uid: str) -> bool:
|
||||||
return (
|
return (
|
||||||
(uid.isupper() or uid.isnumeric())
|
(uid.isupper() or uid.isnumeric())
|
||||||
and len(uid) == StudentCard.UID_SIZE
|
and len(uid) == StudentCard.UID_SIZE
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
|
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
|
||||||
|
|
||||||
{% if counter.type == 'BAR' %}
|
{% if counter.type == 'BAR' %}
|
||||||
{{ student_card }}
|
{{ student_card_fragment }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<div id="student_card_form">
|
<div id="student_card_form">
|
||||||
<h3>{% trans %}Add a student card{% endtrans %}</h3>
|
<h3>{% trans %}Student card{% endtrans %}</h3>
|
||||||
|
{% if not customer.student_card %}
|
||||||
<form
|
<form
|
||||||
hx-post="{{ action }}"
|
hx-post="{{ action }}"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
@ -8,22 +9,14 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.as_p() }}
|
{{ form.as_p() }}
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
<h6>{% trans %}Registered cards{% endtrans %}</h6>
|
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
|
||||||
{% if student_cards %}
|
{% else %}
|
||||||
|
<p>
|
||||||
<ul>
|
{% trans %}Registered{% endtrans %} <i class="fa fa-check"></i> -
|
||||||
{% for card in student_cards %}
|
<a href="{{ url('counter:delete_student_card', customer_id=customer.pk) }}">
|
||||||
<li>
|
|
||||||
{{ card.uid }}
|
|
||||||
<a href="{{ url('counter:delete_student_card', customer_id=customer.pk, card_id=card.id) }}">
|
|
||||||
{% trans %}Delete{% endtrans %}
|
{% trans %}Delete{% endtrans %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</p>
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
|
||||||
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import string
|
import string
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -175,7 +176,6 @@ class TestStudentCard(TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.customer = subscriber_user.make()
|
cls.customer = subscriber_user.make()
|
||||||
cls.customer.save()
|
|
||||||
cls.barmen = subscriber_user.make(password=make_password("plop"))
|
cls.barmen = subscriber_user.make(password=make_password("plop"))
|
||||||
cls.board_admin = board_user.make()
|
cls.board_admin = board_user.make()
|
||||||
cls.club_admin = baker.make(User)
|
cls.club_admin = baker.make(User)
|
||||||
@ -205,6 +205,22 @@ class TestStudentCard(TestCase):
|
|||||||
{"username": self.barmen.username, "password": "plop"},
|
{"username": self.barmen.username, "password": "plop"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def invalid_uids(self) -> list[tuple[str, str]]:
|
||||||
|
"""Return a list of invalid uids, with the associated error message"""
|
||||||
|
return [
|
||||||
|
("8B90734A802A8", ""), # too short
|
||||||
|
(
|
||||||
|
"8B90734A802A8FA",
|
||||||
|
"Assurez-vous que cette valeur comporte au plus 14 caractères (actuellement 15).",
|
||||||
|
), # too long
|
||||||
|
("8b90734a802a9f", ""), # has lowercases
|
||||||
|
(" " * 14, "Ce champ est obligatoire."), # empty
|
||||||
|
(
|
||||||
|
self.customer.customer.student_card.uid,
|
||||||
|
"Un objet Carte étudiante avec ce champ Uid existe déjà.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
def test_search_user_with_student_card(self):
|
def test_search_user_with_student_card(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("counter:details", args=[self.counter.id]),
|
reverse("counter:details", args=[self.counter.id]),
|
||||||
@ -213,396 +229,144 @@ class TestStudentCard(TestCase):
|
|||||||
|
|
||||||
assert response.url == reverse(
|
assert response.url == reverse(
|
||||||
"counter:click",
|
"counter:click",
|
||||||
kwargs={"counter_id": self.counter.id, "user_id": self.customer.id},
|
kwargs={"counter_id": self.counter.id, "user_id": self.customer.pk},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_add_student_card_from_counter(self):
|
def test_add_student_card_from_counter(self):
|
||||||
# Test card with mixed letters and numbers
|
for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]:
|
||||||
|
customer = subscriber_user.make().customer
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:add_student_card",
|
"counter:add_student_card", kwargs={"customer_id": customer.pk}
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
{"uid": "8B90734A802A8F"},
|
{"uid": uid},
|
||||||
HTTP_REFERER=reverse(
|
HTTP_REFERER=reverse(
|
||||||
"counter:click",
|
"counter:click",
|
||||||
kwargs={
|
kwargs={"counter_id": self.counter.id, "user_id": customer.pk},
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
self.assertContains(self.client.get(response.url), text="8B90734A802A8F")
|
customer.refresh_from_db()
|
||||||
|
assert hasattr(customer, "student_card")
|
||||||
# Test card with only numbers
|
assert customer.student_card.uid == uid
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"uid": "04786547890123"},
|
|
||||||
HTTP_REFERER=reverse(
|
|
||||||
"counter:click",
|
|
||||||
kwargs={
|
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
assert response.status_code == 302
|
|
||||||
self.assertContains(self.client.get(response.url), text="04786547890123")
|
|
||||||
|
|
||||||
# Test card with only letters
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"uid": "ABCAAAFAAFAAAB"},
|
|
||||||
HTTP_REFERER=reverse(
|
|
||||||
"counter:click",
|
|
||||||
kwargs={
|
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
assert response.status_code == 302
|
|
||||||
self.assertContains(self.client.get(response.url), text="ABCAAAFAAFAAAB")
|
|
||||||
|
|
||||||
def test_add_student_card_from_counter_fail(self):
|
def test_add_student_card_from_counter_fail(self):
|
||||||
# UID too short
|
customer = subscriber_user.make().customer
|
||||||
|
for uid, error_msg in self.invalid_uids():
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:add_student_card",
|
"counter:add_student_card", kwargs={"customer_id": customer.pk}
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
{"uid": "8B90734A802A8"},
|
{"uid": uid},
|
||||||
HTTP_REFERER=reverse(
|
HTTP_REFERER=reverse(
|
||||||
"counter:click",
|
"counter:click",
|
||||||
kwargs={
|
kwargs={"counter_id": self.counter.id, "user_id": customer.pk},
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
self.assertContains(response, text="Cet UID est invalide")
|
||||||
|
self.assertContains(response, text=error_msg)
|
||||||
# UID too long
|
customer.refresh_from_db()
|
||||||
response = self.client.post(
|
assert not hasattr(customer, "student_card")
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"uid": "8B90734A802A8FA"},
|
|
||||||
HTTP_REFERER=reverse(
|
|
||||||
"counter:click",
|
|
||||||
kwargs={
|
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
|
||||||
self.assertContains(
|
|
||||||
response,
|
|
||||||
text="Assurez-vous que cette valeur comporte au plus 14 caractères (actuellement 15).",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test with already existing card
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"uid": self.valid_card.uid},
|
|
||||||
HTTP_REFERER=reverse(
|
|
||||||
"counter:click",
|
|
||||||
kwargs={
|
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
|
||||||
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.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"uid": "8b90734a802a9f"},
|
|
||||||
HTTP_REFERER=reverse(
|
|
||||||
"counter:click",
|
|
||||||
kwargs={
|
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
|
||||||
|
|
||||||
# Test with white spaces
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
{"uid": " "},
|
|
||||||
HTTP_REFERER=reverse(
|
|
||||||
"counter:click",
|
|
||||||
kwargs={
|
|
||||||
"counter_id": self.counter.id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
|
||||||
self.assertContains(response, text="Ce champ est obligatoire.")
|
|
||||||
|
|
||||||
def test_add_student_card_from_counter_unauthorized(self):
|
def test_add_student_card_from_counter_unauthorized(self):
|
||||||
# Send to a counter where you aren't logged in
|
barman = subscriber_user.make()
|
||||||
self.client.post(
|
self.counter.sellers.add(barman)
|
||||||
reverse("counter:logout", args=[self.counter.id]),
|
customer = self.customer.customer
|
||||||
{"user_id": self.barmen.id},
|
# There is someone logged to a counter
|
||||||
)
|
# with the client of this TestCase instance,
|
||||||
|
# so we create a new client, in order to check
|
||||||
|
# that using a client not logged to a counter
|
||||||
|
# where another client is logged still isn't authorized.
|
||||||
|
client = Client()
|
||||||
|
|
||||||
def send_valid_request(client, counter_id):
|
def send_valid_request(counter_id):
|
||||||
return client.post(
|
return client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:add_student_card",
|
"counter:add_student_card", kwargs={"customer_id": customer.pk}
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
{"uid": "8B90734A802A8F"},
|
{"uid": "8B90734A802A8F"},
|
||||||
HTTP_REFERER=reverse(
|
HTTP_REFERER=reverse(
|
||||||
"counter:click",
|
"counter:click",
|
||||||
kwargs={
|
kwargs={"counter_id": counter_id, "user_id": customer.pk},
|
||||||
"counter_id": counter_id,
|
|
||||||
"user_id": self.customer.customer.pk,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert send_valid_request(self.client, self.counter.id).status_code == 403
|
# Send to a counter where you aren't logged in
|
||||||
|
assert send_valid_request(self.counter.id).status_code == 403
|
||||||
|
|
||||||
# Send to a non bar counter
|
# Send to a non bar counter
|
||||||
self.client.force_login(self.club_admin)
|
client.force_login(self.club_admin)
|
||||||
assert send_valid_request(self.client, self.club_counter.id).status_code == 403
|
assert send_valid_request(self.club_counter.id).status_code == 403
|
||||||
|
|
||||||
def test_delete_student_card_with_owner(self):
|
def test_delete_student_card_with_owner(self):
|
||||||
self.client.force_login(self.customer)
|
self.client.force_login(self.customer)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:delete_student_card",
|
"counter:delete_student_card",
|
||||||
kwargs={
|
kwargs={"customer_id": self.customer.customer.pk},
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
"card_id": self.customer.customer.student_cards.first().id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert not self.customer.customer.student_cards.exists()
|
self.customer.customer.refresh_from_db()
|
||||||
|
assert not hasattr(self.customer.customer, "student_card")
|
||||||
|
|
||||||
def test_delete_student_card_with_board_member(self):
|
def test_delete_student_card_with_admin_user(self):
|
||||||
self.client.force_login(self.board_admin)
|
"""Test that AE board members and root users can delete student cards"""
|
||||||
|
for user in self.board_admin, self.root:
|
||||||
|
self.client.force_login(user)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:delete_student_card",
|
"counter:delete_student_card",
|
||||||
kwargs={
|
kwargs={"customer_id": self.customer.customer.pk},
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
"card_id": self.customer.customer.student_cards.first().id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert not self.customer.customer.student_cards.exists()
|
self.customer.customer.refresh_from_db()
|
||||||
|
assert not hasattr(self.customer.customer, "student_card")
|
||||||
def test_delete_student_card_with_root(self):
|
|
||||||
self.client.force_login(self.root)
|
|
||||||
self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:delete_student_card",
|
|
||||||
kwargs={
|
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
"card_id": self.customer.customer.student_cards.first().id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert not self.customer.customer.student_cards.exists()
|
|
||||||
|
|
||||||
def test_delete_student_card_fail(self):
|
def test_delete_student_card_fail(self):
|
||||||
|
"""Test that non-admin users cannot delete student cards"""
|
||||||
self.client.force_login(self.subscriber)
|
self.client.force_login(self.subscriber)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:delete_student_card",
|
"counter:delete_student_card",
|
||||||
kwargs={
|
kwargs={"customer_id": self.customer.customer.pk},
|
||||||
"customer_id": self.customer.customer.pk,
|
|
||||||
"card_id": self.customer.customer.student_cards.first().id,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
assert self.customer.customer.student_cards.exists()
|
self.subscriber.customer.refresh_from_db()
|
||||||
|
assert not hasattr(self.subscriber.customer, "student_card")
|
||||||
|
|
||||||
def test_add_student_card_from_user_preferences(self):
|
def test_add_student_card_from_user_preferences(self):
|
||||||
# Test with owner of the card
|
users = [self.subscriber, self.board_admin, self.root]
|
||||||
self.client.force_login(self.customer)
|
uids = ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]
|
||||||
|
for user, uid in itertools.product(users, uids):
|
||||||
|
self.customer.customer.student_card.delete()
|
||||||
|
self.client.force_login(user)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"counter:add_student_card",
|
"counter:add_student_card",
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
kwargs={"customer_id": self.customer.customer.pk},
|
||||||
),
|
),
|
||||||
{"uid": "8B90734A802A8F"},
|
{"uid": uid},
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 302
|
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
|
||||||
self.assertContains(response, text="8B90734A802A8F")
|
|
||||||
|
|
||||||
# Test with board member
|
|
||||||
self.client.force_login(self.board_admin)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "8B90734A802A8A"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 302
|
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
|
||||||
self.assertContains(response, text="8B90734A802A8A")
|
|
||||||
|
|
||||||
# Test card with only numbers
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "04786547890123"},
|
|
||||||
)
|
)
|
||||||
assert response.status_code == 302
|
assert response.status_code == 302
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
response = self.client.get(response.url)
|
||||||
self.assertContains(response, text="04786547890123")
|
|
||||||
|
|
||||||
# Test card with only letters
|
self.customer.customer.refresh_from_db()
|
||||||
response = self.client.post(
|
assert self.customer.customer.student_card.uid == uid
|
||||||
reverse(
|
self.assertContains(response, text="Enregistré")
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "ABCAAAFAAFAAAB"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 302
|
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
|
||||||
self.assertContains(response, text="ABCAAAFAAFAAAB")
|
|
||||||
|
|
||||||
# Test with root
|
|
||||||
self.client.force_login(self.root)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "8B90734A802A8B"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 302
|
|
||||||
|
|
||||||
response = self.client.get(response.url)
|
|
||||||
self.assertContains(response, text="8B90734A802A8B")
|
|
||||||
|
|
||||||
def test_add_student_card_from_user_preferences_fail(self):
|
def test_add_student_card_from_user_preferences_fail(self):
|
||||||
self.client.force_login(self.customer)
|
customer = subscriber_user.make()
|
||||||
# UID too short
|
self.client.force_login(customer)
|
||||||
response = self.client.post(
|
for uid, error_msg in self.invalid_uids():
|
||||||
reverse(
|
url = reverse(
|
||||||
"counter:add_student_card",
|
"counter:add_student_card", kwargs={"customer_id": customer.customer.pk}
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "8B90734A802A8"},
|
|
||||||
)
|
)
|
||||||
|
response = self.client.post(url, {"uid": uid})
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
self.assertContains(response, text="Cet UID est invalide")
|
||||||
|
self.assertContains(response, text=error_msg)
|
||||||
# UID too long
|
customer.refresh_from_db()
|
||||||
response = self.client.post(
|
assert not hasattr(customer.customer, "student_card")
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "8B90734A802A8FA"},
|
|
||||||
)
|
|
||||||
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.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": self.valid_card.uid},
|
|
||||||
)
|
|
||||||
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.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "8b90734a802a9f"},
|
|
||||||
)
|
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
|
||||||
|
|
||||||
# Test with white spaces
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": " " * 14},
|
|
||||||
)
|
|
||||||
self.assertContains(response, text="Cet UID est invalide")
|
|
||||||
|
|
||||||
# Test with unauthorized user
|
|
||||||
self.client.force_login(self.subscriber)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse(
|
|
||||||
"counter:add_student_card",
|
|
||||||
kwargs={"customer_id": self.customer.customer.pk},
|
|
||||||
),
|
|
||||||
{"uid": "8B90734A802A8F"},
|
|
||||||
)
|
|
||||||
assert response.status_code == 403
|
|
||||||
|
|
||||||
|
|
||||||
class TestCustomerAccountId(TestCase):
|
class TestCustomerAccountId(TestCase):
|
||||||
|
@ -81,7 +81,7 @@ urlpatterns = [
|
|||||||
name="add_student_card",
|
name="add_student_card",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"customer/<int:customer_id>/card/delete/<int:card_id>/",
|
"customer/<int:customer_id>/card/delete/",
|
||||||
StudentCardDeleteView.as_view(),
|
StudentCardDeleteView.as_view(),
|
||||||
name="delete_student_card",
|
name="delete_student_card",
|
||||||
),
|
),
|
||||||
|
@ -415,7 +415,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
kwargs["basket_total"] = self.sum_basket(self.request)
|
kwargs["basket_total"] = self.sum_basket(self.request)
|
||||||
kwargs["refill_form"] = self.refill_form or RefillForm()
|
kwargs["refill_form"] = self.refill_form or RefillForm()
|
||||||
kwargs["barmens_can_refill"] = self.object.can_refill()
|
kwargs["barmens_can_refill"] = self.object.can_refill()
|
||||||
kwargs["student_card"] = StudentCardFormView.get_template_data(
|
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
|
||||||
self.customer
|
self.customer
|
||||||
).render(self.request)
|
).render(self.request)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -15,9 +15,10 @@
|
|||||||
|
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpRequest
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.edit import DeleteView, FormView
|
from django.views.generic.edit import DeleteView, FormView
|
||||||
|
|
||||||
from core.utils import FormFragmentTemplateData
|
from core.utils import FormFragmentTemplateData
|
||||||
@ -32,16 +33,21 @@ class StudentCardDeleteView(DeleteView, CanEditMixin):
|
|||||||
|
|
||||||
model = StudentCard
|
model = StudentCard
|
||||||
template_name = "core/delete_confirm.jinja"
|
template_name = "core/delete_confirm.jinja"
|
||||||
pk_url_kwarg = "card_id"
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
|
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_object(self, queryset=None):
|
||||||
return reverse_lazy(
|
if not hasattr(self.customer, "student_card"):
|
||||||
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
|
raise Http404(
|
||||||
|
_("%(name)s has no registered student card")
|
||||||
|
% {"name": self.customer.user.get_full_name()}
|
||||||
)
|
)
|
||||||
|
return self.customer.student_card
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id})
|
||||||
|
|
||||||
|
|
||||||
class StudentCardFormView(FormView):
|
class StudentCardFormView(FormView):
|
||||||
@ -53,23 +59,22 @@ class StudentCardFormView(FormView):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_template_data(
|
def get_template_data(
|
||||||
cls, customer: Customer
|
cls, customer: Customer
|
||||||
) -> FormFragmentTemplateData[form_class]:
|
) -> FormFragmentTemplateData[StudentCardForm]:
|
||||||
"""Get necessary data to pre-render the fragment"""
|
"""Get necessary data to pre-render the fragment"""
|
||||||
return FormFragmentTemplateData[cls.form_class](
|
return FormFragmentTemplateData(
|
||||||
form=cls.form_class(),
|
form=cls.form_class(),
|
||||||
template=cls.template_name,
|
template=cls.template_name,
|
||||||
context={
|
context={
|
||||||
"action": reverse_lazy(
|
"action": reverse(
|
||||||
"counter:add_student_card", kwargs={"customer_id": customer.pk}
|
"counter:add_student_card", kwargs={"customer_id": customer.pk}
|
||||||
),
|
),
|
||||||
"customer": customer,
|
"customer": customer,
|
||||||
"student_cards": customer.student_cards.all(),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, *args, **kwargs):
|
def dispatch(self, request: HttpRequest, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(
|
self.customer = get_object_or_404(
|
||||||
Customer.objects.prefetch_related("student_cards"), pk=kwargs["customer_id"]
|
Customer.objects.select_related("student_card"), pk=kwargs["customer_id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not is_logged_in_counter(request) and not StudentCard.can_create(
|
if not is_logged_in_counter(request) and not StudentCard.can_create(
|
||||||
@ -79,11 +84,12 @@ class StudentCardFormView(FormView):
|
|||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form: StudentCardForm) -> HttpResponse:
|
||||||
data = form.clean()
|
data = form.clean()
|
||||||
res = super(FormView, self).form_valid(form)
|
StudentCard.objects.update_or_create(
|
||||||
StudentCard(customer=self.customer, uid=data["uid"]).save()
|
customer=self.customer, defaults={"uid": data["uid"]}
|
||||||
return res
|
)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-12-09 12:28+0100\n"
|
"POT-Creation-Date: 2024-12-11 09:34+0100\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@ -369,7 +369,7 @@ msgstr "Compte en banque : "
|
|||||||
#: core/templates/core/user_clubs.jinja:34
|
#: core/templates/core/user_clubs.jinja:34
|
||||||
#: core/templates/core/user_clubs.jinja:63
|
#: core/templates/core/user_clubs.jinja:63
|
||||||
#: core/templates/core/user_edit.jinja:62
|
#: core/templates/core/user_edit.jinja:62
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja:21
|
#: counter/templates/counter/fragments/create_student_card.jinja:18
|
||||||
#: counter/templates/counter/last_ops.jinja:35
|
#: counter/templates/counter/last_ops.jinja:35
|
||||||
#: counter/templates/counter/last_ops.jinja:65
|
#: counter/templates/counter/last_ops.jinja:65
|
||||||
#: election/templates/election/election_detail.jinja:191
|
#: election/templates/election/election_detail.jinja:191
|
||||||
@ -950,11 +950,11 @@ msgstr "Une action est requise"
|
|||||||
msgid "You must specify at least an user or an email address"
|
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"
|
msgstr "vous devez spécifier au moins un utilisateur ou une adresse email"
|
||||||
|
|
||||||
#: club/forms.py:149 counter/forms.py:193
|
#: club/forms.py:149 counter/forms.py:189
|
||||||
msgid "Begin date"
|
msgid "Begin date"
|
||||||
msgstr "Date de début"
|
msgstr "Date de début"
|
||||||
|
|
||||||
#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:196
|
#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:192
|
||||||
#: election/views.py:170 subscription/forms.py:21
|
#: election/views.py:170 subscription/forms.py:21
|
||||||
msgid "End date"
|
msgid "End date"
|
||||||
msgstr "Date de fin"
|
msgstr "Date de fin"
|
||||||
@ -3048,7 +3048,7 @@ msgstr "Facture eboutic"
|
|||||||
msgid "Etickets"
|
msgid "Etickets"
|
||||||
msgstr "Etickets"
|
msgstr "Etickets"
|
||||||
|
|
||||||
#: core/templates/core/user_account.jinja:69 core/views/user.py:639
|
#: core/templates/core/user_account.jinja:69 core/views/user.py:633
|
||||||
msgid "User has no account"
|
msgid "User has no account"
|
||||||
msgstr "L'utilisateur n'a pas de compte"
|
msgstr "L'utilisateur n'a pas de compte"
|
||||||
|
|
||||||
@ -3371,7 +3371,7 @@ msgstr "Cotisations"
|
|||||||
msgid "Subscription stats"
|
msgid "Subscription stats"
|
||||||
msgstr "Statistiques de cotisation"
|
msgstr "Statistiques de cotisation"
|
||||||
|
|
||||||
#: core/templates/core/user_tools.jinja:48 counter/forms.py:166
|
#: core/templates/core/user_tools.jinja:48 counter/forms.py:162
|
||||||
#: counter/views/mixins.py:89
|
#: counter/views/mixins.py:89
|
||||||
msgid "Counters"
|
msgid "Counters"
|
||||||
msgstr "Comptoirs"
|
msgstr "Comptoirs"
|
||||||
@ -3543,7 +3543,7 @@ msgstr "Parrain / Marraine"
|
|||||||
msgid "Godchild"
|
msgid "Godchild"
|
||||||
msgstr "Fillot / Fillote"
|
msgstr "Fillot / Fillote"
|
||||||
|
|
||||||
#: core/views/forms.py:310 counter/forms.py:80 trombi/views.py:151
|
#: core/views/forms.py:310 counter/forms.py:78 trombi/views.py:151
|
||||||
msgid "Select user"
|
msgid "Select user"
|
||||||
msgstr "Choisir un utilisateur"
|
msgstr "Choisir un utilisateur"
|
||||||
|
|
||||||
@ -3596,11 +3596,11 @@ msgstr "Galaxie"
|
|||||||
msgid "counter"
|
msgid "counter"
|
||||||
msgstr "comptoir"
|
msgstr "comptoir"
|
||||||
|
|
||||||
#: counter/forms.py:61
|
#: counter/forms.py:59
|
||||||
msgid "This UID is invalid"
|
msgid "This UID is invalid"
|
||||||
msgstr "Cet UID est invalide"
|
msgstr "Cet UID est invalide"
|
||||||
|
|
||||||
#: counter/forms.py:109
|
#: counter/forms.py:107
|
||||||
msgid "User not found"
|
msgid "User not found"
|
||||||
msgstr "Utilisateur non trouvé"
|
msgstr "Utilisateur non trouvé"
|
||||||
|
|
||||||
@ -3862,9 +3862,13 @@ msgstr "secret"
|
|||||||
msgid "uid"
|
msgid "uid"
|
||||||
msgstr "uid"
|
msgstr "uid"
|
||||||
|
|
||||||
#: counter/models.py:1144
|
#: counter/models.py:1144 counter/models.py:1149
|
||||||
|
msgid "student card"
|
||||||
|
msgstr "carte étudiante"
|
||||||
|
|
||||||
|
#: counter/models.py:1150
|
||||||
msgid "student cards"
|
msgid "student cards"
|
||||||
msgstr "cartes étudiante"
|
msgstr "cartes étudiantes"
|
||||||
|
|
||||||
#: counter/templates/counter/activity.jinja:5
|
#: counter/templates/counter/activity.jinja:5
|
||||||
#: counter/templates/counter/activity.jinja:13
|
#: counter/templates/counter/activity.jinja:13
|
||||||
@ -3929,7 +3933,7 @@ msgstr "Vente"
|
|||||||
|
|
||||||
#: counter/templates/counter/counter_click.jinja:50
|
#: counter/templates/counter/counter_click.jinja:50
|
||||||
#: counter/templates/counter/counter_click.jinja:115
|
#: counter/templates/counter/counter_click.jinja:115
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja:10
|
#: counter/templates/counter/fragments/create_student_card.jinja:11
|
||||||
#: counter/templates/counter/invoices_call.jinja:16
|
#: counter/templates/counter/invoices_call.jinja:16
|
||||||
#: launderette/templates/launderette/launderette_admin.jinja:35
|
#: launderette/templates/launderette/launderette_admin.jinja:35
|
||||||
#: launderette/templates/launderette/launderette_click.jinja:13
|
#: launderette/templates/launderette/launderette_click.jinja:13
|
||||||
@ -4032,17 +4036,17 @@ msgid "There is no eticket in this website."
|
|||||||
msgstr "Il n'y a pas de eticket sur ce site web."
|
msgstr "Il n'y a pas de eticket sur ce site web."
|
||||||
|
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja:2
|
#: counter/templates/counter/fragments/create_student_card.jinja:2
|
||||||
msgid "Add a student card"
|
msgid "Student card"
|
||||||
msgstr "Ajouter une carte étudiante"
|
msgstr "Carte étudiante"
|
||||||
|
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja:13
|
#: counter/templates/counter/fragments/create_student_card.jinja:13
|
||||||
msgid "Registered cards"
|
|
||||||
msgstr "Cartes enregistrées"
|
|
||||||
|
|
||||||
#: counter/templates/counter/fragments/create_student_card.jinja:27
|
|
||||||
msgid "No student card registered."
|
msgid "No student card registered."
|
||||||
msgstr "Aucune carte étudiante enregistrée."
|
msgstr "Aucune carte étudiante enregistrée."
|
||||||
|
|
||||||
|
#: counter/templates/counter/fragments/create_student_card.jinja:16
|
||||||
|
msgid "Registered"
|
||||||
|
msgstr "Enregistré"
|
||||||
|
|
||||||
#: counter/templates/counter/invoices_call.jinja:8
|
#: counter/templates/counter/invoices_call.jinja:8
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Invoices call for %(date)s"
|
msgid "Invoices call for %(date)s"
|
||||||
@ -4313,6 +4317,11 @@ msgstr "Administration des comptoirs"
|
|||||||
msgid "Product types"
|
msgid "Product types"
|
||||||
msgstr "Types de produit"
|
msgstr "Types de produit"
|
||||||
|
|
||||||
|
#: counter/views/student_card.py:44
|
||||||
|
#, python-format
|
||||||
|
msgid "%(name)s has no registered student card"
|
||||||
|
msgstr "%(name)s n'a pas de carte étudiante enregistrée"
|
||||||
|
|
||||||
#: eboutic/forms.py:88
|
#: eboutic/forms.py:88
|
||||||
msgid "The request was badly formatted."
|
msgid "The request was badly formatted."
|
||||||
msgstr "La requête a été mal formatée."
|
msgstr "La requête a été mal formatée."
|
||||||
|
Loading…
Reference in New Issue
Block a user