diff --git a/api/permissions.py b/api/permissions.py
index f371910b..38377c98 100644
--- a/api/permissions.py
+++ b/api/permissions.py
@@ -46,7 +46,7 @@ from django.http import HttpRequest
from ninja_extra import ControllerBase
from ninja_extra.permissions import BasePermission
-from counter.models import Counter
+from counter.utils import is_logged_in_counter
class IsInGroup(BasePermission):
@@ -186,12 +186,7 @@ class IsLoggedInCounter(BasePermission):
"""Check that a user is logged in a counter."""
def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
- if "/counter/" not in request.META.get("HTTP_REFERER", ""):
- return False
- token = request.session.get("counter_token")
- if not token:
- return False
- return Counter.objects.filter(token=token).exists()
+ return is_logged_in_counter(request)
CanAccessLookup = IsLoggedInCounter | HasPerm("core.access_lookup")
diff --git a/club/migrations/0017_linktype_clublink.py b/club/migrations/0017_linktype_clublink.py
index 097e77f3..3e6d53ca 100644
--- a/club/migrations/0017_linktype_clublink.py
+++ b/club/migrations/0017_linktype_clublink.py
@@ -25,8 +25,7 @@ class Migration(migrations.Migration):
"url_base",
models.URLField(
help_text=(
- "The base url that links with this type "
- "must respect (e.g. `https://www.instagram.com`)"
+ "The base url that links with this type must respect"
),
unique=True,
verbose_name="url base",
diff --git a/club/models.py b/club/models.py
index 6e98848e..a226cbcd 100644
--- a/club/models.py
+++ b/club/models.py
@@ -793,10 +793,7 @@ class LinkType(models.Model):
url_base = models.URLField(
"url base",
unique=True,
- help_text=_(
- "The base url that links with this type must respect (e.g. `%(url)s`)"
- )
- % {"url": "https://www.instagram.com"},
+ help_text=_("The base url that links with this type must respect"),
)
icon = models.CharField(
_("icon"),
diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py
index 38da5e95..d4da37fc 100644
--- a/core/management/commands/populate.py
+++ b/core/management/commands/populate.py
@@ -43,6 +43,7 @@ from core.models import BanGroup, Group, Page, PageRev, SithFile, User
from core.utils import resize_image
from counter.models import (
Counter,
+ CounterSellers,
Price,
Product,
ProductType,
@@ -364,10 +365,10 @@ class Command(BaseCommand):
Counter.objects.create(name="Carte AE", club=clubs.refound, type="OFFICE")
# Add barman to counter
- Counter.sellers.through.objects.bulk_create(
+ CounterSellers.objects.bulk_create(
[
- Counter.sellers.through(counter_id=1, user=skia), # MDE
- Counter.sellers.through(counter_id=2, user=krophil), # Foyer
+ CounterSellers(counter_id=1, user=skia, is_regular=True), # MDE
+ CounterSellers(counter_id=2, user=krophil, is_regular=True), # Foyer
]
)
diff --git a/core/templates/core/base/header.jinja b/core/templates/core/base/header.jinja
index de0169b9..5735f099 100644
--- a/core/templates/core/base/header.jinja
+++ b/core/templates/core/base/header.jinja
@@ -22,14 +22,9 @@
{% cache 100 "counters_activity" %}
- {# The sith has no periodic tasks manager
- and using cron jobs would be way too overkill here.
- Thus the barmen timeout is handled in the only place that
- 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() %}
+ {# It would be cleaner to handle the timeout with django-celery-beat,
+ but doing it here is simpler and less error-prone #}
+ {% do Counter.objects.filter(type="BAR").handle_timeout() %}
{% endcache %}
{% for bar in Counter.objects.annotate_has_barman(user).annotate_is_open().filter(type="BAR") %}
-
diff --git a/counter/forms.py b/counter/forms.py
index 8a631faa..6641a80e 100644
--- a/counter/forms.py
+++ b/counter/forms.py
@@ -9,6 +9,7 @@ from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Exists, OuterRef, Q
from django.forms import BaseModelFormSet
+from django.http import HttpRequest
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import ClockedSchedule
@@ -17,6 +18,7 @@ from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.models import Club
from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User, UserQuerySet
+from core.views import LoginForm
from core.views.forms import (
FutureDateTimeField,
NFCTextInput,
@@ -91,30 +93,18 @@ class StudentCardForm(forms.ModelForm):
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,
- 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)
- """
+ """Find a user to show its click page."""
code = forms.CharField(
label="Code",
max_length=StudentCard.UID_SIZE,
required=False,
- widget=NFCTextInput,
+ widget=NFCTextInput(attrs={"autofocus": True}),
)
id = forms.CharField(
- label=_("Select user"),
- help_text=None,
- widget=AutoCompleteSelectUser,
- required=False,
+ label=_("Select user"), widget=AutoCompleteSelectUser, required=False
)
- def as_p(self):
- self.fields["code"].widget.attrs["autofocus"] = True
- return super().as_p()
-
def clean(self):
cleaned_data = super().clean()
customer = None
@@ -136,11 +126,40 @@ class GetUserForm(forms.Form):
if customer is None or not customer.can_buy:
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
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):
allowed_refilling_methods = [
Refilling.PaymentMethod.CASH,
diff --git a/counter/middleware.py b/counter/middleware.py
new file mode 100644
index 00000000..7483a331
--- /dev/null
+++ b/counter/middleware.py
@@ -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
diff --git a/counter/migrations/0040_product_clic_limit.py b/counter/migrations/0040_product_clic_limit.py
index 42f2b3e9..b923520e 100644
--- a/counter/migrations/0040_product_clic_limit.py
+++ b/counter/migrations/0040_product_clic_limit.py
@@ -21,4 +21,5 @@ class Migration(migrations.Migration):
verbose_name="clic limit",
),
),
+ migrations.RemoveField(model_name="counter", name="token"),
]
diff --git a/counter/models.py b/counter/models.py
index 10920e86..6e87fb25 100644
--- a/counter/models.py
+++ b/counter/models.py
@@ -619,7 +619,6 @@ class Counter(models.Model):
view_groups = models.ManyToManyField(
Group, related_name="viewable_counters", blank=True
)
- token = models.CharField(_("token"), max_length=30, null=True, blank=True)
objects = CounterQuerySet.as_manager()
diff --git a/counter/signals.py b/counter/signals.py
index 27d7c142..dfa145f7 100644
--- a/counter/signals.py
+++ b/counter/signals.py
@@ -20,41 +20,34 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
+import random
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from core.middleware import get_signal_request
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():
request = get_signal_request()
if not request:
return None
- # Get a random barmen if deletion is from a counter
- session = getattr(request, "session", {})
- 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()
+ if request.barmen:
+ return random.choice(list(request.barmen))
# 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 None by default
return None
OperationLog(
- label=str(instance),
- operator=get_user(),
- operation_type=operation_type,
+ label=str(instance), operator=get_user(), operation_type=operation_type
).save()
diff --git a/counter/templates/counter/counter_main.jinja b/counter/templates/counter/counter_main.jinja
index b418f263..d4a14088 100644
--- a/counter/templates/counter/counter_main.jinja
+++ b/counter/templates/counter/counter_main.jinja
@@ -32,12 +32,11 @@
{% trans %}Total: {% endtrans %}{{ last_total }} €
{% endif %}
- {% if barmen %}
+ {% if can_click %}
{% trans %}Enter client code:{% endtrans %}
-
{% else %}
@@ -45,17 +44,36 @@
{% endif %}
{% if counter.type == 'BAR' %}
-
-
{% trans %}Barman: {% endtrans %}
+
{% trans %}Barmen:{% endtrans %}
+
+ {% if barmen_here %}
+
+
+
{% trans %}On this device{% endtrans %}
+ {% for b in barmen_here %}
+
{{ barman_logout_link(b) }}
+ {% endfor %}
+
+
+
{% trans %}Elsewhere{% endtrans %}
+ {% if barmen_here|length == barmen|length %}
+ {# all logged barmen are logged in this session #}
+
{% trans %}No barman logged elsewhere{% endtrans %}
+ {% else %}
+ {% for b in barmen %}
+ {%- if b not in barmen_here -%}
+
{{ barman_logout_link(b) }}
+ {%- endif -%}
+ {% endfor %}
+ {% endif %}
+
+
+ {% else %}
{% for b in barmen %}
{{ barman_logout_link(b) }}
{% endfor %}
-
-
+ {% endif %}
+ {{ login_fragment }}
{% endif %}
{% endblock %}
@@ -63,10 +81,10 @@
{{ super() }}