Merge pull request #1413 from ae-utbm/counter-barmen

feat: `request.barmen`
This commit is contained in:
thomas girod
2026-06-05 00:31:19 +02:00
committed by GitHub
20 changed files with 438 additions and 299 deletions
+2 -7
View File
@@ -46,7 +46,7 @@ from django.http import HttpRequest
from ninja_extra import ControllerBase from ninja_extra import ControllerBase
from ninja_extra.permissions import BasePermission from ninja_extra.permissions import BasePermission
from counter.models import Counter from counter.utils import is_logged_in_counter
class IsInGroup(BasePermission): class IsInGroup(BasePermission):
@@ -186,12 +186,7 @@ class IsLoggedInCounter(BasePermission):
"""Check that a user is logged in a counter.""" """Check that a user is logged in a counter."""
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool: def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
if "/counter/" not in request.META.get("HTTP_REFERER", ""): return is_logged_in_counter(request)
return False
token = request.session.get("counter_token")
if not token:
return False
return Counter.objects.filter(token=token).exists()
CanAccessLookup = IsLoggedInCounter | HasPerm("core.access_lookup") CanAccessLookup = IsLoggedInCounter | HasPerm("core.access_lookup")
+1 -2
View File
@@ -25,8 +25,7 @@ class Migration(migrations.Migration):
"url_base", "url_base",
models.URLField( models.URLField(
help_text=( help_text=(
"The base url that links with this type " "The base url that links with this type must respect"
"must respect (e.g. `https://www.instagram.com`)"
), ),
unique=True, unique=True,
verbose_name="url base", verbose_name="url base",
+1 -4
View File
@@ -793,10 +793,7 @@ class LinkType(models.Model):
url_base = models.URLField( url_base = models.URLField(
"url base", "url base",
unique=True, unique=True,
help_text=_( help_text=_("The base url that links with this type must respect"),
"The base url that links with this type must respect (e.g. `%(url)s`)"
)
% {"url": "https://www.instagram.com"},
) )
icon = models.CharField( icon = models.CharField(
_("icon"), _("icon"),
+4 -3
View File
@@ -43,6 +43,7 @@ from core.models import BanGroup, Group, Page, PageRev, SithFile, User
from core.utils import resize_image from core.utils import resize_image
from counter.models import ( from counter.models import (
Counter, Counter,
CounterSellers,
Price, Price,
Product, Product,
ProductType, ProductType,
@@ -364,10 +365,10 @@ class Command(BaseCommand):
Counter.objects.create(name="Carte AE", club=clubs.refound, type="OFFICE") Counter.objects.create(name="Carte AE", club=clubs.refound, type="OFFICE")
# Add barman to counter # Add barman to counter
Counter.sellers.through.objects.bulk_create( CounterSellers.objects.bulk_create(
[ [
Counter.sellers.through(counter_id=1, user=skia), # MDE CounterSellers(counter_id=1, user=skia, is_regular=True), # MDE
Counter.sellers.through(counter_id=2, user=krophil), # Foyer CounterSellers(counter_id=2, user=krophil, is_regular=True), # Foyer
] ]
) )
+3 -8
View File
@@ -22,14 +22,9 @@
</form> </form>
<ul class="bars"> <ul class="bars">
{% cache 100 "counters_activity" %} {% cache 100 "counters_activity" %}
{# The sith has no periodic tasks manager {# It would be cleaner to handle the timeout with django-celery-beat,
and using cron jobs would be way too overkill here. but doing it here is simpler and less error-prone #}
Thus the barmen timeout is handled in the only place that {% do Counter.objects.filter(type="BAR").handle_timeout() %}
is loaded on every page : the header bar.
However, let's be clear : this has nothing to do here.
It's' merely a contrived workaround that should
replaced by a proper task manager as soon as possible. #}
{% set _ = Counter.objects.filter(type="BAR").handle_timeout() %}
{% endcache %} {% endcache %}
{% for bar in Counter.objects.annotate_has_barman(user).annotate_is_open().filter(type="BAR") %} {% for bar in Counter.objects.annotate_has_barman(user).annotate_is_open().filter(type="BAR") %}
<li> <li>
+35 -16
View File
@@ -9,6 +9,7 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.forms import BaseModelFormSet from django.forms import BaseModelFormSet
from django.http import HttpRequest
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import ClockedSchedule from django_celery_beat.models import ClockedSchedule
@@ -17,6 +18,7 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.models import Club from club.models import Club
from club.widgets.ajax_select import AutoCompleteSelectClub from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User, UserQuerySet from core.models import User, UserQuerySet
from core.views import LoginForm
from core.views.forms import ( from core.views.forms import (
FutureDateTimeField, FutureDateTimeField,
NFCTextInput, NFCTextInput,
@@ -91,30 +93,18 @@ class StudentCardForm(forms.ModelForm):
class GetUserForm(forms.Form): class GetUserForm(forms.Form):
"""The Form class aims at providing a valid user_id field in its cleaned data, in order to pass it to some view, """Find a user to show its click page."""
reverse function, or any other use.
The Form implements a nice JS widget allowing the user to type a customer account id, or search the database with
some nickname, first name, or last name (TODO)
"""
code = forms.CharField( code = forms.CharField(
label="Code", label="Code",
max_length=StudentCard.UID_SIZE, max_length=StudentCard.UID_SIZE,
required=False, required=False,
widget=NFCTextInput, widget=NFCTextInput(attrs={"autofocus": True}),
) )
id = forms.CharField( id = forms.CharField(
label=_("Select user"), label=_("Select user"), widget=AutoCompleteSelectUser, required=False
help_text=None,
widget=AutoCompleteSelectUser,
required=False,
) )
def as_p(self):
self.fields["code"].widget.attrs["autofocus"] = True
return super().as_p()
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
customer = None customer = None
@@ -136,11 +126,40 @@ class GetUserForm(forms.Form):
if customer is None or not customer.can_buy: if customer is None or not customer.can_buy:
raise forms.ValidationError(_("User not found")) raise forms.ValidationError(_("User not found"))
cleaned_data["user_id"] = customer.user.id cleaned_data["user_id"] = customer.user_id
cleaned_data["user"] = customer.user cleaned_data["user"] = customer.user
return cleaned_data return cleaned_data
class CounterLoginForm(LoginForm):
"""LoginForm to log a barman in a counter.
To be able to log in a counter, a user must :
- be part of the sellers of the given counter
- not being already logged in any counter
"""
def __init__(self, *args, request: HttpRequest, counter: Counter, **kwargs):
super().__init__(*args, **kwargs)
self.counter = counter
self.request = request
def confirm_login_allowed(self, user: User):
super().confirm_login_allowed(user)
if not self.counter.sellers.contains(user):
raise ValidationError(
message=_("You are not a barman of this counter."), code="not_barman"
)
if user in self.request.barmen:
message = (
_("You are already logged in this counter.")
if user in self.counter.barmen_list
else _("You are already logged in another counter.")
)
raise ValidationError(message=message, code="already_logged_in")
class RefillForm(forms.ModelForm): class RefillForm(forms.ModelForm):
allowed_refilling_methods = [ allowed_refilling_methods = [
Refilling.PaymentMethod.CASH, Refilling.PaymentMethod.CASH,
+64
View File
@@ -0,0 +1,64 @@
from typing import TYPE_CHECKING, Callable
from django.db.models import Exists, OuterRef
from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject, empty
from core.models import User
from counter.models import Permanency
if TYPE_CHECKING:
from django.contrib.sessions.backends.base import SessionBase
SESSION_BARMEN_KEY = "barmen_ids"
def get_cached_barmen(request: HttpRequest) -> set[User]:
if not hasattr(request, "_cached_barmen"):
session: SessionBase = request.session
barmen_ids = session.get(SESSION_BARMEN_KEY, [])
if barmen_ids:
request._cached_barmen = set(
User.objects.filter(
Exists(Permanency.objects.filter(user=OuterRef("pk"), end=None)),
id__in=barmen_ids,
)
)
else:
request._cached_barmen = set()
return request._cached_barmen
class BarmenMiddleware:
"""Inject barmen logged in the current session.
In a similar fashion as `request.user`, `request.barmen` contains
users that are barmen in the current session, and ONLY them ;
if a user is logged as a barman on another session,
it will not be in `request.barmen`.
Notes:
In case of ended permanence, users will be automatically
removed from `request.barmen`.
However, in case of newly started permanence, this middleware
cannot add new barmen in the session data, so that operation
must be explicitly done in the barman login view.
"""
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response
def __call__(self, request: HttpRequest):
request.barmen = SimpleLazyObject(lambda: get_cached_barmen(request))
response = self.get_response(request)
if request.barmen._wrapped is not empty and {
b.id for b in request.barmen
} != set(request.session.get(SESSION_BARMEN_KEY, [])):
# update the session data only if `session.barmen`
# has been accessed and modified.
request.session[SESSION_BARMEN_KEY] = [b.id for b in request.barmen]
return response
@@ -21,4 +21,5 @@ class Migration(migrations.Migration):
verbose_name="clic limit", verbose_name="clic limit",
), ),
), ),
migrations.RemoveField(model_name="counter", name="token"),
] ]
-1
View File
@@ -619,7 +619,6 @@ class Counter(models.Model):
view_groups = models.ManyToManyField( view_groups = models.ManyToManyField(
Group, related_name="viewable_counters", blank=True Group, related_name="viewable_counters", blank=True
) )
token = models.CharField(_("token"), max_length=30, null=True, blank=True)
objects = CounterQuerySet.as_manager() objects = CounterQuerySet.as_manager()
+7 -14
View File
@@ -20,41 +20,34 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
import random
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from core.middleware import get_signal_request from core.middleware import get_signal_request
from core.models import OperationLog from core.models import OperationLog
from counter.models import Counter, Refilling, Selling from counter.models import Refilling, Selling
def write_log(instance, operation_type): def write_log(instance: Selling | Refilling, operation_type):
def get_user(): def get_user():
request = get_signal_request() request = get_signal_request()
if not request: if not request:
return None return None
# Get a random barmen if deletion is from a counter if request.barmen:
session = getattr(request, "session", {}) return random.choice(list(request.barmen))
session_token = session.get("counter_token", None)
if session_token:
counter = Counter.objects.filter(token=session_token).first()
if counter and len(counter.barmen_list) > 0:
return counter.get_random_barman()
# Get the current logged user if not from a counter # Get the current logged user if not from a counter
if request.user and not request.user.is_anonymous: if request.user.is_authenticated:
return request.user return request.user
# Return None by default
return None return None
OperationLog( OperationLog(
label=str(instance), label=str(instance), operator=get_user(), operation_type=operation_type
operator=get_user(),
operation_type=operation_type,
).save() ).save()
+33 -15
View File
@@ -32,12 +32,11 @@
</ul> </ul>
<p><strong>{% trans %}Total: {% endtrans %}{{ last_total }} €</strong></p> <p><strong>{% trans %}Total: {% endtrans %}{{ last_total }} €</strong></p>
{% endif %} {% endif %}
{% if barmen %} {% if can_click %}
<p>{% trans %}Enter client code:{% endtrans %}</p> <p>{% trans %}Enter client code:{% endtrans %}</p>
<form method="post" action=""> <form method="post" action="" id="select-user-form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="counter_token" value="{{ counter.token }}" /> {{ form }}
{{ form.as_p() }}
<p><input type="submit" value="{% trans %}validate{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}validate{% endtrans %}" /></p>
</form> </form>
{% else %} {% else %}
@@ -45,17 +44,36 @@
{% endif %} {% endif %}
</div> </div>
{% if counter.type == 'BAR' %} {% if counter.type == 'BAR' %}
<h3>{% trans %}Barmen:{% endtrans %}</h3>
{% if barmen_here %}
<div class="row gap-2x">
<div> <div>
<h3>{% trans %}Barman: {% endtrans %}</h3> <h4>{% trans %}On this device{% endtrans %}</h4>
{% for b in barmen_here %}
<p>{{ barman_logout_link(b) }}</p>
{% endfor %}
</div>
<div>
<h4>{% trans %}Elsewhere{% endtrans %}</h4>
{% if barmen_here|length == barmen|length %}
{# all logged barmen are logged in this session #}
<p><em>{% trans %}No barman logged elsewhere{% endtrans %}</em></p>
{% else %}
{% for b in barmen %}
{%- if b not in barmen_here -%}
<p>{{ barman_logout_link(b) }}</p>
{%- endif -%}
{% endfor %}
{% endif %}
</div>
</div>
{% else %}
{% for b in barmen %} {% for b in barmen %}
<p>{{ barman_logout_link(b) }}</p> <p>{{ barman_logout_link(b) }}</p>
{% endfor %} {% endfor %}
<form method="post" action="{{ url('counter:login', counter_id=counter.id) }}"> {% endif %}
{% csrf_token %} {{ login_fragment }}
{{ login_form.as_p() }}
<p><input type="submit" value="{% trans %}login{% endtrans %}" /></p>
</form>
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -63,10 +81,10 @@
{{ super() }} {{ super() }}
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
// The login form annoyingly takes priority over the code form {# The login form annoyingly takes priority over the code form
// This is due to the loading time of the web component This is due to the loading time of the web component
// We can't rely on DOMContentLoaded to know if the component is there so we We can't rely on DOMContentLoaded to know if the component is there so we
// periodically run a script until the field is there periodically run a script until the field is there #}
const autofocus = () => { const autofocus = () => {
const field = document.querySelector("input[id='id_code']"); const field = document.querySelector("input[id='id_code']");
if (field === null){ if (field === null){
@@ -0,0 +1,5 @@
<form hx-post="{{ action }}" hx-swap="outerHTML">
{% csrf_token %}
{{ form }}
<input type="submit" value="{% trans %}Confirm{% endtrans %}"/>
</form>
+114 -50
View File
@@ -17,9 +17,11 @@ from datetime import timedelta
from decimal import Decimal from decimal import Decimal
import pytest import pytest
from bs4 import BeautifulSoup
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission, make_password from django.contrib.auth.models import Permission, make_password
from django.contrib.messages import DEFAULT_LEVELS, get_messages
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.test import Client, TestCase from django.test import Client, TestCase
@@ -37,6 +39,7 @@ from core.models import BanGroup, Group, User
from counter.baker_recipes import price_recipe, product_recipe, sale_recipe from counter.baker_recipes import price_recipe, product_recipe, sale_recipe
from counter.models import ( from counter.models import (
Counter, Counter,
CounterSellers,
Customer, Customer,
Permanency, Permanency,
ProductType, ProductType,
@@ -66,10 +69,14 @@ class TestFullClickBase(TestCase):
cls.subscriber = subscriber_user.make() cls.subscriber = subscriber_user.make()
cls.counter = baker.make(Counter, type="BAR") cls.counter = baker.make(Counter, type="BAR")
cls.counter.sellers.add(cls.barmen, cls.board_admin)
cls.other_counter = baker.make(Counter, type="BAR") cls.other_counter = baker.make(Counter, type="BAR")
cls.other_counter.sellers.add(cls.barmen) CounterSellers.objects.bulk_create(
[
CounterSellers(counter=cls.counter, user=cls.barmen),
CounterSellers(counter=cls.counter, user=cls.board_admin),
CounterSellers(counter=cls.other_counter, user=cls.barmen),
]
)
cls.yet_another_counter = baker.make(Counter, type="BAR") cls.yet_another_counter = baker.make(Counter, type="BAR")
@@ -114,7 +121,10 @@ class TestRefilling(TestFullClickBase):
) -> HttpResponse: ) -> HttpResponse:
used_client = client if client is not None else self.client used_client = client if client is not None else self.client
return used_client.post( return used_client.post(
reverse("counter:refilling_create", kwargs={"customer_id": user.pk}), reverse(
"counter:refilling_create",
kwargs={"customer_id": user.pk, "counter_id": self.counter.pk},
),
{"amount": str(amount), "payment_method": Refilling.PaymentMethod.CASH}, {"amount": str(amount), "payment_method": Refilling.PaymentMethod.CASH},
HTTP_REFERER=reverse( HTTP_REFERER=reverse(
"counter:click", kwargs={"counter_id": counter.id, "user_id": user.pk} "counter:click", kwargs={"counter_id": counter.id, "user_id": user.pk}
@@ -138,7 +148,10 @@ class TestRefilling(TestFullClickBase):
return self.client.post( return self.client.post(
reverse( reverse(
"counter:refilling_create", "counter:refilling_create",
kwargs={"customer_id": self.customer.pk}, kwargs={
"customer_id": self.customer.pk,
"counter_id": self.counter.pk,
},
), ),
{"amount": "10", "payment_method": "CASH"}, {"amount": "10", "payment_method": "CASH"},
) )
@@ -442,9 +455,19 @@ class TestCounterClick(TestFullClickBase):
def test_click_not_connected(self): def test_click_not_connected(self):
force_refill_user(self.customer, 10) force_refill_user(self.customer, 10)
# trying to click on a bar without being logged should result
# in a redirect to the counter page with an error message
res = self.submit_basket(self.customer, [BasketItem(self.snack.id, 2)]) res = self.submit_basket(self.customer, [BasketItem(self.snack.id, 2)])
assertRedirects(res, self.counter.get_absolute_url()) assertRedirects(res, self.counter.get_absolute_url())
messages = list(get_messages(res.wsgi_request))
assert len(messages) == 1
assert messages[0].level == DEFAULT_LEVELS["ERROR"]
assert (
messages[0].message == "Vous ne pouvez pas cliquer des gens sur ce comptoir"
)
# trying to click on an office counter without permission should 403
res = self.submit_basket( res = self.submit_basket(
self.customer, [BasketItem(self.snack.id, 2)], counter=self.club_counter self.customer, [BasketItem(self.snack.id, 2)], counter=self.club_counter
) )
@@ -718,59 +741,97 @@ class TestCounterStats(TestCase):
class TestBarmanConnection(TestCase): class TestBarmanConnection(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
cls.krophil = User.objects.get(username="krophil") cls.barman = subscriber_user.make()
cls.skia = User.objects.get(username="skia") cls.barman.set_password("plop")
cls.skia.customer.account = 800 cls.barman.save()
cls.krophil.customer.save() cls.counter = baker.make(Counter, type="BAR", sellers=[cls.barman])
cls.skia.customer.save() cls.login_url = reverse("counter:login", kwargs={"counter_id": cls.counter.id})
cls.detail_url = reverse(
cls.counter = Counter.objects.get(id=2) "counter:details", kwargs={"counter_id": cls.counter.id}
)
def test_barman_granted(self): def test_barman_granted(self):
response = self.client.post(
self.login_url, {"username": self.barman.username, "password": "plop"}
)
assert response.status_code == 200
assert response.headers["HX-Redirect"] == self.detail_url
last_perm = Permanency.objects.last()
assert last_perm.counter == self.counter
assert last_perm.user == self.barman
assert last_perm.end is None
assert self.barman in response.wsgi_request.barmen
response = self.client.get(
self.detail_url, {"username": self.barman.username, "password": "plop"}
)
assert response.context_data.get("barmen") == [self.barman]
soup = BeautifulSoup(response.text, "lxml")
assert soup.find("form", id="select-user-form") is not None
def assert_counter_login_fails(self, user: User):
initial_perms = set(self.counter.permanencies.filter(user=user, end=None))
response = self.client.post(
self.login_url, {"username": user.username, "password": "plop"}
)
assert "HX-Redirect" not in response.headers
assert (
set(self.counter.permanencies.filter(user=user, end=None)) == initial_perms
)
if initial_perms:
# the user was already logged in, and we already tested
# that it didn't re-login, so we can skip the next assertions.
return
self.counter.refresh_from_db()
assert response.wsgi_request.barmen.isdisjoint(set(self.counter.barmen_list))
response = self.client.get(self.detail_url)
assert response.context_data.get("barmen") == []
soup = BeautifulSoup(response.text, "lxml")
assert soup.find("form", id="select-user-form") is None
def test_barman_not_seller(self):
"""Test when the barman is not a seller of the counter"""
not_barman = subscriber_user.make()
not_barman.set_password("plop")
not_barman.save()
self.assert_counter_login_fails(not_barman)
def test_barman_already_logged(self):
"""Test when the barman is already logged in the current counter."""
self.client.post( self.client.post(
reverse("counter:login", args=[self.counter.id]), self.login_url, {"username": self.barman.username, "password": "plop"}
{"username": "krophil", "password": "plop"},
) )
response = self.client.get(reverse("counter:details", args=[self.counter.id])) self.assert_counter_login_fails(self.barman)
assert "<p>Entrez un code client : </p>" in str(response.content) def test_barman_already_logged_elsewhere(self):
"""Test when the barman is already logged in another counter."""
def test_counters_list_barmen(self): other_counter = baker.make(Counter, type="BAR")
CounterSellers.objects.create(counter=other_counter, user=self.barman)
self.client.post( self.client.post(
reverse("counter:login", args=[self.counter.id]), reverse("counter:login", kwargs={"counter_id": other_counter.id}),
{"username": "krophil", "password": "plop"}, {"username": self.barman.username, "password": "plop"},
) )
response = self.client.get(reverse("counter:activity", args=[self.counter.id])) self.assert_counter_login_fails(self.barman)
assert '<li><a href="/user/10/">Kro Phil&#39;</a></li>' in str(response.content) def test_login_on_non_bar_counter(self):
counter = baker.make(Counter, type="OFFICE")
def test_barman_denied(self): CounterSellers.objects.create(counter=counter, user=self.barman)
self.client.post( url = reverse("counter:login", kwargs={"counter_id": counter.id})
reverse("counter:login", args=[self.counter.id]), response = self.client.get(url)
{"username": "skia", "password": "plop"}, assert response.status_code == 403
response = self.client.post(
url, {"username": self.barman.username, "password": "plop"}
) )
response_get = self.client.get( assert response.status_code == 403
reverse("counter:details", args=[self.counter.id])
)
assert "<p>Merci de vous identifier</p>" in str(response_get.content)
def test_counters_list_no_barmen(self):
self.client.post(
reverse("counter:login", args=[self.counter.id]),
{"username": "krophil", "password": "plop"},
)
response = self.client.get(reverse("counter:activity", args=[self.counter.id]))
assert '<li><a href="/user/1/">S&#39; Kia</a></li>' not in str(response.content)
@pytest.mark.django_db @pytest.mark.django_db
def test_barman_timeout(): def test_barman_timeout(client: Client):
"""Test that barmen timeout is well managed.""" """Test that barmen timeout is well managed."""
bar = baker.make(Counter, type="BAR") bar = baker.make(Counter, type="BAR")
user = baker.make(User) user = baker.make(User)
bar.sellers.add(user) CounterSellers.objects.create(counter=bar, user=user)
baker.make(Permanency, counter=bar, user=user, start=now()) baker.make(Permanency, counter=bar, user=user, start=now())
qs = Counter.objects.annotate_is_open().filter(pk=bar.pk) qs = Counter.objects.annotate_is_open().filter(pk=bar.pk)
@@ -786,6 +847,8 @@ def test_barman_timeout():
bar = qs[0] bar = qs[0]
assert not bar.is_open assert not bar.is_open
assert bar.barmen_list == [] assert bar.barmen_list == []
res = client.get("")
assert res.wsgi_request.barmen == set()
class TestClubCounterClickAccess(TestCase): class TestClubCounterClickAccess(TestCase):
@@ -835,14 +898,14 @@ class TestClubCounterClickAccess(TestCase):
def test_barman(self): def test_barman(self):
"""Sellers should be able to click on office counters""" """Sellers should be able to click on office counters"""
self.counter.sellers.add(self.user) CounterSellers.objects.create(counter=self.counter, user=self.user)
self.client.force_login(self.user) self.client.force_login(self.user)
res = self.client.get(self.click_url) res = self.client.get(self.click_url)
assert res.status_code == 200 assert res.status_code == 200
def test_both_barman_and_board_member(self): def test_both_barman_and_board_member(self):
"""If the user is barman and board member, he should be authorized as well.""" """If the user is barman and board member, he should be authorized as well."""
self.counter.sellers.add(self.user) CounterSellers.objects.create(counter=self.counter, user=self.user)
baker.make( baker.make(
Membership, club=self.counter.club, user=self.user, role=self.board_role Membership, club=self.counter.club, user=self.user, role=self.board_role
) )
@@ -868,14 +931,15 @@ class TestCounterLogout:
) )
assertRedirects( assertRedirects(
res, res,
reverse( reverse("counter:details", kwargs={"counter_id": permanence.counter_id}),
"counter:details", kwargs={"counter_id": permanence.counter_id}
),
) )
permanence.refresh_from_db() permanence.refresh_from_db()
assert permanence.end == now() assert permanence.end == permanence.activity
assert permanence.user not in res.wsgi_request.barmen
def test_logout_doesnt_change_old_permanences(self, client: Client): def test_logout_doesnt_change_old_permanences(self, client: Client):
# regression test for #1141
# https://github.com/ae-utbm/sith/pull/1141
perm_counter = baker.make(Counter, type="BAR") perm_counter = baker.make(Counter, type="BAR")
permanence = baker.make( permanence = baker.make(
Permanency, Permanency,
@@ -896,6 +960,6 @@ class TestCounterLogout:
data={"user_id": permanence.user_id}, data={"user_id": permanence.user_id},
) )
permanence.refresh_from_db() permanence.refresh_from_db()
assert permanence.end == now() assert permanence.end == permanence.activity
old_permanence.refresh_from_db() old_permanence.refresh_from_db()
assert old_permanence.end == old_end assert old_permanence.end == old_end
+4 -3
View File
@@ -41,7 +41,6 @@ from counter.views.admin import (
ReturnableProductUpdateView, ReturnableProductUpdateView,
SellingDeleteView, SellingDeleteView,
) )
from counter.views.auth import counter_login, counter_logout
from counter.views.cash import ( from counter.views.cash import (
CashSummaryEditView, CashSummaryEditView,
CashSummaryListView, CashSummaryListView,
@@ -57,7 +56,9 @@ from counter.views.eticket import (
from counter.views.home import ( from counter.views.home import (
CounterActivityView, CounterActivityView,
CounterLastOperationsView, CounterLastOperationsView,
CounterLoginFragment,
CounterMain, CounterMain,
counter_logout,
) )
from counter.views.invoice import InvoiceCallView from counter.views.invoice import InvoiceCallView
from counter.views.student_card import StudentCardDeleteView, StudentCardFormFragment from counter.views.student_card import StudentCardDeleteView, StudentCardFormFragment
@@ -66,7 +67,7 @@ urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"), path("<int:counter_id>/", CounterMain.as_view(), name="details"),
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"), path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"),
path( path(
"refill/<int:customer_id>/", "<int:counter_id>/refill/<int:customer_id>/",
RefillingCreateView.as_view(), RefillingCreateView.as_view(),
name="refilling_create", name="refilling_create",
), ),
@@ -82,7 +83,7 @@ urlpatterns = [
), ),
path("<int:counter_id>/activity/", CounterActivityView.as_view(), name="activity"), path("<int:counter_id>/activity/", CounterActivityView.as_view(), name="activity"),
path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"), path("<int:counter_id>/stats/", CounterStatView.as_view(), name="stats"),
path("<int:counter_id>/login/", counter_login, name="login"), path("<int:counter_id>/login/", CounterLoginFragment.as_view(), name="login"),
path("<int:counter_id>/logout/", counter_logout, name="logout"), path("<int:counter_id>/logout/", counter_logout, name="logout"),
path("eticket/<int:selling_id>/pdf/", EticketPDFView.as_view(), name="eticket_pdf"), path("eticket/<int:selling_id>/pdf/", EticketPDFView.as_view(), name="eticket_pdf"),
path( path(
+3 -16
View File
@@ -3,8 +3,6 @@ from urllib.parse import urlparse
from django.http import HttpRequest from django.http import HttpRequest
from django.urls import resolve from django.urls import resolve
from counter.models import Counter
def is_logged_in_counter(request: HttpRequest) -> bool: def is_logged_in_counter(request: HttpRequest) -> bool:
"""Check if the request is sent from a device logged to a counter. """Check if the request is sent from a device logged to a counter.
@@ -20,24 +18,13 @@ def is_logged_in_counter(request: HttpRequest) -> bool:
or the request path belongs to the counter app or the request path belongs to the counter app
(eg. the barman went back to the main by missclick and go back (eg. the barman went back to the main by missclick and go back
to the counter) to the counter)
- The current session has a counter token associated with it. - There are barmen logged in the current session
- A counter with this token exists.
- The counter is open
""" """
referer_ok = ( referer_ok = (
"HTTP_REFERER" in request.META "HTTP_REFERER" in request.META
and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter" and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter"
) )
has_token = ( if not referer_ok and request.resolver_match.app_name != "counter":
(referer_ok or request.resolver_match.app_name == "counter")
and "counter_token" in request.session
and request.session["counter_token"]
)
if not has_token:
return False return False
return ( return bool(request.barmen)
Counter.objects.annotate_is_open()
.filter(token=request.session["counter_token"], is_open=True)
.exists()
)
-53
View File
@@ -1,53 +0,0 @@
#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.utils.timezone import now
from django.views.decorators.http import require_POST
from core.views.forms import LoginForm
from counter.models import Counter, Permanency
@require_POST
def counter_login(request: HttpRequest, counter_id: int) -> HttpResponseRedirect:
"""Log a user in a counter.
A successful login will result in the beginning of a counter duty
for the user.
"""
counter = get_object_or_404(Counter, pk=counter_id)
form = LoginForm(request, data=request.POST)
if not form.is_valid():
return redirect(counter.get_absolute_url() + "?credentials")
user = form.get_user()
if not counter.sellers.contains(user) or user in counter.barmen_list:
return redirect(counter.get_absolute_url() + "?sellers")
if len(counter.barmen_list) == 0:
counter.gen_token()
request.session["counter_token"] = counter.token
counter.permanencies.create(user=user, start=timezone.now())
return redirect(counter)
@require_POST
def counter_logout(request: HttpRequest, counter_id: int) -> HttpResponseRedirect:
"""End the permanency of a user in this counter."""
Permanency.objects.filter(
counter=counter_id, user=request.POST["user_id"], end=None
).update(end=now())
return redirect("counter:details", counter_id=counter_id)
+19 -19
View File
@@ -12,8 +12,10 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
import random
from collections import defaultdict from collections import defaultdict
from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
@@ -21,6 +23,7 @@ from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, resolve_url from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from ninja.main import HttpRequest from ninja.main import HttpRequest
@@ -29,13 +32,7 @@ from core.auth.mixins import CanViewMixin
from core.models import User from core.models import User
from core.views.mixins import FragmentMixin, UseFragmentsMixin from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BasketForm, RefillForm from counter.forms import BasketForm, RefillForm
from counter.models import ( from counter.models import Counter, Customer, ProductFormula, ReturnableProduct, Selling
Counter,
Customer,
ProductFormula,
ReturnableProduct,
Selling,
)
from counter.utils import is_logged_in_counter from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin from counter.views.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormFragment from counter.views.student_card import StudentCardFormFragment
@@ -46,7 +43,7 @@ def get_operator(request: HttpRequest, counter: Counter, customer: Customer) ->
return request.user return request.user
if counter.customer_is_barman(customer): if counter.customer_is_barman(customer):
return customer.user return customer.user
return counter.get_random_barman() return random.choice(list(request.barmen))
class CounterClick( class CounterClick(
@@ -78,7 +75,7 @@ class CounterClick(
return kwargs return kwargs
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"])
obj: Counter = self.get_object() obj: Counter = self.get_object()
if not self.customer.can_buy or self.customer.user.is_banned_counter: if not self.customer.can_buy or self.customer.user.is_banned_counter:
@@ -96,11 +93,10 @@ class CounterClick(
# or a seller of this counter. # or a seller of this counter.
raise PermissionDenied raise PermissionDenied
if obj.type == "BAR" and ( if obj.type == "BAR" and not (
not obj.is_open request.barmen and request.barmen.issubset(set(obj.barmen_list))
or "counter_token" not in request.session
or request.session["counter_token"] != obj.token
): ):
messages.error(request, _("You cannot click users on this counter"))
return redirect(obj) # Redirect to counter return redirect(obj) # Redirect to counter
self.prices = list(obj.get_prices_for(self.customer)) self.prices = list(obj.get_prices_for(self.customer))
@@ -199,7 +195,7 @@ class CounterClick(
) )
if self.object.can_refill(): if self.object.can_refill():
res["refilling_fragment"] = RefillingCreateView.as_fragment()( res["refilling_fragment"] = RefillingCreateView.as_fragment()(
self.request, customer=self.customer self.request, customer=self.customer, counter=self.object
) )
return res return res
@@ -237,11 +233,13 @@ class RefillingCreateView(FragmentMixin, FormView):
if not is_logged_in_counter(request): if not is_logged_in_counter(request):
raise PermissionDenied raise PermissionDenied
self.counter: Counter = get_object_or_404( self.counter: Counter = get_object_or_404(Counter, id=self.kwargs["counter_id"])
Counter, token=request.session["counter_token"]
)
if not self.counter.can_refill(): if not (
request.barmen
and request.barmen.issubset(self.counter.barmen_list)
and self.counter.can_refill()
):
raise PermissionDenied raise PermissionDenied
self.operator = get_operator(request, self.counter, self.customer) self.operator = get_operator(request, self.counter, self.customer)
@@ -250,6 +248,7 @@ class RefillingCreateView(FragmentMixin, FormView):
def render_fragment(self, request, **kwargs) -> SafeString: def render_fragment(self, request, **kwargs) -> SafeString:
self.customer = kwargs.pop("customer") self.customer = kwargs.pop("customer")
self.counter = kwargs.pop("counter")
return super().render_fragment(request, **kwargs) return super().render_fragment(request, **kwargs)
def form_valid(self, form): def form_valid(self, form):
@@ -264,7 +263,8 @@ class RefillingCreateView(FragmentMixin, FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["action"] = reverse( kwargs["action"] = reverse(
"counter:refilling_create", kwargs={"customer_id": self.customer.pk} "counter:refilling_create",
kwargs={"customer_id": self.customer.pk, "counter_id": self.counter.pk},
) )
return kwargs return kwargs
+97 -52
View File
@@ -15,78 +15,120 @@
from datetime import timedelta from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.http import HttpResponseRedirect from django.core.exceptions import PermissionDenied
from django.urls import reverse, reverse_lazy from django.db.models import F
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.safestring import SafeString
from django.views.decorators.http import require_POST
from django.views.generic import DetailView from django.views.generic import DetailView
from django.views.generic.edit import FormMixin, ProcessFormView from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormView
from core.auth.mixins import CanViewMixin from core.auth.mixins import CanViewMixin
from core.views.forms import LoginForm from core.views import FragmentMixin, UseFragmentsMixin
from counter.forms import GetUserForm from counter.forms import CounterLoginForm, GetUserForm
from counter.models import Counter from counter.models import Counter, Permanency
from counter.utils import is_logged_in_counter from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin from counter.views.mixins import CounterTabsMixin
class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
model = Counter
form_class = CounterLoginForm
reload_on_redirect = True
pk_url_kwarg = "counter_id"
template_name = "counter/fragments/login.jinja"
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.type != "BAR":
# barmen have to log in only if it is a bar,
# so calling this view on a non-bar counter makes no sense
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"request": self.request,
"counter": self.object,
}
def form_valid(self, form: CounterLoginForm):
user = form.get_user()
self.object.permanencies.create(user=user, start=timezone.now())
self.request.barmen.add(user)
self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.id}
)
return super().form_valid(form)
def render_fragment(self, request, **kwargs) -> SafeString:
self.object = kwargs.pop("counter")
return super().render_fragment(request, **kwargs)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse("counter:login", kwargs={"counter_id": self.object.id})
}
@require_POST
def counter_logout(request: HttpRequest, counter_id: int) -> HttpResponseRedirect:
"""End the permanency of a user in this counter."""
Permanency.objects.filter(
counter=counter_id, user=request.POST["user_id"], end=None
).update(end=F("activity"))
return redirect("counter:details", counter_id=counter_id)
class CounterMain( class CounterMain(
CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin CounterTabsMixin, UseFragmentsMixin, CanViewMixin, SingleObjectMixin, FormView
): ):
"""The public (barman) view.""" """The public (barman) view."""
model = Counter model = Counter
queryset = Counter.objects.exclude(type="EBOUTIC")
template_name = "counter/counter_main.jinja" template_name = "counter/counter_main.jinja"
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
form_class = ( form_class = GetUserForm
GetUserForm # Form to enter a client code and get the corresponding user id
)
current_tab = "counter" current_tab = "counter"
def get_queryset(self): def dispatch(self, request, *args, **kwargs):
return super().get_queryset().exclude(type="EBOUTIC") self.object: Counter = self.get_object()
if self.object.type == "BAR":
self.object.update_activity()
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def get_fragment_context_data(self) -> dict[str, SafeString]:
self.object = self.get_object() login_fragment = (
if self.object.type == "BAR" and not ( CounterLoginFragment.as_fragment()(self.request, counter=self.object)
"counter_token" in self.request.session if self.object.type == "BAR"
and self.request.session["counter_token"] == self.object.token else ""
): # Check the token to avoid the bar to be stolen
return HttpResponseRedirect(
reverse_lazy(
"counter:details",
args=self.args,
kwargs={"counter_id": self.object.id},
) )
+ "?bad_location" return super().get_fragment_context_data() | {"login_fragment": login_fragment}
)
return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""We handle here the login form for the barman.""" """We handle here the login form for the barman."""
if self.request.method == "POST":
self.object = self.get_object()
self.object.update_activity()
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["login_form"] = LoginForm()
kwargs["login_form"].fields["username"].widget.attrs["autofocus"] = True
kwargs[
"login_form"
].cleaned_data = {} # add_error fails if there are no cleaned_data
if "credentials" in self.request.GET:
kwargs["login_form"].add_error(None, _("Bad credentials"))
if "sellers" in self.request.GET:
kwargs["login_form"].add_error(None, _("User is not barman"))
kwargs["form"] = self.get_form()
kwargs["form"].cleaned_data = {} # same as above
if "bad_location" in self.request.GET:
kwargs["form"].add_error(
None, _("Bad location, someone is already logged in somewhere else")
)
if self.object.type == "BAR": if self.object.type == "BAR":
kwargs["barmen"] = self.object.barmen_list kwargs["barmen"] = self.object.barmen_list
elif self.request.user.is_authenticated: kwargs["barmen_here"] = list(
kwargs["barmen"] = [self.request.user] self.request.barmen.intersection(self.object.barmen_list)
)
kwargs["can_click"] = (
self.object.type == "BAR"
and self.request.barmen
and self.request.barmen.issubset(set(self.object.barmen_list))
) or (
self.object.type == "OFFICE"
and (
self.object.sellers.contains(self.request.user)
or self.object.club.has_rights_in_club(self.request.user)
)
)
if "last_basket" in self.request.session: if "last_basket" in self.request.session:
kwargs["last_basket"] = self.request.session.pop("last_basket") kwargs["last_basket"] = self.request.session.pop("last_basket")
kwargs["last_customer"] = self.request.session.pop("last_customer") kwargs["last_customer"] = self.request.session.pop("last_customer")
@@ -96,14 +138,17 @@ class CounterMain(
) )
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form: GetUserForm):
"""We handle here the redirection, passing the user id of the asked customer.""" """We handle here the redirection, passing the user id of the asked customer."""
self.kwargs["user_id"] = form.cleaned_data["user_id"] self.success_url = reverse(
"counter:click",
kwargs={
"counter_id": self.kwargs["counter_id"],
"user_id": form.cleaned_data["user_id"],
},
)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("counter:click", args=self.args, kwargs=self.kwargs)
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView): class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
"""Provide the last operations to allow barmen to delete them.""" """Provide the last operations to allow barmen to delete them."""
+33 -27
View File
@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-23 12:15+0200\n" "POT-Creation-Date: 2026-06-02 10:53+0200\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"
@@ -363,11 +363,8 @@ msgid "Unregistered user"
msgstr "Utilisateur non enregistré" msgstr "Utilisateur non enregistré"
#: club/models.py #: club/models.py
#, python-format msgid "The base url that links with this type must respect"
msgid "The base url that links with this type must respect (e.g. `%(url)s`)" msgstr "L'url de base que tous les liens de ce type doivent respecter"
msgstr ""
"L'url de base que tous les liens de ce type doivent respecter (par exemple "
"`%(url)s`)"
#: club/models.py counter/models.py #: club/models.py counter/models.py
msgid "icon" msgid "icon"
@@ -2203,6 +2200,7 @@ msgstr "Êtes-vous sûr de vouloir supprimer \"%(name)s\" ?"
#: core/templates/core/delete_confirm.jinja #: core/templates/core/delete_confirm.jinja
#: core/templates/core/file_delete_confirm.jinja #: core/templates/core/file_delete_confirm.jinja
#: counter/templates/counter/fragments/delete_student_card.jinja #: counter/templates/counter/fragments/delete_student_card.jinja
#: counter/templates/counter/fragments/login.jinja
msgid "Confirm" msgid "Confirm"
msgstr "Confirmation" msgstr "Confirmation"
@@ -3206,6 +3204,18 @@ msgstr "Cet UID est invalide"
msgid "User not found" msgid "User not found"
msgstr "Utilisateur non trouvé" msgstr "Utilisateur non trouvé"
#: counter/forms.py
msgid "You are not a barman of this counter."
msgstr "Vous n'êtes pas barman sur ce comptoir."
#: counter/forms.py
msgid "You are already logged in this counter."
msgstr "Vous êtes déjà connecté à ce comptoir."
#: counter/forms.py
msgid "You are already logged in another counter."
msgstr "Vous êtes déjà connecté à un autre comptoir."
#: counter/forms.py #: counter/forms.py
msgid "Regular barmen" msgid "Regular barmen"
msgstr "Barmen réguliers" msgstr "Barmen réguliers"
@@ -3484,10 +3494,6 @@ msgstr "Bureau"
msgid "sellers" msgid "sellers"
msgstr "vendeurs" msgstr "vendeurs"
#: counter/models.py
msgid "token"
msgstr "jeton"
#: counter/models.py #: counter/models.py
msgid "regular barman" msgid "regular barman"
msgstr "barman régulier" msgstr "barman régulier"
@@ -3812,7 +3818,7 @@ msgstr ""
#: counter/templates/counter/counter_click.jinja #: counter/templates/counter/counter_click.jinja
msgid "No products available on this counter for this user" msgid "No products available on this counter for this user"
msgstr "Pas de produits disponnibles dans ce comptoir pour cet utilisateur" msgstr "Pas de produits disponibles dans ce comptoir pour cet utilisateur"
#: counter/templates/counter/counter_list.jinja #: counter/templates/counter/counter_list.jinja
msgid "Counter admin list" msgid "Counter admin list"
@@ -3873,12 +3879,20 @@ msgid "Please, login"
msgstr "Merci de vous identifier" msgstr "Merci de vous identifier"
#: counter/templates/counter/counter_main.jinja #: counter/templates/counter/counter_main.jinja
msgid "Barman: " msgid "Barmen:"
msgstr "Barman : " msgstr "Barmen :"
#: counter/templates/counter/counter_main.jinja #: counter/templates/counter/counter_main.jinja
msgid "login" msgid "On this device"
msgstr "login" msgstr "Sur cet appareil"
#: counter/templates/counter/counter_main.jinja
msgid "Elsewhere"
msgstr "Ailleurs"
#: counter/templates/counter/counter_main.jinja
msgid "No barman logged elsewhere"
msgstr "Pas de barman connecté ailleurs"
#: counter/templates/counter/eticket_list.jinja #: counter/templates/counter/eticket_list.jinja
msgid "Eticket list" msgid "Eticket list"
@@ -4283,22 +4297,14 @@ msgstr "Montant du chèque"
msgid "Check quantity" msgid "Check quantity"
msgstr "Nombre de chèque" msgstr "Nombre de chèque"
#: counter/views/click.py
msgid "You cannot click users on this counter"
msgstr "Vous ne pouvez pas cliquer des gens sur ce comptoir"
#: counter/views/eticket.py #: counter/views/eticket.py
msgid "people(s)" msgid "people(s)"
msgstr "personne(s)" msgstr "personne(s)"
#: counter/views/home.py
msgid "Bad credentials"
msgstr "Mauvais identifiants"
#: counter/views/home.py
msgid "User is not barman"
msgstr "L'utilisateur n'est pas barman."
#: counter/views/home.py
msgid "Bad location, someone is already logged in somewhere else"
msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs"
#: counter/views/invoice.py #: counter/views/invoice.py
msgid "Invoice calls status has been updated." msgid "Invoice calls status has been updated."
msgstr "Le statut des appels à facture a été mis à jour." msgstr "Le statut des appels à facture a été mis à jour."
+5 -2
View File
@@ -34,6 +34,7 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
""" """
import binascii import binascii
import contextlib
import os import os
import sys import sys
from datetime import timedelta from datetime import timedelta
@@ -41,6 +42,7 @@ from pathlib import Path
import sentry_sdk import sentry_sdk
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from environs import Env from environs import Env
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
@@ -91,6 +93,7 @@ ALLOWED_HOSTS = ["*"]
# RemovedInDjango60Warning: It's a transitional setting helpful in early # RemovedInDjango60Warning: It's a transitional setting helpful in early
# adoption of "https" as the new default value of forms.URLField.assume_scheme. # adoption of "https" as the new default value of forms.URLField.assume_scheme.
# Remove this after upgrading to Django 6.x # Remove this after upgrading to Django 6.x
with contextlib.suppress(RemovedInDjango60Warning):
FORMS_URLFIELD_ASSUME_HTTPS = True FORMS_URLFIELD_ASSUME_HTTPS = True
# Application definition # Application definition
@@ -138,13 +141,13 @@ MIDDLEWARE = (
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "core.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"core.middleware.AuthenticationMiddleware",
"core.middleware.SignalRequestMiddleware", "core.middleware.SignalRequestMiddleware",
"counter.middleware.BarmenMiddleware",
) )
ROOT_URLCONF = "sith.urls" ROOT_URLCONF = "sith.urls"