Aller au contenu

Home

SESSION_PERMANENCES_KEY = 'permanence_ids' module-attribute

CanViewMixin

Bases: GenericContentPermissionMixinBuilder

Ensure the user has permission to view this view's object.

Raises:

Type Description
PermissionDenied

if the user cannot edit this view's object.

FragmentMixin

Bases: TemplateResponseMixin, AllowFragment, ContextMixin

Make a view buildable as a fragment that can be embedded in a template.

Most fragments are used in two different ways : - in the request/response cycle, like any regular view - in templates, where the rendering is done in another view

This mixin aims to simplify the initial fragment rendering. The rendered fragment will then be able to re-render itself through the request/response cycle if it uses HTMX.

Example

class MyFragment(FragmentMixin, FormView):
    template_name = "app/fragment.jinja"
    form_class = MyForm
    success_url = reverse_lazy("foo:bar")

# in another view :
def some_view(request):
    fragment = MyFragment.as_fragment()
    return render(
        request,
        "app/template.jinja",
        context={"fragment": fragment(request)}
    )

# in urls.py
urlpatterns = [
    path("foo/view", some_view),
    path("foo/fragment", MyFragment.as_view()),
]

reload_on_redirect = False class-attribute instance-attribute

If True, this fragment will trigger a full page reload on redirect.

UseFragmentsMixin

Bases: ContextMixin

Mark a view as using fragments.

This mixin is not mandatory (you may as well render manually your fragments in the get_context_data method). However, the interface of this class bring some distinction between fragments and other context data, which may reduce boilerplate.

Example

class FooFragment(FragmentMixin, FormView): ...

class BarFragment(FragmentMixin, FormView): ...

class AdminFragment(FragmentMixin, FormView): ...

class MyView(UseFragmentsMixin, TemplateView)
    template_name = "app/view.jinja"
    fragments = {
        "foo": FooFragment
        "bar": BarFragment(template_name="some_template.jinja")
    }
    fragments_data = {
        "foo": {"some": "data"}  # this will be passed to the FooFragment renderer
    }

    def get_fragments(self):
        res = super().get_fragments()
        if self.request.user.is_superuser:
            res["admin_fragment"] = AdminFragment
        return res

get_fragment_data()

Return eventual data used to initialize the fragments.

Source code in core/views/mixins.py
def get_fragment_data(self) -> dict[str, dict[str, Any]]:
    """Return eventual data used to initialize the fragments."""
    return self.fragment_data if self.fragment_data is not None else {}

get_fragment_context_data()

Return the rendered fragments as context data.

Source code in core/views/mixins.py
def get_fragment_context_data(self) -> dict[str, SafeString]:
    """Return the rendered fragments as context data."""
    res = {}
    data = self.get_fragment_data()
    for name, fragment in self.get_fragments().items():
        is_cls = inspect.isclass(fragment) and issubclass(fragment, FragmentMixin)
        _fragment = fragment.as_fragment() if is_cls else fragment
        fragment_data = data.get(name, {})
        res[name] = _fragment(self.request, **fragment_data)
    return res

CounterLoginForm(*args, request, counter, **kwargs)

Bases: 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
Source code in counter/forms.py
def __init__(self, *args, request: HttpRequest, counter: Counter, **kwargs):
    super().__init__(*args, **kwargs)
    self.counter = counter
    self.request = request

GetUserForm

Bases: Form

Find a user to show its click page.

Counter

Bases: Model

barmen_list()

Returns the barman list as list of User.

Source code in counter/models.py
@cached_property
def barmen_list(self) -> list[User]:
    """Returns the barman list as list of User."""
    return [
        p.user for p in self.permanencies.filter(end=None).select_related("user")
    ]

update_activity()

Update the barman activity to prevent timeout.

Source code in counter/models.py
def update_activity(self) -> None:
    """Update the barman activity to prevent timeout."""
    self.permanencies.filter(end=None).update(activity=timezone.now())

can_refill()

Show if the counter authorize the refilling with physic money.

Refills are authorized if a user having the required permission is currently logged in.

Source code in counter/models.py
@cached_property
def can_refill(self) -> bool:
    """Show if the counter authorize the refilling with physic money.

    Refills are authorized if a user having the required permission
    is currently logged in.
    """
    return self.type == "BAR" and (
        User.objects.with_perm("counter.add_refilling")
        .filter(id__in=[u.id for u in self.barmen_list])
        .exists()
    )

get_top_barmen()

Return a QuerySet querying the office hours stats of all the barmen of all time of this counter, ordered by descending number of hours.

Each element of the QuerySet corresponds to a barman and has the following data
  • the full name (first name + last name) of the barman
  • the nickname of the barman
  • the promo of the barman
  • the total number of office hours the barman did attend
Source code in counter/models.py
def get_top_barmen(self) -> QuerySet:
    """Return a QuerySet querying the office hours stats of all the barmen of all time
    of this counter, ordered by descending number of hours.

    Each element of the QuerySet corresponds to a barman and has the following data :
        - the full name (first name + last name) of the barman
        - the nickname of the barman
        - the promo of the barman
        - the total number of office hours the barman did attend
    """
    name_expr = Concat(F("user__first_name"), Value(" "), F("user__last_name"))
    return (
        self.permanencies.exclude(end=None)
        .annotate(
            name=name_expr, nickname=F("user__nick_name"), promo=F("user__promo")
        )
        .values("user", "name", "nickname", "promo")
        .annotate(perm_sum=Sum(F("end") - F("start")))
        .exclude(perm_sum=None)
        .order_by("-perm_sum")
    )

get_top_customers(since=None)

Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent.

Each element of the QuerySet corresponds to a customer and has the following data :

  • the full name (first name + last name) of the customer
  • the nickname of the customer
  • the amount of money spent by the customer

Parameters:

Name Type Description Default
since datetime | date | None

timestamp from which to perform the calculation

None
Source code in counter/models.py
def get_top_customers(self, since: datetime | date | None = None) -> QuerySet:
    """Return a QuerySet querying the money spent by customers of this counter
    since the specified date, ordered by descending amount of money spent.

    Each element of the QuerySet corresponds to a customer and has the following data :

    - the full name (first name + last name) of the customer
    - the nickname of the customer
    - the amount of money spent by the customer

    Args:
        since: timestamp from which to perform the calculation
    """
    if since is None:
        since = get_start_of_semester()
    if isinstance(since, date):
        since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)
    name_expr = Concat(
        F("customer__user__first_name"), Value(" "), F("customer__user__last_name")
    )
    return (
        self.sellings.filter(date__gte=since)
        .annotate(
            name=name_expr,
            nickname=F("customer__user__nick_name"),
            promo=F("customer__user__promo"),
            user=F("customer__user"),
        )
        .values("user", "promo", "name", "nickname")
        .annotate(
            selling_sum=Sum(
                F("unit_price") * F("quantity"), output_field=CurrencyField()
            )
        )
        .filter(selling_sum__gt=0)
        .order_by("-selling_sum")
    )

get_total_sales(since=None)

Compute and return the total turnover of this counter since the given date.

By default, the date is the start of the current semester.

Parameters:

Name Type Description Default
since datetime | date | None

timestamp from which to perform the calculation

None

Returns:

Type Description
CurrencyField

Total revenue earned at this counter.

Source code in counter/models.py
def get_total_sales(self, since: datetime | date | None = None) -> CurrencyField:
    """Compute and return the total turnover of this counter since the given date.

    By default, the date is the start of the current semester.

    Args:
        since: timestamp from which to perform the calculation

    Returns:
        Total revenue earned at this counter.
    """
    if since is None:
        since = get_start_of_semester()
    if isinstance(since, date):
        since = datetime(since.year, since.month, since.day, tzinfo=tz.utc)
    return self.sellings.filter(date__gte=since).aggregate(
        total=Sum(
            F("quantity") * F("unit_price"),
            default=0,
            output_field=CurrencyField(),
        )
    )["total"]

customer_is_barman(customer)

Check if this counter is a bar and if the customer is currently logged in. This is useful to compute special prices.

Source code in counter/models.py
def customer_is_barman(self, customer: Customer | User) -> bool:
    """Check if this counter is a `bar` and if the customer is currently logged in.
    This is useful to compute special prices."""

    # Customer and User are two different tables,
    # but they share the same primary key
    return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list)

Permanency

Bases: Model

A permanency of a barman, on a counter.

This aims at storing a traceability of who was barman where and when. Mainly for dick size contest establishing the top 10 barmen of the semester.

CounterTabsMixin

CounterLoginFragment

Bases: FragmentMixin, SingleObjectMixin, FormView

CounterMain

Bases: CounterTabsMixin, UseFragmentsMixin, CanViewMixin, SingleObjectMixin, FormView

The public (barman) view.

get_context_data(**kwargs)

We handle here the login form for the barman.

Source code in counter/views/home.py
def get_context_data(self, **kwargs):
    """We handle here the login form for the barman."""
    kwargs = super().get_context_data(**kwargs)
    if self.object.type == "BAR":
        kwargs["barmen"] = self.object.barmen_list
        kwargs["barmen_here"] = list(
            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:
        kwargs["last_basket"] = self.request.session.pop("last_basket")
        kwargs["last_customer"] = self.request.session.pop("last_customer")
        kwargs["last_total"] = self.request.session.pop("last_total")
        kwargs["new_customer_amount"] = self.request.session.pop(
            "new_customer_amount"
        )
    return kwargs

form_valid(form)

We handle here the redirection, passing the user id of the asked customer.

Source code in counter/views/home.py
def form_valid(self, form: GetUserForm):
    """We handle here the redirection, passing the user id of the asked customer."""
    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)

CounterLastOperationsView

Bases: CounterTabsMixin, UserPassesTestMixin, DetailView

Provide the last operations to allow barmen to delete them.

get_context_data(**kwargs)

Add form to the context.

Source code in counter/views/home.py
def get_context_data(self, **kwargs):
    """Add form to the context."""
    kwargs = super().get_context_data(**kwargs)
    threshold = timezone.now() - timedelta(
        minutes=settings.SITH_LAST_OPERATIONS_LIMIT
    )
    kwargs["last_refillings"] = (
        self.object.refillings.filter(date__gte=threshold)
        .select_related("operator", "customer__user")
        .order_by("-id")[:20]
    )
    kwargs["last_sellings"] = (
        self.object.sellings.filter(date__gte=threshold)
        .select_related("seller", "customer__user")
        .order_by("-id")[:20]
    )
    return kwargs

CounterActivityView

Bases: DetailView

Show the bar activity.

is_logged_in_counter(request, counter=None)

Check if the request is sent from a device logged to a counter.

The request must also be sent within the frame of a counter's activity. Trying to use this function to manage access to non-sas related resources probably won't work.

A request is considered as coming from a logged counter if :

  • Its referer comes from the counter app (eg. fetching user pictures from the click UI) or the request path belongs to the counter app (eg. the barman went back to the main by missclick and go back to the counter)
  • There are barmen logged in the current session
Source code in counter/utils.py
def is_logged_in_counter(request: HttpRequest, counter: Counter | None = None) -> bool:
    """Check if the request is sent from a device logged to a counter.

    The request must also be sent within the frame of a counter's activity.
    Trying to use this function to manage access to non-sas
    related resources probably won't work.

    A request is considered as coming from a logged counter if :

    - Its referer comes from the counter app
      (eg. fetching user pictures from the click UI)
      or the request path belongs to the counter app
      (eg. the barman went back to the main by missclick and go back
      to the counter)
    - There are barmen logged in the current session
    """
    referer_ok = (
        "HTTP_REFERER" in request.META
        and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter"
    )
    if not referer_ok and request.resolver_match.app_name != "counter":
        return False

    if counter is None:
        return bool(request.barmen)
    return bool(request.barmen) and request.barmen.issubset(set(counter.barmen_list))

counter_logout(request, counter_id)

End the permanency of a user in this counter.

Source code in counter/views/home.py
@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)