Optimize barmen timeout and counter state fetch

Le timeout se fait en une seule requête et la récupération de l'état des comptoirs en une seule requête aussi. Grâce à ça, on peut en grande partie retirer le cache pour l'affichage de l'état des comptoirs, ce qui a des implications excellentes en termes d'UX (comme le fait que la redirection vers la page de comptoir ou d'activité aura plus une apparence de truc aléatoire)
This commit is contained in:
imperosol
2024-10-10 00:06:22 +02:00
parent 4bc4d266c2
commit c0a6f5eb30
6 changed files with 224 additions and 176 deletions

View File

@ -23,15 +23,17 @@ from counter.schemas import CounterSchema
class CounterController(ControllerBase):
@route.get("", response=list[CounterSchema], permissions=[IsRoot])
def fetch_all(self):
return Counter.objects.all()
return Counter.objects.annotate_is_open()
@route.get("{counter_id}/", response=CounterSchema, permissions=[CanView])
def fetch_one(self, counter_id: int):
return self.get_object_or_exception(Counter, pk=counter_id)
return self.get_object_or_exception(
Counter.objects.annotate_is_open(), pk=counter_id
)
@route.get("bar/", response=list[CounterSchema], permissions=[CanView])
def fetch_bars(self):
counters = list(Counter.objects.filter(type="BAR"))
counters = list(Counter.objects.annotate_is_open().filter(type="BAR"))
for c in counters:
self.check_object_permissions(c)
return counters

View File

@ -358,7 +358,7 @@ class Product(models.Model):
class CounterQuerySet(models.QuerySet):
def annotate_has_barman(self, user: User) -> CounterQuerySet:
def annotate_has_barman(self, user: User) -> Self:
"""Annotate the queryset with the `user_is_barman` field.
For each counter, this field has value True if the user
@ -383,6 +383,29 @@ class CounterQuerySet(models.QuerySet):
subquery = user.counters.filter(pk=OuterRef("pk"))
return self.annotate(has_annotated_barman=Exists(subquery))
def annotate_is_open(self) -> Self:
"""Annotate tue queryset with the `is_open` field.
For each counter, if `is_open=True`, then the counter is currently opened.
Else the counter is closed.
"""
return self.annotate(
is_open=Exists(
Permanency.objects.filter(counter_id=OuterRef("pk"), end=None)
)
)
def handle_timeout(self) -> int:
"""Disconnect the barmen who are inactive in the given counters.
Returns:
The number of affected rows (ie, the number of timeouted permanences)
"""
timeout = timezone.now() - timedelta(minutes=settings.SITH_BARMAN_TIMEOUT)
return Permanency.objects.filter(
counter__in=self, end=None, activity__lt=timeout
).update(end=F("activity"))
class Counter(models.Model):
name = models.CharField(_("name"), max_length=30)
@ -450,20 +473,10 @@ class Counter(models.Model):
@cached_property
def barmen_list(self) -> list[User]:
return self.get_barmen_list()
def get_barmen_list(self) -> list[User]:
"""Returns the barman list as list of User.
Also handle the timeout of the barmen
"""
perms = self.permanencies.filter(end=None)
# disconnect barmen who are inactive
timeout = timezone.now() - timedelta(minutes=settings.SITH_BARMAN_TIMEOUT)
perms.filter(activity__lte=timeout).update(end=F("activity"))
return [p.user for p in perms.select_related("user")]
"""Returns the barman list as list of User."""
return [
p.user for p in self.permanencies.filter(end=None).select_related("user")
]
def get_random_barman(self) -> User:
"""Return a random user being currently a barman."""

View File

@ -15,20 +15,29 @@
import json
import re
import string
from datetime import timedelta
import pytest
from django.conf import settings
from django.core.cache import cache
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import timedelta
from django.utils.timezone import now
from freezegun import freeze_time
from model_bakery import baker
from club.models import Club, Membership
from core.baker_recipes import subscriber_user
from core.models import User
from counter.models import BillingInfo, Counter, Customer, Permanency, Product, Selling
from sith.settings import SITH_MAIN_CLUB
from counter.models import (
BillingInfo,
Counter,
Customer,
Permanency,
Product,
Selling,
)
class TestCounter(TestCase):
@ -219,7 +228,7 @@ class TestCounterStats(TestCase):
s = Selling(
label=barbar.name,
product=barbar,
club=Club.objects.get(name=SITH_MAIN_CLUB["name"]),
club=Club.objects.get(name=settings.SITH_MAIN_CLUB["name"]),
counter=cls.counter,
unit_price=2,
seller=cls.skia,
@ -497,6 +506,29 @@ class TestBarmanConnection(TestCase):
assert not '<li><a href="/user/1/">S&#39; Kia</a></li>' in str(response.content)
@pytest.mark.django_db
def test_barman_timeout():
"""Test that barmen timeout is well managed."""
bar = baker.make(Counter, type="BAR")
user = baker.make(User)
bar.sellers.add(user)
baker.make(Permanency, counter=bar, user=user, start=now())
qs = Counter.objects.annotate_is_open().filter(pk=bar.pk)
bar = qs[0]
assert bar.is_open
assert bar.barmen_list == [user]
qs.handle_timeout() # handling timeout before the actual timeout should be no-op
assert qs[0].is_open
with freeze_time() as frozen_time:
frozen_time.tick(timedelta(minutes=settings.SITH_BARMAN_TIMEOUT + 1))
qs.handle_timeout()
bar = qs[0]
assert not bar.is_open
assert bar.barmen_list == []
class TestStudentCard(TestCase):
"""Tests for adding and deleting Stundent Cards
Test that an user can be found with it's student card.

View File

@ -239,6 +239,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
"""
model = Counter
queryset = Counter.objects.annotate_is_open()
template_name = "counter/counter_click.jinja"
pk_url_kwarg = "counter_id"
current_tab = "counter"