-
Tom Teichler authoredTom Teichler authored
models.py 15.53 KiB
from datetime import datetime
from decimal import Decimal
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from ckeditor.fields import RichTextField
from colorfield.fields import ColorField
from payments import PurchasedItem
from aleksis.apps.tezor.models.base import Client
from aleksis.apps.tezor.models.invoice import Invoice, InvoiceGroup
from aleksis.core.mixins import ExtensibleModel
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
class RegistrationState(ExtensibleModel):
name = models.CharField(verbose_name=_("Name"), max_length=255)
colour = ColorField(blank=True, verbose_name=_("Colour"))
def __str__(self) -> str:
return self.name
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"))
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)
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)
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"))
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,
)
def save(self, *args, **kwargs):
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()
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)
)
def can_register(self, request=None):
now = datetime.today().date()
if request and request.user.is_authenticated:
if request.user.person in self.linked_group.members.all():
return False
if EventRegistration.objects.filter(person=request.user.person).exists():
return False
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:
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})
@property
def booked_percentage(self):
return self.linked_group.members.count() / self.max_participants * 100
@property
def members_persons(self):
return self.linked_group.members.all()
@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,
)
class Voucher(ExtensibleModel):
class Meta:
verbose_name = _("Vouchers")
verbose_name_plural = _("Vouchers")
code = models.CharField(max_length=255, blank=True, default="")
event = models.ForeignKey(
Event,
related_name="vouchers",
verbose_name=_("Event"),
on_delete=models.CASCADE,
null=True,
)
person = models.ForeignKey(
Person,
related_name="vouchers",
verbose_name=_("Person"),
on_delete=models.CASCADE,
)
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:
self.code = generate_random_code(5, 3)
super().save(*args, **kwargs)
class EventRegistration(ExtensibleModel):
event = models.ForeignKey(
Event, on_delete=models.CASCADE, verbose_name=_("Event"), related_name="registrations"
)
person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person"))
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)
comment = models.TextField(verbose_name=_("Comment / remarks"), blank=True, default="")
medical_information = models.TextField(
verbose_name=_("Medical information / intolerances"), blank=True, default=""
)
voucher = models.ForeignKey(
Voucher,
on_delete=models.CASCADE,
verbose_name=_("Voucher"),
blank=True,
null=True,
)
donation = models.PositiveIntegerField(verbose_name=_("Donation"), blank=True, null=True)
accepted_terms = models.ManyToManyField(
Terms,
verbose_name=_("Accepted terms"),
related_name="registrations",
)
states = models.ManyToManyField(
RegistrationState, verbose_name=_("States"), related_name="registrations"
)
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)
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!"))
def retract(self):
# Remove person from group
self.event.linked_group.members.remove(self.person)
# Mark registration as retracted
self.retracted = True
self.retracted_date = datetime.today()
self.save()
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))
def get_invoice(self):
# FIXME Maybe do not hard-code this
client, __ = Client.objects.get_or_create(name="Teckids e.V.")
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],
"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,
},
)
return invoice
def get_purchased_items(self):
# FIXME Maybe do not hard-code the tax rate and currency
# First, return main amount
yield PurchasedItem(
name=self.event.display_name,
quantity=1,
price=Decimal(self.event.cost / 1.07),
currency="EUR",
sku="EVENT",
tax_rate=7,
)
# If a dnoation was made, add it
if self.donation:
yield PurchasedItem(
name=_("Social Sponsoring / Extra Donation"),
quantity=1,
price=Decimal(self.donation),
currency="EUR",
sku="DONAT",
tax_rate=0,
)
# If a voucher was used, add it
if self.voucher:
yield PurchasedItem(
name=_("Voucher / Granted discount"),
quantity=1,
price=Decimal(-1 * self.voucher.discount * (self.event.cost / 1.07) / 100),
currency="EUR",
sku="DISCO",
tax_rate=7,
)
def _get_total_amount(self):
total, total_tax = 0, 0
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
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()
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):
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)
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
)