byceps/byceps

View on GitHub
byceps/blueprints/admin/shop/article/forms.py

Summary

Maintainability
A
0 mins
Test Coverage
F
53%
"""
byceps.blueprints.admin.shop.article.forms
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2014-2024 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""

from collections.abc import Iterable
from datetime import date, datetime, time
from decimal import Decimal

from flask_babel import gettext, lazy_gettext, pgettext
from wtforms import (
    BooleanField,
    DateField,
    DecimalField,
    IntegerField,
    SelectField,
    StringField,
    TimeField,
)
from wtforms.validators import (
    InputRequired,
    NumberRange,
    Optional,
    ValidationError,
)

from byceps.services.brand.models import BrandID
from byceps.services.party import party_service
from byceps.services.shop.article import article_service
from byceps.services.shop.shop.models import ShopID
from byceps.services.ticketing import ticket_category_service
from byceps.services.user_badge.models import Badge
from byceps.util.l10n import LocalizedForm


class _ArticleBaseForm(LocalizedForm):
    name = StringField(lazy_gettext('Name'), validators=[InputRequired()])
    price_amount = DecimalField(
        lazy_gettext('Unit price'), places=2, validators=[InputRequired()]
    )
    tax_rate = DecimalField(
        lazy_gettext('Tax rate'),
        places=1,
        validators=[
            InputRequired(),
            NumberRange(min=Decimal('0.0'), max=Decimal('99.9')),
        ],
    )
    available_from_date = DateField(
        lazy_gettext('Available from date'), validators=[Optional()]
    )
    available_from_time = TimeField(
        lazy_gettext('Available from time'), validators=[Optional()]
    )
    available_until_date = DateField(
        lazy_gettext('Available until date'), validators=[Optional()]
    )
    available_until_time = TimeField(
        lazy_gettext('Available until time'), validators=[Optional()]
    )
    total_quantity = IntegerField(
        lazy_gettext('Total quantity'), validators=[InputRequired()]
    )
    max_quantity_per_order = IntegerField(
        lazy_gettext('Maximum quantity per order'),
        validators=[InputRequired()],
    )
    not_directly_orderable = BooleanField(
        lazy_gettext('can only be ordered indirectly')
    )
    separate_order_required = BooleanField(
        lazy_gettext('must be ordered separately')
    )

    @staticmethod
    def validate_available_from_date(form, field):
        """Ensure that either both date and time or neither of them is given."""
        d = form.available_from_date.data
        t = form.available_from_time.data
        _validate_date_and_time(d, t)

    @staticmethod
    def validate_available_from_time(form, field):
        """Ensure that either both date and time or neither of them is given."""
        d = form.available_from_date.data
        t = form.available_from_time.data
        _validate_date_and_time(d, t)

    @staticmethod
    def validate_available_until_date(form, field):
        """Ensure that either both date and time or neither of them is given."""
        d = form.available_until_date.data
        t = form.available_until_time.data
        _validate_date_and_time(d, t)

        available_from = form._get_available_from()
        available_until = form._get_available_until()
        _validate_availability_range(available_from, available_until)

    @staticmethod
    def validate_available_until_time(form, field):
        """Ensure that either both date and time or neither of them is given."""
        d = form.available_until_date.data
        t = form.available_until_time.data
        _validate_date_and_time(d, t)

        available_from = form._get_available_from()
        available_until = form._get_available_until()
        _validate_availability_range(available_from, available_until)

    def _get_available_from(self):
        d = self.available_from_date.data
        t = self.available_from_time.data
        if (d is None) or (t is None):
            return None

        return datetime.combine(d, t)

    def _get_available_until(self):
        d = self.available_until_date.data
        t = self.available_until_time.data
        if (d is None) or (t is None):
            return None

        return datetime.combine(d, t)


def _validate_date_and_time(d: date, t: time):
    if ((d is None) and (t is not None)) or ((d is not None) and (t is None)):
        raise ValidationError(
            gettext(
                'Either date and time must be specified or neither of them.'
            )
        )


def _validate_availability_range(
    available_from: datetime, available_until: datetime
):
    """Ensure that the availability range's begin is before its end."""
    if (
        (available_from is not None)
        and (available_until is not None)
        and (available_from >= available_until)
    ):
        raise ValidationError(
            gettext(
                'The end of the availability period must be after its begin.'
            )
        )


class ArticleCreateForm(_ArticleBaseForm):
    def __init__(self, shop_id: ShopID, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._shop_id = shop_id

    article_number_sequence_id = SelectField(
        lazy_gettext('Article number sequence'), validators=[InputRequired()]
    )

    def set_article_number_sequence_choices(self, sequences):
        sequences.sort(key=lambda seq: seq.prefix, reverse=True)

        choices = [(str(seq.id), seq.prefix) for seq in sequences]
        choices.insert(0, ('', '<' + pgettext('sequence', 'none') + '>'))
        self.article_number_sequence_id.choices = choices

    @staticmethod
    def validate_name(form, field):
        name = field.data.strip()

        if not article_service.is_name_available(form._shop_id, name):
            raise ValidationError(
                lazy_gettext(
                    'This value is not available. Please choose another.'
                )
            )


class TicketArticleCreateForm(ArticleCreateForm):
    ticket_category_id = SelectField(
        lazy_gettext('Ticket category'), [InputRequired()]
    )

    def set_ticket_category_choices(self, brand_id: BrandID) -> None:
        self.ticket_category_id.choices = _get_ticket_category_choices(brand_id)


class TicketBundleArticleCreateForm(ArticleCreateForm):
    ticket_category_id = SelectField(
        lazy_gettext('Ticket category'), [InputRequired()]
    )
    ticket_quantity = IntegerField(
        lazy_gettext('Ticket quantity'), [InputRequired()]
    )

    def set_ticket_category_choices(self, brand_id: BrandID) -> None:
        self.ticket_category_id.choices = _get_ticket_category_choices(brand_id)


class ArticleUpdateForm(_ArticleBaseForm):
    def __init__(self, shop_id: ShopID, current_name: str, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._shop_id = shop_id
        self._current_name = current_name

    @staticmethod
    def validate_name(form, field):
        name = field.data.strip()

        if name != form._current_name and not article_service.is_name_available(
            form._shop_id, name
        ):
            raise ValidationError(
                lazy_gettext(
                    'This value is not available. Please choose another.'
                )
            )


class ArticleAttachmentCreateForm(LocalizedForm):
    article_to_attach_id = SelectField(
        lazy_gettext('Article'), validators=[InputRequired()]
    )
    quantity = IntegerField(
        lazy_gettext('Quantity'), validators=[InputRequired()]
    )

    def set_article_to_attach_choices(self, attachable_articles):
        def to_label(article):
            return f'{article.item_number} – {article.name}'

        choices = [
            (str(article.id), to_label(article))
            for article in attachable_articles
        ]
        choices.sort(key=lambda choice: choice[1])

        self.article_to_attach_id.choices = choices


class ArticleNumberSequenceCreateForm(LocalizedForm):
    prefix = StringField(
        lazy_gettext('Static prefix'), validators=[InputRequired()]
    )


class RegisterBadgeAwardingActionForm(LocalizedForm):
    badge_id = SelectField(lazy_gettext('Badge'), [InputRequired()])

    def set_badge_choices(self, badges: Iterable[Badge]) -> None:
        choices = [(str(badge.id), badge.label) for badge in badges]
        choices.sort(key=lambda choice: choice[1])
        self.badge_id.choices = choices


class RegisterTicketsCreationActionForm(LocalizedForm):
    category_id = SelectField(lazy_gettext('Category'), [InputRequired()])

    def set_category_choices(self, brand_id: BrandID) -> None:
        self.category_id.choices = _get_ticket_category_choices(brand_id)


class RegisterTicketBundlesCreationActionForm(LocalizedForm):
    category_id = SelectField(lazy_gettext('Category'), [InputRequired()])
    ticket_quantity = IntegerField(
        lazy_gettext('Ticket quantity'), [InputRequired()]
    )

    def set_category_choices(self, brand_id: BrandID) -> None:
        self.category_id.choices = _get_ticket_category_choices(brand_id)


def _get_ticket_category_choices(brand_id: BrandID) -> list[tuple[str, str]]:
    choices = [
        (str(category.id), f'{party.title}: {category.title}')
        for party in party_service.get_active_parties(brand_id)
        for category in ticket_category_service.get_categories_for_party(
            party.id
        )
    ]
    choices.sort(key=lambda choice: choice[1])
    return choices