1 Commits

Author SHA1 Message Date
imperosol 519a7758c5 manage case where barman is already logged in another device 2026-06-12 10:32:16 +02:00
7 changed files with 74 additions and 30 deletions
+8 -6
View File
@@ -39,6 +39,7 @@ from counter.models import (
Customer, Customer,
Eticket, Eticket,
InvoiceCall, InvoiceCall,
Permanency,
Price, Price,
Product, Product,
ProductFormula, ProductFormula,
@@ -151,12 +152,13 @@ class CounterLoginForm(LoginForm):
raise ValidationError( raise ValidationError(
message=_("You are not a barman of this counter."), code="not_barman" message=_("You are not a barman of this counter."), code="not_barman"
) )
if user in self.request.barmen: if Permanency.objects.filter(end=None, user=user).exists():
message = ( if user in self.request.barmen:
_("You are already logged in this counter.") message = _("You are already logged in this counter.")
if user in self.counter.barmen_list elif user in self.counter.barmen_list:
else _("You are already logged in another counter.") message = _("You are already logged in another counter.")
) else:
message = _("You are already logged on another device")
raise ValidationError(message=message, code="already_logged_in") raise ValidationError(message=message, code="already_logged_in")
+21 -19
View File
@@ -1,8 +1,7 @@
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable
from django.db.models import Exists, OuterRef
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject, empty from django.utils.functional import SimpleLazyObject
from core.models import User from core.models import User
from counter.models import Permanency from counter.models import Permanency
@@ -11,20 +10,31 @@ if TYPE_CHECKING:
from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.base import SessionBase
SESSION_BARMEN_KEY = "barmen_ids" SESSION_PERMANENCES_KEY = "permanence_ids"
def get_cached_barmen(request: HttpRequest) -> set[User]: def get_cached_barmen(request: HttpRequest) -> set[User]:
if not hasattr(request, "_cached_barmen"): if not hasattr(request, "_cached_barmen"):
session: SessionBase = request.session session: SessionBase = request.session
barmen_ids = session.get(SESSION_BARMEN_KEY, [])
if barmen_ids: if session_ids := session.get(SESSION_PERMANENCES_KEY, None):
request._cached_barmen = set( # Get ongoing permanences which id is in session.
User.objects.filter( # Note : we store permanence ids rather than user id to be sure
Exists(Permanency.objects.filter(user=OuterRef("pk"), end=None)), # not to wrongfully mark someone as logged here,
id__in=barmen_ids, # even if it logged out then logged in elsewhere.
) permanences = (
Permanency.objects.filter(end=None, id__in=session_ids)
.order_by("id")
.select_related("user")
) )
# if the list of permanences occurring on this device has changed
# since the last page load, change the ids stored in session
real_ids = [p.id for p in permanences]
if real_ids != session_ids:
session[SESSION_PERMANENCES_KEY] = real_ids
request._cached_barmen = {p.user for p in permanences}
else: else:
request._cached_barmen = set() request._cached_barmen = set()
@@ -53,12 +63,4 @@ class BarmenMiddleware:
def __call__(self, request: HttpRequest): def __call__(self, request: HttpRequest):
request.barmen = SimpleLazyObject(lambda: get_cached_barmen(request)) request.barmen = SimpleLazyObject(lambda: get_cached_barmen(request))
response = self.get_response(request) return 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
+1 -1
View File
@@ -1105,7 +1105,7 @@ class Permanency(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
start = models.DateTimeField(_("start date")) start = models.DateTimeField(_("start date"))
end = models.DateTimeField(_("end date"), null=True, db_index=True) end = models.DateTimeField(_("end date"), null=True, blank=True, db_index=True)
activity = models.DateTimeField(_("last activity date"), auto_now=True) activity = models.DateTimeField(_("last activity date"), auto_now=True)
class Meta: class Meta:
+36 -1
View File
@@ -760,10 +760,10 @@ class TestBarmanConnection(TestCase):
assert last_perm.counter == self.counter assert last_perm.counter == self.counter
assert last_perm.user == self.barman assert last_perm.user == self.barman
assert last_perm.end is None assert last_perm.end is None
assert self.barman in response.wsgi_request.barmen
response = self.client.get( response = self.client.get(
self.detail_url, {"username": self.barman.username, "password": "plop"} self.detail_url, {"username": self.barman.username, "password": "plop"}
) )
assert self.barman in response.wsgi_request.barmen
assert response.context_data.get("barmen") == [self.barman] assert response.context_data.get("barmen") == [self.barman]
soup = BeautifulSoup(response.text, "lxml") soup = BeautifulSoup(response.text, "lxml")
assert soup.find("form", id="select-user-form") is not None assert soup.find("form", id="select-user-form") is not None
@@ -804,6 +804,41 @@ class TestBarmanConnection(TestCase):
) )
self.assert_counter_login_fails(self.barman) self.assert_counter_login_fails(self.barman)
def test_barman_already_logged_in_another_device(self):
"""Test when the barman is already logged in the current counter on another device."""
other_client = Client()
other_client.post(
self.login_url, {"username": self.barman.username, "password": "plop"}
)
self.assert_counter_login_fails(self.barman)
def test_barman_login_elsewhere(self):
"""Test when the barman log himself out then log in on another device."""
self.client.post(
self.login_url, {"username": self.barman.username, "password": "plop"}
)
other_client = Client()
other_client.post(
reverse("counter:logout", kwargs={"counter_id": self.counter.id}),
data={"user_id": self.barman.id},
)
response = other_client.post(
self.login_url, {"username": self.barman.username, "password": "plop"}
)
assert response.status_code == 200
assert response.headers["HX-Redirect"] == self.detail_url
# the barmen should now be logged in `other_client`...
response = other_client.get(
self.detail_url, {"username": self.barman.username, "password": "plop"}
)
assert self.barman in response.wsgi_request.barmen
# ... but not in `self.client`
response = self.client.get(
self.detail_url, {"username": self.barman.username, "password": "plop"}
)
assert self.barman not in response.wsgi_request.barmen
def test_barman_already_logged_elsewhere(self): def test_barman_already_logged_elsewhere(self):
"""Test when the barman is already logged in another counter.""" """Test when the barman is already logged in another counter."""
other_counter = baker.make(Counter, type="BAR") other_counter = baker.make(Counter, type="BAR")
+3 -2
View File
@@ -30,6 +30,7 @@ from django.views.generic.edit import FormView
from core.auth.mixins import CanViewMixin from core.auth.mixins import CanViewMixin
from core.views import FragmentMixin, UseFragmentsMixin from core.views import FragmentMixin, UseFragmentsMixin
from counter.forms import CounterLoginForm, GetUserForm from counter.forms import CounterLoginForm, GetUserForm
from counter.middleware import SESSION_PERMANENCES_KEY
from counter.models import Counter, Permanency 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
@@ -58,8 +59,8 @@ class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
def form_valid(self, form: CounterLoginForm): def form_valid(self, form: CounterLoginForm):
user = form.get_user() user = form.get_user()
self.object.permanencies.create(user=user, start=timezone.now()) perm = self.object.permanencies.create(user=user, start=timezone.now())
self.request.barmen.add(user) self.request.session.setdefault(SESSION_PERMANENCES_KEY, []).append(perm.id)
self.success_url = reverse( self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.id} "counter:details", kwargs={"counter_id": self.object.id}
) )
+4
View File
@@ -3217,6 +3217,10 @@ msgstr "Vous êtes déjà connecté à ce comptoir."
msgid "You are already logged in another counter." msgid "You are already logged in another counter."
msgstr "Vous êtes déjà connecté à un autre comptoir." msgstr "Vous êtes déjà connecté à un autre comptoir."
#: counter/forms.py
msgid "You are already logged on another device"
msgstr "Vous êtes déjà connecté sur un autre appareil"
#: counter/forms.py #: counter/forms.py
msgid "Regular barmen" msgid "Regular barmen"
msgstr "Barmen réguliers" msgstr "Barmen réguliers"
+1 -1
View File
@@ -77,7 +77,7 @@ tests = [
"pytest-cov>=7.1.0,<8.0.0", "pytest-cov>=7.1.0,<8.0.0",
"pytest-django>=4.12.0,<5.0.0", "pytest-django>=4.12.0,<5.0.0",
"model-bakery>=1.23.4,<2.0.0", "model-bakery>=1.23.4,<2.0.0",
"beautifulsoup4>=4.15.0,<5", "beautifulsoup4>=4.14.3,<5",
"lxml>=6.1.1,<7", "lxml>=6.1.1,<7",
] ]
docs = [ docs = [