feat: make student card unique per user

This commit is contained in:
imperosol 2024-12-08 16:07:25 +01:00 committed by Sli
parent 3b7e338808
commit 466fe58763
13 changed files with 250 additions and 426 deletions

View File

@ -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"),

View File

@ -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 %}

View File

@ -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

View File

@ -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()

View 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",
},
),
]

View File

@ -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

View File

@ -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>

View File

@ -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> &nbsp; - &nbsp;
{% 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>

View File

@ -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):

View File

@ -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",
), ),

View File

@ -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

View File

@ -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)

View File

@ -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."