diff --git a/counter/forms.py b/counter/forms.py index 6641a80e..b71ca7fc 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -39,6 +39,7 @@ from counter.models import ( Customer, Eticket, InvoiceCall, + Permanency, Price, Product, ProductFormula, @@ -151,12 +152,13 @@ class CounterLoginForm(LoginForm): 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.") - ) + if Permanency.objects.filter(end=None, user=user).exists(): + if user in self.request.barmen: + message = _("You are already logged in this counter.") + elif user in self.counter.barmen_list: + 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") diff --git a/counter/middleware.py b/counter/middleware.py index 7483a331..5b9efb43 100644 --- a/counter/middleware.py +++ b/counter/middleware.py @@ -1,8 +1,7 @@ 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 django.utils.functional import SimpleLazyObject from core.models import User from counter.models import Permanency @@ -11,20 +10,31 @@ if TYPE_CHECKING: 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]: 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, - ) + + if session_ids := session.get(SESSION_PERMANENCES_KEY, None): + # Get ongoing permanences which id is in session. + # Note : we store permanence ids rather than user id to be sure + # not to wrongfully mark someone as logged here, + # 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: request._cached_barmen = set() @@ -53,12 +63,4 @@ class BarmenMiddleware: 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 + return self.get_response(request) diff --git a/counter/models.py b/counter/models.py index 1907d2fb..7cb36b22 100644 --- a/counter/models.py +++ b/counter/models.py @@ -1105,7 +1105,7 @@ class Permanency(models.Model): on_delete=models.CASCADE, ) 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) class Meta: diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index aa771b25..8e5fa466 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -760,10 +760,10 @@ class TestBarmanConnection(TestCase): 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 self.barman in response.wsgi_request.barmen assert response.context_data.get("barmen") == [self.barman] soup = BeautifulSoup(response.text, "lxml") 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) + 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): """Test when the barman is already logged in another counter.""" other_counter = baker.make(Counter, type="BAR") diff --git a/counter/views/home.py b/counter/views/home.py index a8e2c741..47196503 100644 --- a/counter/views/home.py +++ b/counter/views/home.py @@ -30,6 +30,7 @@ from django.views.generic.edit import FormView from core.auth.mixins import CanViewMixin from core.views import FragmentMixin, UseFragmentsMixin from counter.forms import CounterLoginForm, GetUserForm +from counter.middleware import SESSION_PERMANENCES_KEY from counter.models import Counter, Permanency from counter.utils import is_logged_in_counter from counter.views.mixins import CounterTabsMixin @@ -58,8 +59,8 @@ class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView): def form_valid(self, form: CounterLoginForm): user = form.get_user() - self.object.permanencies.create(user=user, start=timezone.now()) - self.request.barmen.add(user) + perm = self.object.permanencies.create(user=user, start=timezone.now()) + self.request.session.setdefault(SESSION_PERMANENCES_KEY, []).append(perm.id) self.success_url = reverse( "counter:details", kwargs={"counter_id": self.object.id} ) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 862fbde5..72091ee3 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3217,6 +3217,10 @@ msgstr "Vous êtes déjà connecté à ce comptoir." msgid "You are already logged in another counter." 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 msgid "Regular barmen" msgstr "Barmen réguliers"