Merge pull request #924 from ae-utbm/unique-student-card

Make student card unique per user
This commit is contained in:
NaNoMelo 2024-12-15 17:06:35 +01:00 committed by GitHub
commit 0f003870bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 355 additions and 436 deletions

View File

@ -69,7 +69,7 @@ class Command(BaseCommand):
# sqlite doesn't support this operation
return
sqlcmd = StringIO()
call_command("sqlsequencereset", *args, stdout=sqlcmd)
call_command("sqlsequencereset", "--no-color", *args, stdout=sqlcmd)
cursor = connection.cursor()
cursor.execute(sqlcmd.getvalue())
@ -137,11 +137,10 @@ class Command(BaseCommand):
)
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 = [
*[
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="AE", club=main_club, type="OFFICE"),
Counter(name="Vidage comptes AE", club=main_club, type="OFFICE"),

View File

@ -30,3 +30,8 @@ $shadow-color: rgb(223, 223, 223);
$background-button-color: hsl(0, 0%, 95%);
$deepblue: #354a5f;
@mixin shadow {
box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0,
rgba(60, 64, 67, 0.15) 0 4px 8px 3px;
}

View File

@ -42,6 +42,32 @@ body {
}
}
[tooltip] {
position: relative;
}
[tooltip]::before {
@include shadow;
opacity: 0;
z-index: 1;
content: attr(tooltip);
background: hsl(219.6, 20.8%, 96%);
color: $black-color;
border: 0.5px solid hsl(0, 0%, 50%);
;
border-radius: 5px;
padding: 5px;
top: 1em;
position: absolute;
margin-top: 5px;
white-space: nowrap;
transition: opacity 500ms ease-out;
}
[tooltip]:hover::before {
opacity: 1;
}
.ib {
display: inline-block;
padding: 1px;
@ -79,8 +105,7 @@ body {
}
.shadow {
box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0,
rgba(60, 64, 67, 0.15) 0 4px 8px 3px;
@include shadow;
}
.w_big {
@ -308,6 +333,7 @@ body {
font-size: 120%;
background-color: unset;
position: relative;
&:after {
content: '';
position: absolute;
@ -318,14 +344,17 @@ body {
border-radius: 2px;
transition: all 0.2s ease-in-out;
}
&:hover:after {
border-bottom-color: darken($primary-neutral-light-color, 20%);
}
&.active:after {
border-bottom-color: $primary-dark-color;
}
}
}
section {
padding: 20px;
}

View File

@ -35,8 +35,9 @@
{% endif %}
{% if student_card %}
{{ student_card }}
{% if student_card_fragment %}
<h3>{% trans %}Student card{% endtrans %}</h3>
{{ student_card_fragment }}
<p class="justify">
{% 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 %}

View File

@ -559,10 +559,6 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
context_object_name = "profile"
current_tab = "prefs"
def get_object(self, queryset=None):
user = get_object_or_404(User, pk=self.kwargs["user_id"])
return user
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
pref = self.object.preferences
@ -572,12 +568,10 @@ class UserPreferencesView(UserTabsMixin, CanEditMixin, UpdateView):
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
if not (
hasattr(self.object, "trombi_user") and self.request.user.trombi_user.trombi
):
if not hasattr(self.object, "trombi_user"):
kwargs["trombi_form"] = UserTrombiForm()
if hasattr(self.object, "customer"):
kwargs["student_card"] = StudentCardFormView.get_template_data(
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
self.object.customer
).render(self.request)
return kwargs

View File

@ -50,9 +50,7 @@ class StudentCardForm(forms.ModelForm):
class Meta:
model = StudentCard
fields = ["uid"]
widgets = {
"uid": NFCTextInput,
}
widgets = {"uid": NFCTextInput}
def clean(self):
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"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)]
)
customer = models.ForeignKey(
customer = models.OneToOneField(
Customer,
related_name="student_cards",
verbose_name=_("student cards"),
null=False,
blank=False,
related_name="student_card",
verbose_name=_("student card"),
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("student card")
verbose_name_plural = _("student cards")
def __str__(self):
return self.uid
@staticmethod
def is_valid(uid):
def is_valid(uid: str) -> bool:
return (
(uid.isupper() or uid.isnumeric())
and len(uid) == StudentCard.UID_SIZE

View File

@ -31,7 +31,8 @@
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
{% if counter.type == 'BAR' %}
{{ student_card }}
<h5>{% trans %}Student card{% endtrans %}</h3>
{{ student_card_fragment }}
{% endif %}
</div>

View File

@ -1,29 +1,29 @@
<div id="student_card_form">
<h3>{% trans %}Add a student card{% endtrans %}</h3>
<form
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
>
{% csrf_token %}
{{ form.as_p() }}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if student_cards %}
<ul>
{% for card in student_cards %}
<li>
{{ card.uid }}
<a href="{{ url('counter:delete_student_card', customer_id=customer.pk, card_id=card.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</li>
{% endfor %}
</ul>
{% else %}
{% if not customer.student_card %}
<form
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
>
{% csrf_token %}
{{ form.as_p() }}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
{% else %}
<p>
<span tooltip="{% trans uid=customer.student_card.uid %}uid: {{ uid }} {% endtrans %}">
{% trans %}Card registered{% endtrans %}
<i class="fa fa-check" style="color: green"></i>
</span>
&nbsp; - &nbsp;
<button
hx-get="{{ url('counter:delete_student_card', customer_id=customer.pk) }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
>
{% trans %}Delete{% endtrans %}
</button>
</p>
{% endif %}
</div>

View File

@ -0,0 +1,15 @@
<div id="student_card_form">
<form hx-post="{{ action }}" hx-swap="outerHTML" hx-target="#student_card_form">
{% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" />
<input
hx-get="{{ action_cancel }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
type="submit"
name="cancel"
value="{% trans %}Cancel{% endtrans %}"
/>
</form>
</div>

View File

@ -1,3 +1,4 @@
import itertools
import json
import string
from datetime import timedelta
@ -175,7 +176,6 @@ class TestStudentCard(TestCase):
@classmethod
def setUpTestData(cls):
cls.customer = subscriber_user.make()
cls.customer.save()
cls.barmen = subscriber_user.make(password=make_password("plop"))
cls.board_admin = board_user.make()
cls.club_admin = baker.make(User)
@ -198,14 +198,30 @@ class TestStudentCard(TestCase):
StudentCard, customer=cls.customer.customer, uid="8A89B82018B0A0"
)
def setUp(self):
# Auto login on counter
def login_in_counter(self):
self.client.post(
reverse("counter:login", args=[self.counter.id]),
{"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):
self.login_in_counter()
response = self.client.post(
reverse("counter:details", args=[self.counter.id]),
{"code": self.valid_card.uid},
@ -213,396 +229,167 @@ class TestStudentCard(TestCase):
assert response.url == reverse(
"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):
# Test card with mixed letters and numbers
response = self.client.post(
reverse(
"counter:add_student_card",
kwargs={
"customer_id": self.customer.customer.pk,
},
),
{"uid": "8B90734A802A8F"},
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="8B90734A802A8F")
# Test card with only numbers
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")
self.login_in_counter()
for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]:
customer = subscriber_user.make().customer
response = self.client.post(
reverse(
"counter:add_student_card", kwargs={"customer_id": customer.pk}
),
{"uid": uid},
HTTP_REFERER=reverse(
"counter:click",
kwargs={"counter_id": self.counter.id, "user_id": customer.pk},
),
)
assert response.status_code == 302
customer.refresh_from_db()
assert hasattr(customer, "student_card")
assert customer.student_card.uid == uid
def test_add_student_card_from_counter_fail(self):
# UID too short
response = self.client.post(
reverse(
"counter:add_student_card",
kwargs={
"customer_id": self.customer.customer.pk,
},
),
{"uid": "8B90734A802A8"},
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")
# UID too long
response = self.client.post(
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.")
self.login_in_counter()
customer = subscriber_user.make().customer
for uid, error_msg in self.invalid_uids():
response = self.client.post(
reverse(
"counter:add_student_card", kwargs={"customer_id": customer.pk}
),
{"uid": uid},
HTTP_REFERER=reverse(
"counter:click",
kwargs={"counter_id": self.counter.id, "user_id": customer.pk},
),
)
self.assertContains(response, text="Cet UID est invalide")
self.assertContains(response, text=error_msg)
customer.refresh_from_db()
assert not hasattr(customer, "student_card")
def test_add_student_card_from_counter_unauthorized(self):
# Send to a counter where you aren't logged in
self.client.post(
reverse("counter:logout", args=[self.counter.id]),
{"user_id": self.barmen.id},
)
def send_valid_request(client, counter_id):
return client.post(
reverse(
"counter:add_student_card",
kwargs={
"customer_id": self.customer.customer.pk,
},
"counter:add_student_card", kwargs={"customer_id": self.customer.pk}
),
{"uid": "8B90734A802A8F"},
HTTP_REFERER=reverse(
"counter:click",
kwargs={
"counter_id": counter_id,
"user_id": self.customer.customer.pk,
},
kwargs={"counter_id": counter_id, "user_id": self.customer.pk},
),
)
# Send to a counter where you aren't logged in
assert send_valid_request(self.client, self.counter.id).status_code == 403
self.login_in_counter()
barman = subscriber_user.make()
self.counter.sellers.add(barman)
# We want to test sending requests from another counter while
# we are currently registered to another counter
# so we connect to a counter and
# 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()
# Send to a counter where you aren't logged in
assert send_valid_request(client, self.counter.id).status_code == 403
# Send to a non bar counter
self.client.force_login(self.club_admin)
assert send_valid_request(self.client, self.club_counter.id).status_code == 403
client.force_login(self.club_admin)
assert send_valid_request(client, self.club_counter.id).status_code == 403
def test_delete_student_card_with_owner(self):
self.client.force_login(self.customer)
self.client.post(
reverse(
"counter:delete_student_card",
kwargs={
"customer_id": self.customer.customer.pk,
"card_id": self.customer.customer.student_cards.first().id,
},
kwargs={"customer_id": self.customer.customer.pk},
)
)
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):
self.client.force_login(self.board_admin)
def test_delete_student_card_with_admin_user(self):
"""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(
reverse(
"counter:delete_student_card",
kwargs={"customer_id": self.customer.customer.pk},
)
)
self.customer.customer.refresh_from_db()
assert not hasattr(self.customer.customer, "student_card")
def test_delete_student_card_from_counter(self):
self.login_in_counter()
self.client.post(
reverse(
"counter:delete_student_card",
kwargs={"customer_id": self.customer.customer.pk},
),
http_referer=reverse(
"counter:click",
kwargs={
"customer_id": self.customer.customer.pk,
"card_id": self.customer.customer.student_cards.first().id,
"counter_id": self.counter.id,
"user_id": self.customer.customer.pk,
},
)
),
)
assert not self.customer.customer.student_cards.exists()
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()
self.customer.customer.refresh_from_db()
assert not hasattr(self.customer.customer, "student_card")
def test_delete_student_card_fail(self):
"""Test that non-admin users cannot delete student cards"""
self.client.force_login(self.subscriber)
response = self.client.post(
reverse(
"counter:delete_student_card",
kwargs={
"customer_id": self.customer.customer.pk,
"card_id": self.customer.customer.student_cards.first().id,
},
kwargs={"customer_id": self.customer.customer.pk},
)
)
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):
# Test with owner of the card
self.client.force_login(self.customer)
response = self.client.post(
reverse(
"counter:add_student_card",
kwargs={"customer_id": self.customer.customer.pk},
),
{"uid": "8B90734A802A8F"},
)
users = [self.customer, self.board_admin, self.root]
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(
reverse(
"counter:add_student_card",
kwargs={"customer_id": self.customer.customer.pk},
),
{"uid": uid},
)
assert response.status_code == 302
response = self.client.get(response.url)
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
response = self.client.get(response.url)
self.assertContains(response, 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"},
)
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")
self.customer.customer.refresh_from_db()
assert self.customer.customer.student_card.uid == uid
self.assertContains(response, text="Carte enregistrée")
def test_add_student_card_from_user_preferences_fail(self):
self.client.force_login(self.customer)
# UID too short
response = self.client.post(
reverse(
"counter:add_student_card",
kwargs={"customer_id": self.customer.customer.pk},
),
{"uid": "8B90734A802A8"},
)
self.assertContains(response, text="Cet UID est invalide")
# UID too long
response = self.client.post(
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
customer = subscriber_user.make()
self.client.force_login(customer)
for uid, error_msg in self.invalid_uids():
url = reverse(
"counter:add_student_card", kwargs={"customer_id": customer.customer.pk}
)
response = self.client.post(url, {"uid": uid})
self.assertContains(response, text="Cet UID est invalide")
self.assertContains(response, text=error_msg)
customer.refresh_from_db()
assert not hasattr(customer.customer, "student_card")
class TestCustomerAccountId(TestCase):

View File

@ -81,7 +81,7 @@ urlpatterns = [
name="add_student_card",
),
path(
"customer/<int:customer_id>/card/delete/<int:card_id>/",
"customer/<int:customer_id>/card/delete/",
StudentCardDeleteView.as_view(),
name="delete_student_card",
),

View File

@ -415,7 +415,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
kwargs["basket_total"] = self.sum_basket(self.request)
kwargs["refill_form"] = self.refill_form or RefillForm()
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
).render(self.request)
return kwargs

View File

@ -15,32 +15,50 @@
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.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 core.utils import FormFragmentTemplateData
from core.views import CanEditMixin
from core.views import can_edit
from counter.forms import StudentCardForm
from counter.models import Customer, StudentCard
from counter.utils import is_logged_in_counter
class StudentCardDeleteView(DeleteView, CanEditMixin):
"""View used to delete a card from a user."""
class StudentCardDeleteView(DeleteView):
"""View used to delete a card from a user. This is a fragment view !"""
model = StudentCard
template_name = "core/delete_confirm.jinja"
pk_url_kwarg = "card_id"
template_name = "counter/fragments/delete_student_card.jinja"
def dispatch(self, request, *args, **kwargs):
def dispatch(self, request: HttpRequest, *args, **kwargs):
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
if not is_logged_in_counter(request) and not can_edit(
self.get_object(), request.user
):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["action"] = self.request.path
context["action_cancel"] = self.get_success_url()
return context
def get_object(self, queryset=None):
if not hasattr(self.customer, "student_card"):
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_lazy(
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
return reverse(
"counter:add_student_card", kwargs={"customer_id": self.customer.pk}
)
@ -53,23 +71,22 @@ class StudentCardFormView(FormView):
@classmethod
def get_template_data(
cls, customer: Customer
) -> FormFragmentTemplateData[form_class]:
) -> FormFragmentTemplateData[StudentCardForm]:
"""Get necessary data to pre-render the fragment"""
return FormFragmentTemplateData[cls.form_class](
return FormFragmentTemplateData(
form=cls.form_class(),
template=cls.template_name,
context={
"action": reverse_lazy(
"action": reverse(
"counter:add_student_card", kwargs={"customer_id": customer.pk}
),
"customer": customer,
"student_cards": customer.student_cards.all(),
},
)
def dispatch(self, request: HttpRequest, *args, **kwargs):
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(
@ -79,11 +96,12 @@ class StudentCardFormView(FormView):
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
def form_valid(self, form: StudentCardForm) -> HttpResponse:
data = form.clean()
res = super(FormView, self).form_valid(form)
StudentCard(customer=self.customer, uid=data["uid"]).save()
return res
StudentCard.objects.update_or_create(
customer=self.customer, defaults={"uid": data["uid"]}
)
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

View File

@ -6,7 +6,7 @@
msgid ""
msgstr ""
"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"
"Last-Translator: Maréchal <thomas.girod@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:63
#: core/templates/core/user_edit.jinja:62
#: counter/templates/counter/fragments/create_student_card.jinja:21
#: counter/templates/counter/fragments/create_student_card.jinja:22
#: counter/templates/counter/last_ops.jinja:35
#: counter/templates/counter/last_ops.jinja:65
#: 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"
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"
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
msgid "End date"
msgstr "Date de fin"
@ -2574,18 +2574,21 @@ msgstr "Confirmation de suppression"
#: core/templates/core/delete_confirm.jinja:16
#: core/templates/core/file_delete_confirm.jinja:29
#: counter/templates/counter/fragments/delete_student_card.jinja:4
#, python-format
msgid "Are you sure you want to delete \"%(obj)s\"?"
msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?"
#: core/templates/core/delete_confirm.jinja:17
#: core/templates/core/file_delete_confirm.jinja:36
#: counter/templates/counter/fragments/delete_student_card.jinja:5
msgid "Confirm"
msgstr "Confirmation"
#: core/templates/core/delete_confirm.jinja:20
#: core/templates/core/file_delete_confirm.jinja:46
#: counter/templates/counter/counter_click.jinja:104
#: counter/templates/counter/fragments/delete_student_card.jinja:12
#: sas/templates/sas/ask_picture_removal.jinja:20
msgid "Cancel"
msgstr "Annuler"
@ -3048,7 +3051,7 @@ msgstr "Facture eboutic"
msgid "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"
msgstr "L'utilisateur n'a pas de compte"
@ -3371,7 +3374,7 @@ msgstr "Cotisations"
msgid "Subscription stats"
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
msgid "Counters"
msgstr "Comptoirs"
@ -3543,7 +3546,7 @@ msgstr "Parrain / Marraine"
msgid "Godchild"
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"
msgstr "Choisir un utilisateur"
@ -3596,11 +3599,11 @@ msgstr "Galaxie"
msgid "counter"
msgstr "comptoir"
#: counter/forms.py:61
#: counter/forms.py:59
msgid "This UID is invalid"
msgstr "Cet UID est invalide"
#: counter/forms.py:109
#: counter/forms.py:107
msgid "User not found"
msgstr "Utilisateur non trouvé"
@ -3862,9 +3865,13 @@ msgstr "secret"
msgid "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"
msgstr "cartes étudiante"
msgstr "cartes étudiantes"
#: counter/templates/counter/activity.jinja:5
#: counter/templates/counter/activity.jinja:13
@ -3929,7 +3936,7 @@ msgstr "Vente"
#: counter/templates/counter/counter_click.jinja:50
#: 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
#: launderette/templates/launderette/launderette_admin.jinja:35
#: launderette/templates/launderette/launderette_click.jinja:13
@ -4032,17 +4039,22 @@ msgid "There is no eticket in this website."
msgstr "Il n'y a pas de eticket sur ce site web."
#: counter/templates/counter/fragments/create_student_card.jinja:2
msgid "Add a student card"
msgstr "Ajouter une carte étudiante"
msgid "Student card"
msgstr "Carte étudiante"
#: 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."
msgstr "Aucune carte étudiante enregistrée."
#: counter/templates/counter/fragments/create_student_card.jinja:16
msgid "Card registered"
msgstr "Carte enregistrée"
#: counter/templates/counter/fragments/create_student_card.jinja:17
#, python-format
msgid "uid: %(uid)s "
msgstr "uid: %(uid)s"
#: counter/templates/counter/invoices_call.jinja:8
#, python-format
msgid "Invoices call for %(date)s"
@ -4313,6 +4325,11 @@ msgstr "Administration des comptoirs"
msgid "Product types"
msgstr "Types de produit"
#: counter/views/student_card.py:54
#, 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
msgid "The request was badly formatted."
msgstr "La requête a été mal formatée."