Skip to content
Snippets Groups Projects
models.py 17.7 KiB
Newer Older
Tom Teichler's avatar
Tom Teichler committed
from datetime import datetime, timedelta
Nik | Klampfradler's avatar
Nik | Klampfradler committed
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
Tom Teichler's avatar
Tom Teichler committed
from django.utils.text import slugify
from django.utils.timezone import now
Tom Teichler's avatar
Tom Teichler committed
from django.utils.translation import gettext_lazy as _

from ckeditor.fields import RichTextField
from colorfield.fields import ColorField
Tom Teichler's avatar
Tom Teichler committed
from payments import PurchasedItem
Nik | Klampfradler's avatar
Nik | Klampfradler committed
from aleksis.apps.tezor.models.base import Client
from aleksis.apps.tezor.models.invoice import Invoice, InvoiceGroup
from aleksis.core.mixins import ExtensibleModel, GlobalPermissionModel
Tom Teichler's avatar
Tom Teichler committed
from aleksis.core.models import Group, Person
from aleksis.core.util.core_helpers import generate_random_code, get_site_preferences
from aleksis.core.util.email import send_email
from .data_checks import EventMembersSyncDataCheck
Hangzhi Yu's avatar
Hangzhi Yu committed
FIELD_CHOICES = (
    ("BooleanField", _("Boolean (Yes/No)")),
    ("CharField", _("Text (one line)")),
    ("DateField", _("Date")),
    ("DateTimeField", _("Date and time")),
    ("DecimalField", _("Decimal number")),
    ("EmailField", _("E-mail address")),
    ("IntegerField", _("Integer")),
    ("GenericIPAddressField", _("IP address")),
    ("NullBooleanField", _("Boolean or empty (Yes/No/Neither)")),
    ("TimeField", _("Time")),
    ("URLField", _("URL / Link")),
)

Tom Teichler's avatar
Tom Teichler committed

class RegistrationState(ExtensibleModel):
Nik | Klampfradler's avatar
Nik | Klampfradler committed
    name = models.CharField(verbose_name=_("Name"), max_length=255)
    colour = ColorField(blank=True, verbose_name=_("Colour"))

    def __str__(self) -> str:
        return self.name


Tom Teichler's avatar
Tom Teichler committed
class Terms(ExtensibleModel):
    title = models.CharField(max_length=255, verbose_name=_("Title"))
    term = RichTextField(verbose_name=_("Term"))
    confirmation_text = models.TextField(verbose_name=_("Confirmation text"))
Tom Teichler's avatar
Tom Teichler committed

    def __str__(self) -> str:
        return self.title


class InfoMailing(ExtensibleModel):
    subject = models.CharField(max_length=255, verbose_name=_("subject"))
    text = RichTextField(verbose_name=_("Text"))
    reply_to = models.EmailField(verbose_name=_("Request replies to"), blank=True)

    active = models.BooleanField(verbose_name=_("Mailing is active"), default=False)

    sender = models.EmailField(verbose_name=_("Sender"), blank=True)
    send_to_person = models.BooleanField(verbose_name=_("Send to registered person"), default=True)
    send_to_guardians = models.BooleanField(verbose_name=_("Send to guardians"), default=False)
Tom Teichler's avatar
Tom Teichler committed
    send_to_retracted = models.BooleanField(
        verbose_name=_("Send to participants who retracted"), default=False
    )
    send_to_not_checked_in = models.BooleanField(
        verbose_name=_("Send to participants who did not check in"), default=True
    )

    def __str__(self) -> str:
        return self.subject

    @classmethod
    def get_active_mailings(cls):
        return cls.objects.filter(active=True)

    def send(self):
        for event in self.events.all():
            through = EventInfoMailingThrough.objects.get(info_mailing=self, event=event)
            sent_to = through.sent_to.all()

            filter_args = {}
            if not self.send_to_retracted:
                filter_args["retracted"] = False
            if not self.send_to_not_checked_in:
                filter_args["checked_in"] = True

            for registration in event.registrations.filter(**filter_args):
                if registration.person in sent_to:
                    continue

                subject = self.subject.format(
                    event=event, registration=registration, person=registration.person
                )
                body = self.text.format(
                    event=event, registration=registration, person=registration.person
                )

                if self.send_to_person:
                    to = [registration.person.email]
                    if self.send_to_guardians:
                        cc = registration.person.guardians.values_list("email", flat=True).all()
                    else:
                        cc = []
                elif self.send_to_guardians:
                    to = registration.person.guardians.values_list("email", flat=True).all()
                    cc = []

                sender = self.sender or get_site_preferences()["mail__address"]
                reply_to = self.reply_to or sender

                context = {"subject": subject, "body": body}
                send_email(
                    template_name="info_mailing",
                    context=context,
                    from_email=sender,
                    recipient_list=to,
                    cc=cc,
                    headers={
                        "Reply-To": reply_to,
                    },
                )
                through.sent_to.add(registration.person)
Hangzhi Yu's avatar
Hangzhi Yu committed
class EventAdditionalField(ExtensibleModel):
    """An additional field that can be linked to a event."""

Hangzhi Yu's avatar
Hangzhi Yu committed
    title = models.CharField(verbose_name=_("Title of field"), max_length=255, unique=True)
Hangzhi Yu's avatar
Hangzhi Yu committed
    field_type = models.CharField(
        verbose_name=_("Type of field"), choices=FIELD_CHOICES, max_length=50
    )
    required = models.BooleanField(verbose_name=_("Required"), default=False)
    help_text = models.TextField(verbose_name=_("Help text / description"), blank=True)

    def __str__(self) -> str:
        return self.title

    class Meta:
        verbose_name = _("Addtitional field for events")
        verbose_name_plural = _("Addtitional fields for events")


class Event(ExtensibleModel):
    data_checks = [EventMembersSyncDataCheck]

    # Event details
    display_name = models.CharField(verbose_name=_("Display name"), max_length=255)
    linked_group = models.OneToOneField(
        Group, on_delete=models.CASCADE, verbose_name=_("Group"), related_name="linked_event"
    )
    description = models.CharField(max_length=500, verbose_name=_("Description"))
    published = models.BooleanField(default=False, verbose_name=_("Publish"))
    place = models.CharField(max_length=50, verbose_name="Place")
    slug = models.SlugField(max_length=255, verbose_name=_("Slug"), blank=True)

    # Date details
    date_event = models.DateField(verbose_name=_("Date of event"))
    date_registration = models.DateField(verbose_name=_("Registration deadline"))
    date_retraction = models.DateField(verbose_name=_("Retraction deadline"))

    # Other details
    cost = models.IntegerField(verbose_name=_("Cost in €"))
    max_participants = models.PositiveSmallIntegerField(verbose_name=_("Maximum participants"))
    information = RichTextField(verbose_name=_("Information about the event"))
Tom Teichler's avatar
Tom Teichler committed
    terms = models.ManyToManyField(Terms, verbose_name=_("Terms"), related_name="event", blank=True)
    info_mailings = models.ManyToManyField(
        InfoMailing,
        verbose_name=_("Info mailings"),
        related_name="events",
        through="EventInfoMailingThrough",
        blank=True,
Hangzhi Yu's avatar
Hangzhi Yu committed
    additional_fields = models.ManyToManyField(
        EventAdditionalField, verbose_name=_("Additional fields"), blank=True
    )

Tom Teichler's avatar
Tom Teichler committed
    def save(self, *args, **kwargs):
Tom Teichler's avatar
Tom Teichler committed
        if not self.slug:
            if self.linked_group.short_name:
                self.slug = slugify(self.linked_group.short_name)
            else:
                self.slug = slugify(self.display_name)

        super().save(*args, **kwargs)
        self.sync_group_members()
Tom Teichler's avatar
Tom Teichler committed
    def __str__(self) -> str:
        return self.display_name

    def sync_group_members(self):
        self.linked_group.members.set(
            self.registrations.filter(retracted=False).values_list("person", flat=True)
        )
Tom Teichler's avatar
Tom Teichler committed
    def can_register(self, request=None):
        now = datetime.today().date()
Tom Teichler's avatar
Tom Teichler committed

        if request and request.user.is_authenticated:
            if request.user.person in self.linked_group.members.all():
                return False

            if self.registrations.filter(person=request.user.person).exists():
                return False

Tom Teichler's avatar
Tom Teichler committed
            if (
                Voucher.objects.filter(event=self, person=request.user.person, used=False).count()
                > 0
            ):
                return True

        if self.linked_group.members.count() >= self.max_participants:
Tom Teichler's avatar
Tom Teichler committed
            return False

        if self.date_registration:
            return self.date_registration >= now
        return self.date_event > now
    def get_absolute_url(self):
        return reverse("event_by_name", kwargs={"slug": self.slug})
    def individual_cost(self, request=None):
Julian's avatar
Julian committed
        if (
            request
            and request.user.is_authenticated
            and Voucher.objects.filter(event=self, person=request.user.person, used=False).exists()
        ):
            voucher = Voucher.objects.get(event=self, person=request.user.person, used=False)
            individual_cost = (100 - voucher.discount) * self.cost / 100
            return individual_cost
Tom Teichler's avatar
Tom Teichler committed
    @property
    def booked_percentage(self):
        return self.linked_group.members.count() / self.max_participants * 100
Tom Teichler's avatar
Tom Teichler committed

    @property
    def members_persons(self):
        return self.linked_group.members.all()
Tom Teichler's avatar
Tom Teichler committed

    @property
    def owners_persons(self):
        return self.linked_group.owners.all()
    @classmethod
    def upcoming_published_events(cls):
        return Event.objects.filter(published=True, date_event__gte=now())

class EventInfoMailingThrough(ExtensibleModel):
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    info_mailing = models.ForeignKey(InfoMailing, on_delete=models.CASCADE)

    sent_to = models.ManyToManyField(
        Person,
        verbose_name=_("Sent to persons"),
        related_name="received_info_mailings",
        editable=False,
        blank=True,
    )


Tom Teichler's avatar
Tom Teichler committed
class Voucher(ExtensibleModel):
    class Meta:
        verbose_name = _("Vouchers")
        verbose_name_plural = _("Vouchers")

Tom Teichler's avatar
Tom Teichler committed
    code = models.CharField(max_length=255, blank=True, default="")
Tom Teichler's avatar
Tom Teichler committed
    event = models.ForeignKey(
        Event,
        related_name="vouchers",
        verbose_name=_("Event"),
        on_delete=models.CASCADE,
        null=True,
    )
    person = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler committed
        Person,
        related_name="vouchers",
        verbose_name=_("Person"),
        on_delete=models.CASCADE,
Tom Teichler's avatar
Tom Teichler committed
    )
    discount = models.IntegerField(default=100)

    used = models.BooleanField(default=False)
    used_person_uid = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        verbose_name=_("Used by"),
        related_name="used_vouchers",
        null=True,
    )
    deleted = models.BooleanField(default=False)

    def __str__(self) -> str:
        return self.code

    def save(self, *args, **kwargs):
        if not self.code:
Tom Teichler's avatar
Tom Teichler committed
            self.code = generate_random_code(5, 3)
        super().save(*args, **kwargs)

Tom Teichler's avatar
Tom Teichler committed

class EventRegistration(ExtensibleModel):
    event = models.ForeignKey(
        Event, on_delete=models.CASCADE, verbose_name=_("Event"), related_name="registrations"
    )
Tom Teichler's avatar
Tom Teichler committed
    person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person"))
Tom Teichler's avatar
Tom Teichler committed
    date_registered = models.DateTimeField(auto_now_add=True, verbose_name=_("Registration date"))
    school = models.CharField(verbose_name=_("Name of school"), max_length=255)
    school_class = models.CharField(verbose_name=_("School class"), max_length=255)
    school_place = models.CharField(verbose_name=_("Place of the school"), max_length=255)
Tom Teichler's avatar
Tom Teichler committed
    comment = models.TextField(verbose_name=_("Comment / remarks"), blank=True, default="")
    medical_information = models.TextField(
        verbose_name=_("Medical information / intolerances"), blank=True, default=""
    )
Tom Teichler's avatar
Tom Teichler committed
    voucher = models.ForeignKey(
Tom Teichler's avatar
Tom Teichler committed
        Voucher,
        on_delete=models.CASCADE,
        verbose_name=_("Voucher"),
        blank=True,
        null=True,
Tom Teichler's avatar
Tom Teichler committed
    )
    donation = models.PositiveIntegerField(verbose_name=_("Donation"), blank=True, null=True)
    accepted_terms = models.ManyToManyField(
        Terms,
        verbose_name=_("Accepted terms"),
        related_name="registrations",
    )
Tom Teichler's avatar
Tom Teichler committed
    states = models.ManyToManyField(
        RegistrationState, verbose_name=_("States"), related_name="registrations"
    )
Tom Teichler's avatar
Tom Teichler committed
    retracted = models.BooleanField(verbose_name=_("Retracted"), default=False)
    retracted_date = models.DateField(verbose_name=_("Retracted at"), null=True, blank=True)
    checked_in = models.BooleanField(verbose_name=_("Checked in"), default=False)
    checked_in_date = models.DateTimeField(verbose_name=_("Checked in at"), null=True, blank=True)

    cost = models.IntegerField(verbose_name=_("Cost in €"), null=True, blank=True)

    def mark_checked_in(self):
        if not self.checked_in:
            self.checked_in = True
            self.checked_in_date = now()
            self.save()
        else:
            raise ValidationError(_("Person is already checked in!"))

Tom Teichler's avatar
Tom Teichler committed
    def retract(self):
        # Mark registration as retracted
        self.retracted = True
        self.retracted_date = datetime.today()
        self.save()
        # Remove person from group
        self.event.linked_group.members.remove(self.person)
Tom Teichler's avatar
Tom Teichler committed
    def get_person(self):
        return self.person

    def get_billing_email_recipients(self):
        return [self.person.email] + list(self.person.guardians.values_list("email", flat=True))

Nik | Klampfradler's avatar
Nik | Klampfradler committed
    def get_invoice(self):
        # FIXME Maybe do not hard-code this
        client, __ = Client.objects.get_or_create(name="Teckids e.V.")
Tom Teichler's avatar
Tom Teichler committed
        group, __ = InvoiceGroup.objects.get_or_create(
            name="Hack'n'Fun-Veranstaltungen",
            client=client,
            defaults={
                "template_name": "paweljong/invoice_pdf.html",
            },
        )

        invoice, __ = Invoice.objects.get_or_create(
            for_content_type=ContentType.objects.get_for_model(self),
            for_object_id=self.pk,
            defaults={
                "group": group,
                "number": f"HNF-{self.date_registered.strftime('%Y-%m')}-{self.id}",
                "currency": "EUR",
                "total": self._get_total_amount()[0],
Tom Teichler's avatar
Tom Teichler committed
                "due_date": now().date() + timedelta(days=7),
Tom Teichler's avatar
Tom Teichler committed
                "tax": self._get_total_amount()[1],
                "description": _("Participation of {} in event {}").format(
                    self.person.addressing_name, self.event.display_name
                ),
                "billing_first_name": self.person.first_name,
                "billing_last_name": self.person.last_name,
                "billing_address_1": f"{self.person.street} {self.person.housenumber}",
                "billing_city": self.person.place,
                "billing_postcode": self.person.postal_code,
                "billing_email": self.person.email,
            },
        )
Nik | Klampfradler's avatar
Nik | Klampfradler committed

        return invoice

    def get_purchased_items(self):
        # FIXME Maybe do not hard-code the tax rate and currency
        # First, return main amount
Tom Teichler's avatar
Tom Teichler committed
        yield PurchasedItem(
            name=self.event.display_name,
            quantity=1,
            price=Decimal(self.cost / 1.07),
Tom Teichler's avatar
Tom Teichler committed
            currency="EUR",
            sku="EVENT",
Tom Teichler's avatar
Tom Teichler committed
            tax_rate=7,
        )
Nik | Klampfradler's avatar
Nik | Klampfradler committed

        # If a dnoation was made, add it
        if self.donation:
Tom Teichler's avatar
Tom Teichler committed
            yield PurchasedItem(
                name=_("Social Sponsoring / Extra Donation"),
                quantity=1,
                price=Decimal(self.donation),
Tom Teichler's avatar
Tom Teichler committed
                currency="EUR",
                sku="DONAT",
Tom Teichler's avatar
Tom Teichler committed
                tax_rate=0,
            )
Nik | Klampfradler's avatar
Nik | Klampfradler committed

        # If a voucher was used, add it
        if self.voucher:
Tom Teichler's avatar
Tom Teichler committed
            yield PurchasedItem(
                name=_("Voucher / Granted discount"),
                quantity=1,
                price=Decimal(-1 * self.voucher.discount * (self.cost / 1.07) / 100),
Tom Teichler's avatar
Tom Teichler committed
                currency="EUR",
                sku="DISCO",
Tom Teichler's avatar
Tom Teichler committed
                tax_rate=7,
            )
Nik | Klampfradler's avatar
Nik | Klampfradler committed

    def _get_total_amount(self):
Nik | Klampfradler's avatar
Nik | Klampfradler committed
        for item in self.get_purchased_items():
            tax = item.price * item.tax_rate / 100
            total += item.price + tax
            total_tax += tax
        return total, total_tax
Tom Teichler's avatar
Tom Teichler committed
    def __str__(self) -> str:
        return f"{self.event}, {self.person.first_name} {self.person.last_name}"
    def save(self, *args, **kwargs):
        self.event.sync_group_members()
Tom Teichler's avatar
Tom Teichler committed
        if self.cost is None:
            self.cost = self.event.cost
        super().save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        self.event.sync_group_members()
        super().delete(*args, **kwargs)

    class Meta:
        verbose_name = _("Event registration")
        verbose_name_plural = _("Event registrations")
        constraints = [
            models.UniqueConstraint(
                fields=["person", "event"], name="unique_person_registration_per_event"
            )
        ]


class Checkpoint(ExtensibleModel):
Tom Teichler's avatar
Tom Teichler committed
    event = models.ForeignKey(
        Event, verbose_name=_("Related event"), related_name="checkpoints", on_delete=models.CASCADE
    )
    person = models.ForeignKey(
        Person,
        verbose_name=_("Checked person"),
        related_name="event_checkpoints",
        on_delete=models.CASCADE,
    )
    checked_by = models.ForeignKey(
        Person,
        verbose_name=_("Checked by person"),
        related_name="event_checkpoints_created",
        on_delete=models.CASCADE,
    )

    comment = models.CharField(max_length=60, verbose_name=_("Comment"))

    timestamp = models.DateTimeField(verbose_name=_("Date and time of check"), auto_now_add=True)
Tom Teichler's avatar
Tom Teichler committed
    lat = models.DecimalField(
        max_digits=10, decimal_places=8, verbose_name=_("Latitude of check"), blank=True, null=True
    )
    lon = models.DecimalField(
        max_digits=11, decimal_places=8, verbose_name=_("Longitude of check"), blank=True, null=True
    )


class PaweljongGlobalPermissions(GlobalPermissionModel):
    class Meta:
        managed = False
        permissions = (
            ("generate_lists", _("Can generate lists of participants of a group")),
        )