diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py
index 9e261bba..7098101a 100644
--- a/core/management/commands/populate.py
+++ b/core/management/commands/populate.py
@@ -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"),
diff --git a/core/static/core/colors.scss b/core/static/core/colors.scss
index 5453dd34..35dc6a69 100644
--- a/core/static/core/colors.scss
+++ b/core/static/core/colors.scss
@@ -29,4 +29,9 @@ $shadow-color: rgb(223, 223, 223);
$background-button-color: hsl(0, 0%, 95%);
-$deepblue: #354a5f;
\ No newline at end of file
+$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;
+}
\ No newline at end of file
diff --git a/core/static/core/style.scss b/core/static/core/style.scss
index 50892df3..cbe8d326 100644
--- a/core/static/core/style.scss
+++ b/core/static/core/style.scss
@@ -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;
}
diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja
index 722e7c44..13bee656 100644
--- a/core/templates/core/user_preferences.jinja
+++ b/core/templates/core/user_preferences.jinja
@@ -35,8 +35,9 @@
{% endif %}
- {% if student_card %}
- {{ student_card }}
+ {% if student_card_fragment %}
+
{% trans %}Student card{% endtrans %}
+ {{ student_card_fragment }}
{% 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 %}
diff --git a/core/views/user.py b/core/views/user.py
index 2c6b01fc..9f724fca 100644
--- a/core/views/user.py
+++ b/core/views/user.py
@@ -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
diff --git a/counter/forms.py b/counter/forms.py
index 538b387c..0a8bb3be 100644
--- a/counter/forms.py
+++ b/counter/forms.py
@@ -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()
diff --git a/counter/migrations/0026_alter_studentcard_customer.py b/counter/migrations/0026_alter_studentcard_customer.py
new file mode 100644
index 00000000..f1f5cd49
--- /dev/null
+++ b/counter/migrations/0026_alter_studentcard_customer.py
@@ -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",
+ },
+ ),
+ ]
diff --git a/counter/models.py b/counter/models.py
index cf285839..292cd59f 100644
--- a/counter/models.py
+++ b/counter/models.py
@@ -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
diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja
index 7c36b01b..3df77555 100644
--- a/counter/templates/counter/counter_click.jinja
+++ b/counter/templates/counter/counter_click.jinja
@@ -31,7 +31,8 @@
{% trans %}Amount: {% endtrans %}{{ customer.amount }} €
{% if counter.type == 'BAR' %}
- {{ student_card }}
+ {% trans %}Student card{% endtrans %}
+ {{ student_card_fragment }}
{% endif %}
diff --git a/counter/templates/counter/fragments/create_student_card.jinja b/counter/templates/counter/fragments/create_student_card.jinja
index ab846c55..f15716e4 100644
--- a/counter/templates/counter/fragments/create_student_card.jinja
+++ b/counter/templates/counter/fragments/create_student_card.jinja
@@ -1,29 +1,29 @@
diff --git a/counter/templates/counter/fragments/delete_student_card.jinja b/counter/templates/counter/fragments/delete_student_card.jinja
new file mode 100644
index 00000000..94be1c47
--- /dev/null
+++ b/counter/templates/counter/fragments/delete_student_card.jinja
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py
index f7e599e6..21f5604c 100644
--- a/counter/tests/test_customer.py
+++ b/counter/tests/test_customer.py
@@ -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):
diff --git a/counter/urls.py b/counter/urls.py
index e196894f..fa659ba0 100644
--- a/counter/urls.py
+++ b/counter/urls.py
@@ -81,7 +81,7 @@ urlpatterns = [
name="add_student_card",
),
path(
- "customer//card/delete//",
+ "customer//card/delete/",
StudentCardDeleteView.as_view(),
name="delete_student_card",
),
diff --git a/counter/views/click.py b/counter/views/click.py
index 2fa9684d..65e889ff 100644
--- a/counter/views/click.py
+++ b/counter/views/click.py
@@ -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
diff --git a/counter/views/student_card.py b/counter/views/student_card.py
index 99f67316..35226e95 100644
--- a/counter/views/student_card.py
+++ b/counter/views/student_card.py
@@ -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)
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index e122343d..482befd3 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 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 \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."