Aller au contenu

Views

CurrencyField(*args, **kwargs)

Bases: DecimalField

Custom database field used for currency.

Source code in accounting/models.py
def __init__(self, *args, **kwargs):
    kwargs["max_digits"] = 12
    kwargs["decimal_places"] = 2
    super().__init__(*args, **kwargs)

CanEditMixin

Bases: GenericContentPermissionMixinBuilder

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

Raises:

Type Description
PermissionDenied

if the user cannot edit this view's object.

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.

TabedViewMixin

Bases: View

Basic functions for displaying tabs in the template.

CashSummaryFormBase

Bases: Form

CounterEditForm

Bases: ModelForm

EticketForm

Bases: ModelForm

GetUserForm

Bases: 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)

NFCCardForm

Bases: Form

ProductEditForm(*args, **kwargs)

Bases: ModelForm

Source code in counter/forms.py
def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    if self.instance.id:
        self.fields["counters"].initial = self.instance.counters.all()

RefillForm

Bases: ModelForm

StudentCardForm

Bases: ModelForm

Form for adding student cards Only used for user profile since CounterClick is to complicated.

CashRegisterSummary

Bases: Model

is_owned_by(user)

Method to see if that object can be edited by the given user.

Source code in counter/models.py
def is_owned_by(self, user):
    """Method to see if that object can be edited by the given user."""
    if user.is_anonymous:
        return False
    return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)

CashRegisterSummaryItem

Bases: Model

Counter

Bases: Model

gen_token()

Generate a new token for this counter.

Source code in counter/models.py
def gen_token(self) -> None:
    """Generate a new token for this counter."""
    self.token = "".join(
        random.choice(string.ascii_letters + string.digits) for _ in range(30)
    )
    self.save()

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")
    ]

get_random_barman()

Return a random user being currently a barman.

Source code in counter/models.py
def get_random_barman(self) -> User:
    """Return a random user being currently a barman."""
    return random.choice(self.barmen_list)

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.

Source code in counter/models.py
def can_refill(self) -> bool:
    """Show if the counter authorize the refilling with physic money."""
    if self.type != "BAR":
        return False
    if self.id in SITH_COUNTER_OFFICES:
        # If the counter is either 'AE' or 'BdF', refills are authorized
        return True
    # at least one of the barmen is in the AE board
    ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
    return any(ae.get_membership_for(barman) for barman in self.barmen_list)

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
    """
    return (
        self.permanencies.exclude(end=None)
        .annotate(
            name=Concat(F("user__first_name"), Value(" "), F("user__last_name"))
        )
        .annotate(nickname=F("user__nick_name"))
        .annotate(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)
    return (
        self.sellings.filter(date__gte=since)
        .annotate(
            name=Concat(
                F("customer__user__first_name"),
                Value(" "),
                F("customer__user__last_name"),
            )
        )
        .annotate(nickname=F("customer__user__nick_name"))
        .annotate(promo=F("customer__user__promo"))
        .annotate(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

Bases: Model

Customer data of a User.

It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.

can_buy: bool property

Check if whether this customer has the right to purchase any item.

This must be not confused with the Product.can_be_sold_to(user) method as the present method returns an information about a customer whereas the other tells something about the relation between a User (not a Customer, don't mix them) and a Product.

save(*args, allow_negative=False, is_selling=False, **kwargs)

is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.

Source code in counter/models.py
def save(self, *args, allow_negative=False, is_selling=False, **kwargs):
    """is_selling : tell if the current action is a selling
    allow_negative : ignored if not a selling. Allow a selling to put the account in negative
    Those two parameters avoid blocking the save method of a customer if his account is negative.
    """
    if self.amount < 0 and (is_selling and not allow_negative):
        raise ValidationError(_("Not enough money"))
    super().save(*args, **kwargs)

get_or_create(user) classmethod

Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.

If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id

Example : ::

user = User.objects.get(pk=1)
account, created = Customer.get_or_create(user)
if created:
    print(f"created a new account with id {account.id}")
else:
    print(f"user has already an account, with {account.id} € on it"
Source code in counter/models.py
@classmethod
def get_or_create(cls, user: User) -> Tuple[Customer, bool]:
    """Work in pretty much the same way as the usual get_or_create method,
    but with the default field replaced by some under the hood.

    If the user has an account, return it as is.
    Else create a new account with no money on it and a new unique account id

    Example : ::

        user = User.objects.get(pk=1)
        account, created = Customer.get_or_create(user)
        if created:
            print(f"created a new account with id {account.id}")
        else:
            print(f"user has already an account, with {account.id} € on it"
    """
    if hasattr(user, "customer"):
        return user.customer, False

    # account_id are always a number with a letter appended
    account_id = (
        Customer.objects.order_by(Length("account_id"), "account_id")
        .values("account_id")
        .last()
    )
    if account_id is None:
        # legacy from the old site
        account = cls.objects.create(user=user, account_id="1504a")
        return account, True

    account_id = account_id["account_id"]
    account_num = int(account_id[:-1])
    while Customer.objects.filter(account_id=account_id).exists():
        # when entering the first iteration, we are using an already existing account id
        # so the loop should always execute at least one time
        account_num += 1
        account_id = f"{account_num}{random.choice(string.ascii_lowercase)}"

    account = cls.objects.create(user=user, account_id=account_id)
    return account, True

Eticket

Bases: Model

Eticket can be linked to a product an allows PDF generation.

is_owned_by(user)

Method to see if that object can be edited by the given user.

Source code in counter/models.py
def is_owned_by(self, user):
    """Method to see if that object can be edited by the given user."""
    if user.is_anonymous:
        return False
    return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)

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.

Product

Bases: Model

A product, with all its related information.

is_owned_by(user)

Method to see if that object can be edited by the given user.

Source code in counter/models.py
def is_owned_by(self, user):
    """Method to see if that object can be edited by the given user."""
    if user.is_anonymous:
        return False
    return user.is_in_group(
        pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
    ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)

can_be_sold_to(user)

Check if whether the user given in parameter has the right to buy this product or not.

This must be not confused with the Customer.can_buy() method as the present method returns an information about the relation between a User and a Product, whereas the other tells something about a Customer (and not a user, they are not the same model).

Returns:

Type Description
bool

True if the user can buy this product else False

Warning

This performs a db query, thus you can quickly have a N+1 queries problem if you call it in a loop. Hopefully, you can avoid that if you prefetch the buying_groups :

user = User.objects.get(username="foobar")
products = [
    p
    for p in Product.objects.prefetch_related("buying_groups")
    if p.can_be_sold_to(user)
]
Source code in counter/models.py
def can_be_sold_to(self, user: User) -> bool:
    """Check if whether the user given in parameter has the right to buy
    this product or not.

    This must be not confused with the Customer.can_buy()
    method as the present method returns an information
    about the relation between a User and a Product,
    whereas the other tells something about a Customer
    (and not a user, they are not the same model).

    Returns:
        True if the user can buy this product else False

    Warning:
        This performs a db query, thus you can quickly have
        a N+1 queries problem if you call it in a loop.
        Hopefully, you can avoid that if you prefetch the buying_groups :

        ```python
        user = User.objects.get(username="foobar")
        products = [
            p
            for p in Product.objects.prefetch_related("buying_groups")
            if p.can_be_sold_to(user)
        ]
        ```
    """
    buying_groups = list(self.buying_groups.all())
    if not buying_groups:
        return True
    return any(user.is_in_group(pk=group.id) for group in buying_groups)

ProductType

Bases: Model

A product type.

Useful only for categorizing.

is_owned_by(user)

Method to see if that object can be edited by the given user.

Source code in counter/models.py
def is_owned_by(self, user):
    """Method to see if that object can be edited by the given user."""
    if user.is_anonymous:
        return False
    return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)

Refilling

Bases: Model

Handle the refilling.

Selling

Bases: Model

Handle the sellings.

save(*args, allow_negative=False, **kwargs)

allow_negative : Allow this selling to use more money than available for this user.

Source code in counter/models.py
def save(self, *args, allow_negative=False, **kwargs):
    """allow_negative : Allow this selling to use more money than available for this user."""
    if not self.date:
        self.date = timezone.now()
    self.full_clean()
    if not self.is_validated:
        self.customer.amount -= self.quantity * self.unit_price
        self.customer.save(allow_negative=allow_negative, is_selling=True)
        self.is_validated = True
    user = self.customer.user
    if user.was_subscribed:
        if (
            self.product
            and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER
        ):
            sub = Subscription(
                member=user,
                subscription_type="un-semestre",
                payment_method="EBOUTIC",
                location="EBOUTIC",
            )
            sub.subscription_start = Subscription.compute_start()
            sub.subscription_start = Subscription.compute_start(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ]
            )
            sub.subscription_end = Subscription.compute_end(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ],
                start=sub.subscription_start,
            )
            sub.save()
        elif (
            self.product
            and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS
        ):
            sub = Subscription(
                member=user,
                subscription_type="deux-semestres",
                payment_method="EBOUTIC",
                location="EBOUTIC",
            )
            sub.subscription_start = Subscription.compute_start()
            sub.subscription_start = Subscription.compute_start(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ]
            )
            sub.subscription_end = Subscription.compute_end(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ],
                start=sub.subscription_start,
            )
            sub.save()
    if user.preferences.notify_on_click:
        Notification(
            user=user,
            url=reverse(
                "core:user_account_detail",
                kwargs={
                    "user_id": user.id,
                    "year": self.date.year,
                    "month": self.date.month,
                },
            ),
            param="%d x %s" % (self.quantity, self.label),
            type="SELLING",
        ).save()
    super().save(*args, **kwargs)
    if hasattr(self.product, "eticket"):
        self.send_mail_customer()

StudentCard

Bases: Model

Alternative way to connect a customer into a counter.

We are using Mifare DESFire EV1 specs since it's used for izly cards https://www.nxp.com/docs/en/application-note/AN10927.pdf UID is 7 byte long that means 14 hexa characters.

User

Bases: AbstractBaseUser

Defines the base user class, useable in every app.

This is almost the same as the auth module AbstractUser since it inherits from it, but some fields are required, and the username is generated automatically with the name of the user (see generate_username()).

Added field: nick_name, date_of_birth Required fields: email, first_name, last_name, date_of_birth

cached_groups: list[Group] property

Get the list of groups this user is in.

The result is cached for the default duration (should be 5 minutes)

Returns: A list of all the groups this user is in.

is_in_group(*, pk=None, name=None)

Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered.

The group will be fetched using the given parameter. If no group is found, return False. If a group is found, check if this user is in the latter.

Returns:

Type Description
bool

True if the user is the group, else False

Source code in core/models.py
def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool:
    """Check if this user is in the given group.
    Either a group id or a group name must be provided.
    If both are passed, only the id will be considered.

    The group will be fetched using the given parameter.
    If no group is found, return False.
    If a group is found, check if this user is in the latter.

    Returns:
         True if the user is the group, else False
    """
    if pk is not None:
        group: Optional[Group] = get_group(pk=pk)
    elif name is not None:
        group: Optional[Group] = get_group(name=name)
    else:
        raise ValueError("You must either provide the id or the name of the group")
    if group is None:
        return False
    if group.id == settings.SITH_GROUP_PUBLIC_ID:
        return True
    if group.id == settings.SITH_GROUP_SUBSCRIBERS_ID:
        return self.is_subscribed
    if group.id == settings.SITH_GROUP_OLD_SUBSCRIBERS_ID:
        return self.was_subscribed
    if group.id == settings.SITH_GROUP_ROOT_ID:
        return self.is_root
    if group.is_meta:
        # check if this group is associated with a club
        group.__class__ = MetaGroup
        club = group.associated_club
        if club is None:
            return False
        membership = club.get_membership_for(self)
        if membership is None:
            return False
        if group.name.endswith(settings.SITH_MEMBER_SUFFIX):
            return True
        return membership.role > settings.SITH_MAXIMUM_FREE_ROLE
    return group in self.cached_groups

age()

Return the age this user has the day the method is called. If the user has not filled his age, return 0.

Source code in core/models.py
@cached_property
def age(self) -> int:
    """Return the age this user has the day the method is called.
    If the user has not filled his age, return 0.
    """
    if self.date_of_birth is None:
        return 0
    today = timezone.now()
    age = today.year - self.date_of_birth.year
    # remove a year if this year's birthday is yet to come
    age -= (today.month, today.day) < (
        self.date_of_birth.month,
        self.date_of_birth.day,
    )
    return age

get_full_name()

Returns the first_name plus the last_name, with a space in between.

Source code in core/models.py
def get_full_name(self):
    """Returns the first_name plus the last_name, with a space in between."""
    full_name = "%s %s" % (self.first_name, self.last_name)
    return full_name.strip()

get_short_name()

Returns the short name for the user.

Source code in core/models.py
def get_short_name(self):
    """Returns the short name for the user."""
    if self.nick_name:
        return self.nick_name
    return self.first_name + " " + self.last_name

get_display_name()

Returns the display name of the user.

A nickname if possible, otherwise, the full name.

Source code in core/models.py
def get_display_name(self) -> str:
    """Returns the display name of the user.

    A nickname if possible, otherwise, the full name.
    """
    if self.nick_name:
        return "%s (%s)" % (self.get_full_name(), self.nick_name)
    return self.get_full_name()

get_age()

Returns the age.

Source code in core/models.py
def get_age(self):
    """Returns the age."""
    today = timezone.now()
    born = self.date_of_birth
    return (
        today.year - born.year - ((today.month, today.day) < (born.month, born.day))
    )

get_family(godfathers_depth=4, godchildren_depth=4)

Get the family of the user, with the given depth.

Parameters:

Name Type Description Default
godfathers_depth NonNegativeInt

The number of generations of godfathers to fetch

4
godchildren_depth NonNegativeInt

The number of generations of godchildren to fetch

4

Returns:

Type Description
set[through]

A list of family relationships in this user's family

Source code in core/models.py
def get_family(
    self,
    godfathers_depth: NonNegativeInt = 4,
    godchildren_depth: NonNegativeInt = 4,
) -> set[User.godfathers.through]:
    """Get the family of the user, with the given depth.

    Args:
        godfathers_depth: The number of generations of godfathers to fetch
        godchildren_depth: The number of generations of godchildren to fetch

    Returns:
        A list of family relationships in this user's family
    """
    res = []
    for depth, key, reverse_key in [
        (godfathers_depth, "from_user_id", "to_user_id"),
        (godchildren_depth, "to_user_id", "from_user_id"),
    ]:
        if depth == 0:
            continue
        links = list(User.godfathers.through.objects.filter(**{key: self.id}))
        res.extend(links)
        for _ in range(1, depth):  # noqa: F402 we don't care about gettext here
            ids = [getattr(c, reverse_key) for c in links]
            links = list(
                User.godfathers.through.objects.filter(
                    **{f"{key}__in": ids}
                ).exclude(id__in=[r.id for r in res])
            )
            if not links:
                break
            res.extend(links)
    return set(res)

email_user(subject, message, from_email=None, **kwargs)

Sends an email to this User.

Source code in core/models.py
def email_user(self, subject, message, from_email=None, **kwargs):
    """Sends an email to this User."""
    if from_email is None:
        from_email = settings.DEFAULT_FROM_EMAIL
    send_mail(subject, message, from_email, [self.email], **kwargs)

generate_username()

Generates a unique username based on the first and last names.

For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.

Returns:

Type Description
str

The generated username.

Source code in core/models.py
def generate_username(self) -> str:
    """Generates a unique username based on the first and last names.

    For example: Guy Carlier gives gcarlier, and gcarlier1 if the first one exists.

    Returns:
        The generated username.
    """

    def remove_accents(data):
        return "".join(
            x
            for x in unicodedata.normalize("NFKD", data)
            if unicodedata.category(x)[0] == "L"
        ).lower()

    user_name = (
        remove_accents(self.first_name[0] + self.last_name)
        .encode("ascii", "ignore")
        .decode("utf-8")
    )
    un_set = [u.username for u in User.objects.all()]
    if user_name in un_set:
        i = 1
        while user_name + str(i) in un_set:
            i += 1
        user_name += str(i)
    self.username = user_name
    return user_name

is_owner(obj)

Determine if the object is owned by the user.

Source code in core/models.py
def is_owner(self, obj):
    """Determine if the object is owned by the user."""
    if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
        return True
    if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
        return True
    return self.is_root

can_edit(obj)

Determine if the object can be edited by the user.

Source code in core/models.py
def can_edit(self, obj):
    """Determine if the object can be edited by the user."""
    if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
        return True
    if hasattr(obj, "edit_groups"):
        for pk in obj.edit_groups.values_list("pk", flat=True):
            if self.is_in_group(pk=pk):
                return True
    if isinstance(obj, User) and obj == self:
        return True
    return self.is_owner(obj)

can_view(obj)

Determine if the object can be viewed by the user.

Source code in core/models.py
def can_view(self, obj):
    """Determine if the object can be viewed by the user."""
    if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
        return True
    if hasattr(obj, "view_groups"):
        for pk in obj.view_groups.values_list("pk", flat=True):
            if self.is_in_group(pk=pk):
                return True
    return self.can_edit(obj)

clubs_with_rights()

The list of clubs where the user has rights

Source code in core/models.py
@cached_property
def clubs_with_rights(self) -> list[Club]:
    """The list of clubs where the user has rights"""
    memberships = self.memberships.ongoing().board().select_related("club")
    return [m.club for m in memberships]

CounterAdminMixin

Bases: View

Protect counter admin section.

StudentCardDeleteView

Bases: DeleteView, CanEditMixin

View used to delete a card from a user.

CounterTabsMixin

Bases: TabedViewMixin

CounterMain

Bases: CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin

The public (barman) view.

get_context_data(**kwargs)

We handle here the login form for the barman.

Source code in counter/views.py
def get_context_data(self, **kwargs):
    """We handle here the login form for the barman."""
    if self.request.method == "POST":
        self.object = self.get_object()
    self.object.update_activity()
    kwargs = super().get_context_data(**kwargs)
    kwargs["login_form"] = LoginForm()
    kwargs["login_form"].fields["username"].widget.attrs["autofocus"] = True
    kwargs[
        "login_form"
    ].cleaned_data = {}  # add_error fails if there are no cleaned_data
    if "credentials" in self.request.GET:
        kwargs["login_form"].add_error(None, _("Bad credentials"))
    if "sellers" in self.request.GET:
        kwargs["login_form"].add_error(None, _("User is not barman"))
    kwargs["form"] = self.get_form()
    kwargs["form"].cleaned_data = {}  # same as above
    if "bad_location" in self.request.GET:
        kwargs["form"].add_error(
            None, _("Bad location, someone is already logged in somewhere else")
        )
    if self.object.type == "BAR":
        kwargs["barmen"] = self.object.barmen_list
    elif self.request.user.is_authenticated:
        kwargs["barmen"] = [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.py
def form_valid(self, form):
    """We handle here the redirection, passing the user id of the asked customer."""
    self.kwargs["user_id"] = form.cleaned_data["user_id"]
    return super().form_valid(form)

CounterClick

Bases: CounterTabsMixin, CanViewMixin, DetailView

The click view This is a detail view not to have to worry about loading the counter Everything is made by hand in the post method.

get(request, *args, **kwargs)

Simple get view.

Source code in counter/views.py
def get(self, request, *args, **kwargs):
    """Simple get view."""
    if "basket" not in request.session:  # Init the basket session entry
        request.session["basket"] = {}
        request.session["basket_total"] = 0
    request.session["not_enough"] = False  # Reset every variable
    request.session["too_young"] = False
    request.session["not_allowed"] = False
    request.session["no_age"] = False
    self.refill_form = None
    ret = super().get(request, *args, **kwargs)
    if (self.object.type != "BAR" and not request.user.is_authenticated) or (
        self.object.type == "BAR" and len(self.object.barmen_list) == 0
    ):  # Check that at least one barman is logged in
        ret = self.cancel(request)  # Otherwise, go to main view
    return ret

post(request, *args, **kwargs)

Handle the many possibilities of the post request.

Source code in counter/views.py
def post(self, request, *args, **kwargs):
    """Handle the many possibilities of the post request."""
    self.object = self.get_object()
    self.refill_form = None
    if (self.object.type != "BAR" and not request.user.is_authenticated) or (
        self.object.type == "BAR" and len(self.object.barmen_list) < 1
    ):  # Check that at least one barman is logged in
        return self.cancel(request)
    if self.object.type == "BAR" and not (
        "counter_token" in self.request.session
        and self.request.session["counter_token"] == self.object.token
    ):  # Also check the token to avoid the bar to be stolen
        return HttpResponseRedirect(
            reverse_lazy(
                "counter:details",
                args=self.args,
                kwargs={"counter_id": self.object.id},
            )
            + "?bad_location"
        )
    if "basket" not in request.session:
        request.session["basket"] = {}
        request.session["basket_total"] = 0
    request.session["not_enough"] = False  # Reset every variable
    request.session["too_young"] = False
    request.session["not_allowed"] = False
    request.session["no_age"] = False
    request.session["not_valid_student_card_uid"] = False
    if self.object.type != "BAR":
        self.operator = request.user
    elif self.customer_is_barman():
        self.operator = self.customer.user
    else:
        self.operator = self.object.get_random_barman()
    action = self.request.POST.get("action", None)
    if action is None:
        action = parse_qs(request.body.decode()).get("action", [""])[0]
    if action == "add_product":
        self.add_product(request)
    elif action == "add_student_card":
        self.add_student_card(request)
    elif action == "del_product":
        self.del_product(request)
    elif action == "refill":
        self.refill(request)
    elif action == "code":
        return self.parse_code(request)
    elif action == "cancel":
        return self.cancel(request)
    elif action == "finish":
        return self.finish(request)
    context = self.get_context_data(object=self.object)
    return self.render_to_response(context)

add_product(request, q=1, p=None)

Add a product to the basket q is the quantity passed as integer p is the product id, passed as an integer.

Source code in counter/views.py
def add_product(self, request, q=1, p=None):
    """Add a product to the basket
    q is the quantity passed as integer
    p is the product id, passed as an integer.
    """
    pid = p or parse_qs(request.body.decode())["product_id"][0]
    pid = str(pid)
    price = self.get_price(pid)
    total = self.sum_basket(request)
    product: Product = self.get_product(pid)
    user: User = self.customer.user
    buying_groups = list(product.buying_groups.values_list("pk", flat=True))
    can_buy = len(buying_groups) == 0 or any(
        user.is_in_group(pk=group_id) for group_id in buying_groups
    )
    if not can_buy:
        request.session["not_allowed"] = True
        return False
    bq = 0  # Bonus quantity, for trays
    if (
        product.tray
    ):  # Handle the tray to adjust the quantity q to add and the bonus quantity bq
        total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6
        bq = int((total_qty_mod_6 + q) / 6)  # Integer division
        q -= bq
    if self.customer.amount < (
        total + round(q * float(price), 2)
    ):  # Check for enough money
        request.session["not_enough"] = True
        return False
    if product.is_unrecord_product and not self.is_record_product_ok(
        request, product
    ):
        request.session["not_allowed"] = True
        return False
    if product.limit_age >= 18 and not user.date_of_birth:
        request.session["no_age"] = True
        return False
    if product.limit_age >= 18 and user.is_banned_alcohol:
        request.session["not_allowed"] = True
        return False
    if user.is_banned_counter:
        request.session["not_allowed"] = True
        return False
    if (
        user.date_of_birth and self.customer.user.get_age() < product.limit_age
    ):  # Check if affordable
        request.session["too_young"] = True
        return False
    if pid in request.session["basket"]:  # Add if already in basket
        request.session["basket"][pid]["qty"] += q
        request.session["basket"][pid]["bonus_qty"] += bq
    else:  # or create if not
        request.session["basket"][pid] = {
            "qty": q,
            "price": int(price * 100),
            "bonus_qty": bq,
        }
    request.session.modified = True
    return True

add_student_card(request)

Add a new student card on the customer account.

Source code in counter/views.py
def add_student_card(self, request):
    """Add a new student card on the customer account."""
    uid = str(request.POST["student_card_uid"])
    if not StudentCard.is_valid(uid):
        request.session["not_valid_student_card_uid"] = True
        return False

    if not (
        self.object.type == "BAR"
        and "counter_token" in request.session
        and request.session["counter_token"] == self.object.token
        and self.object.is_open
    ):
        raise PermissionDenied
    StudentCard(customer=self.customer, uid=uid).save()
    return True

del_product(request)

Delete a product from the basket.

Source code in counter/views.py
def del_product(self, request):
    """Delete a product from the basket."""
    pid = parse_qs(request.body.decode())["product_id"][0]
    product = self.get_product(pid)
    if pid in request.session["basket"]:
        if (
            product.tray
            and (self.get_total_quantity_for_pid(request, pid) % 6 == 0)
            and request.session["basket"][pid]["bonus_qty"]
        ):
            request.session["basket"][pid]["bonus_qty"] -= 1
        else:
            request.session["basket"][pid]["qty"] -= 1
        if request.session["basket"][pid]["qty"] <= 0:
            del request.session["basket"][pid]
    request.session.modified = True

parse_code(request)

Parse the string entered by the barman.

This can be of two forms
  • <str>, where the string is the code of the product
  • <int>X<str>, where the integer is the quantity and str the code.
Source code in counter/views.py
def parse_code(self, request):
    """Parse the string entered by the barman.

    This can be of two forms :
        - `<str>`, where the string is the code of the product
        - `<int>X<str>`, where the integer is the quantity and str the code.
    """
    string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
    if string == "FIN":
        return self.finish(request)
    elif string == "ANN":
        return self.cancel(request)
    regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
    m = regex.match(string)
    if m is not None:
        nb = m.group("nb")
        code = m.group("code")
        nb = int(nb) if nb is not None else 1
        p = self.object.products.filter(code=code).first()
        if p is not None:
            self.add_product(request, nb, p.id)
    context = self.get_context_data(object=self.object)
    return self.render_to_response(context)

finish(request)

Finish the click session, and validate the basket.

Source code in counter/views.py
def finish(self, request):
    """Finish the click session, and validate the basket."""
    with transaction.atomic():
        request.session["last_basket"] = []
        if self.sum_basket(request) > self.customer.amount:
            raise DataError(_("You have not enough money to buy all the basket"))

        for pid, infos in request.session["basket"].items():
            # This duplicates code for DB optimization (prevent to load many times the same object)
            p = Product.objects.filter(pk=pid).first()
            if self.customer_is_barman():
                uprice = p.special_selling_price
            else:
                uprice = p.selling_price
            request.session["last_basket"].append(
                "%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name)
            )
            s = Selling(
                label=p.name,
                product=p,
                club=p.club,
                counter=self.object,
                unit_price=uprice,
                quantity=infos["qty"],
                seller=self.operator,
                customer=self.customer,
            )
            s.save()
            if infos["bonus_qty"]:
                s = Selling(
                    label=p.name + " (Plateau)",
                    product=p,
                    club=p.club,
                    counter=self.object,
                    unit_price=0,
                    quantity=infos["bonus_qty"],
                    seller=self.operator,
                    customer=self.customer,
                )
                s.save()
            self.customer.recorded_products -= self.compute_record_product(request)
            self.customer.save()
        request.session["last_customer"] = self.customer.user.get_display_name()
        request.session["last_total"] = "%0.2f" % self.sum_basket(request)
        request.session["new_customer_amount"] = str(self.customer.amount)
        del request.session["basket"]
        request.session.modified = True
        kwargs = {"counter_id": self.object.id}
        return HttpResponseRedirect(
            reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
        )

cancel(request)

Cancel the click session.

Source code in counter/views.py
def cancel(self, request):
    """Cancel the click session."""
    kwargs = {"counter_id": self.object.id}
    request.session.pop("basket", None)
    return HttpResponseRedirect(
        reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
    )

refill(request)

Refill the customer's account.

Source code in counter/views.py
def refill(self, request):
    """Refill the customer's account."""
    if not self.object.can_refill():
        raise PermissionDenied
    form = RefillForm(request.POST)
    if form.is_valid():
        form.instance.counter = self.object
        form.instance.operator = self.operator
        form.instance.customer = self.customer
        form.instance.save()
    else:
        self.refill_form = form

get_context_data(**kwargs)

Add customer to the context.

Source code in counter/views.py
def get_context_data(self, **kwargs):
    """Add customer to the context."""
    kwargs = super().get_context_data(**kwargs)
    products = self.object.products.select_related("product_type")
    if self.customer_is_barman():
        products = products.annotate(price=F("special_selling_price"))
    else:
        products = products.annotate(price=F("selling_price"))
    kwargs["products"] = products
    kwargs["categories"] = {}
    for product in kwargs["products"]:
        if product.product_type:
            kwargs["categories"].setdefault(product.product_type, []).append(
                product
            )
    kwargs["customer"] = self.customer
    kwargs["student_cards"] = self.customer.student_cards.all()
    kwargs["student_card_input"] = NFCCardForm()
    kwargs["basket_total"] = self.sum_basket(self.request)
    kwargs["refill_form"] = self.refill_form or RefillForm()
    kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
    kwargs["barmens_can_refill"] = self.object.can_refill()
    return kwargs

CounterAdminTabsMixin

Bases: TabedViewMixin

CounterListView

Bases: CounterAdminTabsMixin, CanViewMixin, ListView

A list view for the admins.

CounterEditView

Bases: CounterAdminTabsMixin, CounterAdminMixin, UpdateView

Edit a counter's main informations (for the counter's manager).

CounterEditPropView

Bases: CounterAdminTabsMixin, CounterAdminMixin, UpdateView

Edit a counter's main informations (for the counter's admin).

CounterCreateView

Bases: CounterAdminTabsMixin, CounterAdminMixin, CreateView

Create a counter (for the admins).

CounterDeleteView

Bases: CounterAdminTabsMixin, CounterAdminMixin, DeleteView

Delete a counter (for the admins).

ProductTypeListView

Bases: CounterAdminTabsMixin, CounterAdminMixin, ListView

A list view for the admins.

ProductTypeCreateView

Bases: CounterAdminTabsMixin, CounterAdminMixin, CreateView

A create view for the admins.

ProductTypeEditView

Bases: CounterAdminTabsMixin, CounterAdminMixin, UpdateView

An edit view for the admins.

ProductListView

Bases: CounterAdminTabsMixin, CounterAdminMixin, ListView

ArchivedProductListView

Bases: ProductListView

A list view for the admins.

ActiveProductListView

Bases: ProductListView

A list view for the admins.

ProductCreateView

Bases: CounterAdminTabsMixin, CounterAdminMixin, CreateView

A create view for the admins.

ProductEditView

Bases: CounterAdminTabsMixin, CounterAdminMixin, UpdateView

An edit view for the admins.

RefillingDeleteView

Bases: DeleteView

Delete a refilling (for the admins).

dispatch(request, *args, **kwargs)

We have here a very particular right handling, we can't inherit from CanEditPropMixin.

Source code in counter/views.py
def dispatch(self, request, *args, **kwargs):
    """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
    self.object = self.get_object()
    if timezone.now() - self.object.date <= timedelta(
        minutes=settings.SITH_LAST_OPERATIONS_LIMIT
    ) and is_logged_in_counter(request):
        self.success_url = reverse(
            "counter:details", kwargs={"counter_id": self.object.counter.id}
        )
        return super().dispatch(request, *args, **kwargs)
    elif self.object.is_owned_by(request.user):
        self.success_url = reverse(
            "core:user_account", kwargs={"user_id": self.object.customer.user.id}
        )
        return super().dispatch(request, *args, **kwargs)
    raise PermissionDenied

SellingDeleteView

Bases: DeleteView

Delete a selling (for the admins).

dispatch(request, *args, **kwargs)

We have here a very particular right handling, we can't inherit from CanEditPropMixin.

Source code in counter/views.py
def dispatch(self, request, *args, **kwargs):
    """We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
    self.object = self.get_object()
    if timezone.now() - self.object.date <= timedelta(
        minutes=settings.SITH_LAST_OPERATIONS_LIMIT
    ) and is_logged_in_counter(request):
        self.success_url = reverse(
            "counter:details", kwargs={"counter_id": self.object.counter.id}
        )
        return super().dispatch(request, *args, **kwargs)
    elif self.object.is_owned_by(request.user):
        self.success_url = reverse(
            "core:user_account", kwargs={"user_id": self.object.customer.user.id}
        )
        return super().dispatch(request, *args, **kwargs)
    raise PermissionDenied

CashRegisterSummaryForm(*args, **kwargs)

Bases: Form

Provide the cash summary form.

Source code in counter/views.py
def __init__(self, *args, **kwargs):
    instance = kwargs.pop("instance", None)
    super().__init__(*args, **kwargs)
    if instance:
        self.fields["ten_cents"].initial = (
            instance.ten_cents.quantity if instance.ten_cents else 0
        )
        self.fields["twenty_cents"].initial = (
            instance.twenty_cents.quantity if instance.twenty_cents else 0
        )
        self.fields["fifty_cents"].initial = (
            instance.fifty_cents.quantity if instance.fifty_cents else 0
        )
        self.fields["one_euro"].initial = (
            instance.one_euro.quantity if instance.one_euro else 0
        )
        self.fields["two_euros"].initial = (
            instance.two_euros.quantity if instance.two_euros else 0
        )
        self.fields["five_euros"].initial = (
            instance.five_euros.quantity if instance.five_euros else 0
        )
        self.fields["ten_euros"].initial = (
            instance.ten_euros.quantity if instance.ten_euros else 0
        )
        self.fields["twenty_euros"].initial = (
            instance.twenty_euros.quantity if instance.twenty_euros else 0
        )
        self.fields["fifty_euros"].initial = (
            instance.fifty_euros.quantity if instance.fifty_euros else 0
        )
        self.fields["hundred_euros"].initial = (
            instance.hundred_euros.quantity if instance.hundred_euros else 0
        )
        self.fields["check_1_quantity"].initial = (
            instance.check_1.quantity if instance.check_1 else 0
        )
        self.fields["check_2_quantity"].initial = (
            instance.check_2.quantity if instance.check_2 else 0
        )
        self.fields["check_3_quantity"].initial = (
            instance.check_3.quantity if instance.check_3 else 0
        )
        self.fields["check_4_quantity"].initial = (
            instance.check_4.quantity if instance.check_4 else 0
        )
        self.fields["check_5_quantity"].initial = (
            instance.check_5.quantity if instance.check_5 else 0
        )
        self.fields["check_1_value"].initial = (
            instance.check_1.value if instance.check_1 else 0
        )
        self.fields["check_2_value"].initial = (
            instance.check_2.value if instance.check_2 else 0
        )
        self.fields["check_3_value"].initial = (
            instance.check_3.value if instance.check_3 else 0
        )
        self.fields["check_4_value"].initial = (
            instance.check_4.value if instance.check_4 else 0
        )
        self.fields["check_5_value"].initial = (
            instance.check_5.value if instance.check_5 else 0
        )
        self.fields["comment"].initial = instance.comment
        self.fields["emptied"].initial = instance.emptied
        self.instance = instance
    else:
        self.instance = None

CounterLastOperationsView

Bases: CounterTabsMixin, CanViewMixin, DetailView

Provide the last operations to allow barmen to delete them.

dispatch(request, *args, **kwargs)

We have here again a very particular right handling.

Source code in counter/views.py
def dispatch(self, request, *args, **kwargs):
    """We have here again a very particular right handling."""
    self.object = self.get_object()
    if is_logged_in_counter(request) and self.object.barmen_list:
        return super().dispatch(request, *args, **kwargs)
    return HttpResponseRedirect(
        reverse("counter:details", kwargs={"counter_id": self.object.id})
        + "?bad_location"
    )

get_context_data(**kwargs)

Add form to the context.

Source code in counter/views.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

CounterCashSummaryView

Bases: CounterTabsMixin, CanViewMixin, DetailView

Provide the cash summary form.

dispatch(request, *args, **kwargs)

We have here again a very particular right handling.

Source code in counter/views.py
def dispatch(self, request, *args, **kwargs):
    """We have here again a very particular right handling."""
    self.object = self.get_object()
    if is_logged_in_counter(request) and self.object.barmen_list:
        return super().dispatch(request, *args, **kwargs)
    return HttpResponseRedirect(
        reverse("counter:details", kwargs={"counter_id": self.object.id})
        + "?bad_location"
    )

get_context_data(**kwargs)

Add form to the context.

Source code in counter/views.py
def get_context_data(self, **kwargs):
    """Add form to the context."""
    kwargs = super().get_context_data(**kwargs)
    kwargs["form"] = self.form
    return kwargs

CounterActivityView

Bases: DetailView

Show the bar activity.

CounterStatView

Bases: DetailView, CounterAdminMixin

Show the bar stats.

get_context_data(**kwargs)

Add stats to the context.

Source code in counter/views.py
def get_context_data(self, **kwargs):
    """Add stats to the context."""
    counter: Counter = self.object
    semester_start = get_start_of_semester()
    office_hours = counter.get_top_barmen()
    kwargs = super().get_context_data(**kwargs)
    kwargs.update(
        {
            "counter": counter,
            "current_semester": get_semester_code(),
            "total_sellings": counter.get_total_sales(since=semester_start),
            "top_customers": counter.get_top_customers(since=semester_start)[:100],
            "top_barman": office_hours[:100],
            "top_barman_semester": (
                office_hours.filter(start__gt=semester_start)[:100]
            ),
        }
    )
    return kwargs

CashSummaryEditView

Bases: CounterAdminTabsMixin, CounterAdminMixin, UpdateView

Edit cash summaries.

CashSummaryListView

Bases: CounterAdminTabsMixin, CounterAdminMixin, ListView

Display a list of cash summaries.

get_context_data(**kwargs)

Add sums to the context.

Source code in counter/views.py
def get_context_data(self, **kwargs):
    """Add sums to the context."""
    kwargs = super().get_context_data(**kwargs)
    form = CashSummaryFormBase(self.request.GET)
    kwargs["form"] = form
    kwargs["summaries_sums"] = {}
    kwargs["refilling_sums"] = {}
    for c in Counter.objects.filter(type="BAR").all():
        refillings = Refilling.objects.filter(counter=c)
        cashredistersummaries = CashRegisterSummary.objects.filter(counter=c)
        if form.is_valid() and form.cleaned_data["begin_date"]:
            refillings = refillings.filter(
                date__gte=form.cleaned_data["begin_date"]
            )
            cashredistersummaries = cashredistersummaries.filter(
                date__gte=form.cleaned_data["begin_date"]
            )
        else:
            last_summary = (
                CashRegisterSummary.objects.filter(counter=c, emptied=True)
                .order_by("-date")
                .first()
            )
            if last_summary:
                refillings = refillings.filter(date__gt=last_summary.date)
                cashredistersummaries = cashredistersummaries.filter(
                    date__gt=last_summary.date
                )
            else:
                refillings = refillings.filter(
                    date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)
                )  # My birth date should be old enough
                cashredistersummaries = cashredistersummaries.filter(
                    date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)
                )
        if form.is_valid() and form.cleaned_data["end_date"]:
            refillings = refillings.filter(date__lte=form.cleaned_data["end_date"])
            cashredistersummaries = cashredistersummaries.filter(
                date__lte=form.cleaned_data["end_date"]
            )
        kwargs["summaries_sums"][c.name] = sum(
            [s.get_total() for s in cashredistersummaries.all()]
        )
        kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()])
    return kwargs

InvoiceCallView

Bases: CounterAdminTabsMixin, CounterAdminMixin, TemplateView

get_context_data(**kwargs)

Add sums to the context.

Source code in counter/views.py
def get_context_data(self, **kwargs):
    """Add sums to the context."""
    kwargs = super().get_context_data(**kwargs)
    kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
    if "month" in self.request.GET:
        start_date = datetime.strptime(self.request.GET["month"], "%Y-%m")
    else:
        start_date = datetime(
            year=timezone.now().year,
            month=(timezone.now().month + 10) % 12 + 1,
            day=1,
        )
    start_date = start_date.replace(tzinfo=tz.utc)
    end_date = (start_date + timedelta(days=32)).replace(
        day=1, hour=0, minute=0, microsecond=0
    )
    from django.db.models import Case, F, Sum, When

    kwargs["sum_cb"] = sum(
        [
            r.amount
            for r in Refilling.objects.filter(
                payment_method="CARD",
                is_validated=True,
                date__gte=start_date,
                date__lte=end_date,
            )
        ]
    )
    kwargs["sum_cb"] += sum(
        [
            s.quantity * s.unit_price
            for s in Selling.objects.filter(
                payment_method="CARD",
                is_validated=True,
                date__gte=start_date,
                date__lte=end_date,
            )
        ]
    )
    kwargs["start_date"] = start_date
    kwargs["sums"] = (
        Selling.objects.values("club__name")
        .annotate(
            selling_sum=Sum(
                Case(
                    When(
                        date__gte=start_date,
                        date__lt=end_date,
                        then=F("unit_price") * F("quantity"),
                    ),
                    output_field=CurrencyField(),
                )
            )
        )
        .exclude(selling_sum=None)
        .order_by("-selling_sum")
    )
    return kwargs

EticketListView

Bases: CounterAdminTabsMixin, CounterAdminMixin, ListView

A list view for the admins.

EticketCreateView

Bases: CounterAdminTabsMixin, CounterAdminMixin, CreateView

Create an eticket.

EticketEditView

Bases: CounterAdminTabsMixin, CounterAdminMixin, UpdateView

Edit an eticket.

EticketPDFView

Bases: CanViewMixin, DetailView

Display the PDF of an eticket.

CounterRefillingListView

Bases: CounterAdminTabsMixin, CounterAdminMixin, ListView

List of refillings on a counter.

StudentCardFormView

Bases: FormView

Add a new student card.

get_semester_code(d=None)

Return the semester code of the given date. If no date is given, return the semester code of the current semester.

The semester code is an upper letter (A for autumn, P for spring), followed by the last two digits of the year. For example, the autumn semester of 2018 is "A18".

Parameters:

Name Type Description Default
d Optional[date]

the date to use to compute the semester. If None, use today's date.

None

Returns:

Type Description
str

the semester code corresponding to the given date

Source code in core/utils.py
def get_semester_code(d: Optional[date] = None) -> str:
    """Return the semester code of the given date.
    If no date is given, return the semester code of the current semester.

    The semester code is an upper letter (A for autumn, P for spring),
    followed by the last two digits of the year.
    For example, the autumn semester of 2018 is "A18".

    Args:
        d: the date to use to compute the semester. If None, use today's date.

    Returns:
        the semester code corresponding to the given date
    """
    if d is None:
        d = localdate()

    start = get_start_of_semester(d)

    if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN:
        return "A" + str(start.year)[-2:]
    return "P" + str(start.year)[-2:]

get_start_of_semester(today=None)

Return the date of the start of the semester of the given date. If no date is given, return the start date of the current semester.

The current semester is computed as follows:

  • If the date is between 15/08 and 31/12 => Autumn semester.
  • If the date is between 01/01 and 15/02 => Autumn semester of the previous year.
  • If the date is between 15/02 and 15/08 => Spring semester

Parameters:

Name Type Description Default
today Optional[date]

the date to use to compute the semester. If None, use today's date.

None

Returns:

Type Description
date

the date of the start of the semester

Source code in core/utils.py
def get_start_of_semester(today: Optional[date] = None) -> date:
    """Return the date of the start of the semester of the given date.
    If no date is given, return the start date of the current semester.

    The current semester is computed as follows:

    - If the date is between 15/08 and 31/12  => Autumn semester.
    - If the date is between 01/01 and 15/02  => Autumn semester of the previous year.
    - If the date is between 15/02 and 15/08  => Spring semester

    Args:
        today: the date to use to compute the semester. If None, use today's date.

    Returns:
        the date of the start of the semester
    """
    if today is None:
        today = localdate()

    autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)
    spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)

    if today >= autumn:  # between 15/08 (included) and 31/12 -> autumn semester
        return autumn
    if today >= spring:  # between 15/02 (included) and 15/08 -> spring semester
        return spring
    # between 01/01 and 15/02 -> autumn semester of the previous year
    return autumn.replace(year=autumn.year - 1)

is_logged_in_counter(request)

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)
  • The current session has a counter token associated with it.
  • A counter with this token exists.
Source code in counter/utils.py
def is_logged_in_counter(request: HttpRequest) -> 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)
    - The current session has a counter token associated with it.
    - A counter with this token exists.
    """
    referer_ok = (
        "HTTP_REFERER" in request.META
        and resolve(urlparse(request.META["HTTP_REFERER"]).path).app_name == "counter"
    )
    return (
        (referer_ok or request.resolver_match.app_name == "counter")
        and "counter_token" in request.session
        and request.session["counter_token"]
        and Counter.objects.filter(token=request.session["counter_token"]).exists()
    )

counter_login(request, counter_id)

Log a user in a counter.

A successful login will result in the beginning of a counter duty for the user.

Source code in counter/views.py
@require_POST
def counter_login(request: HttpRequest, counter_id: int) -> HttpResponseRedirect:
    """Log a user in a counter.

    A successful login will result in the beginning of a counter duty
    for the user.
    """
    counter = get_object_or_404(Counter, pk=counter_id)
    form = LoginForm(request, data=request.POST)
    if not form.is_valid():
        return redirect(counter.get_absolute_url() + "?credentials")
    user = form.get_user()
    if not counter.sellers.contains(user) or user in counter.barmen_list:
        return redirect(counter.get_absolute_url() + "?sellers")
    if len(counter.barmen_list) == 0:
        counter.gen_token()
    request.session["counter_token"] = counter.token
    counter.permanencies.create(user=user, start=timezone.now())
    return redirect(counter)

counter_logout(request, counter_id)

End the permanency of a user in this counter.

Source code in counter/views.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"]).update(
        end=F("activity")
    )
    return redirect("counter:details", counter_id=counter_id)