This commit is contained in:
relikd
2023-05-29 15:20:07 +02:00
commit 1380b156d8
126 changed files with 3612 additions and 0 deletions

10
app/base/models/__init__.py Executable file
View File

@@ -0,0 +1,10 @@
from app.base.models.account import Account
from app.base.models.booking import Booking
from app.base.models.booking_type import BookingType
from app.base.models.course import Course
from app.base.models.course_visit import CourseVisit
from app.base.models.note import Note
from app.base.models.person import Person
from app.base.models.trait import Trait
from app.base.models.trait_mapping import TraitMapping
from app.base.models.transaction import Transaction

32
app/base/models/account.py Executable file
View File

@@ -0,0 +1,32 @@
from django.db import models
from app.base.forms.fields import CurrencyField
from app.base.models.person import Person
class Account(models.Model):
user = models.OneToOneField(Person, on_delete=models.CASCADE)
balance = CurrencyField('Guthaben')
locked = models.BooleanField('Gesperrt', default=False)
class Meta:
verbose_name = 'Konto'
verbose_name_plural = 'Konten'
def __str__(self):
return f"{self.user}'s Konto"
# def change_balance(self, amount):
# self.balance = self.balance - amount
# self.save()
# return True
# def lock(self):
# self.locked = True
# self.save()
# return True
# def unlock(self):
# self.locked = False
# self.save()
# return True

63
app/base/models/booking.py Executable file
View File

@@ -0,0 +1,63 @@
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils import timezone
from app.base.forms.fields import DateTimeField, TextField
from datetime import datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from django.db.models import OuterRef
from app.base.models.person import Person
from app.base.models.booking_type import BookingType
class Booking(models.Model):
type: 'models.ForeignKey[BookingType]' = models.ForeignKey(
'BookingType', on_delete=models.PROTECT, verbose_name='Art')
user: 'models.ForeignKey[Person]' = models.ForeignKey(
'Person', on_delete=models.CASCADE, verbose_name='Nutzer:in')
begin_time = DateTimeField('Beginn', default=datetime.now)
end_time = DateTimeField('Ende', blank=True, null=True)
comment = TextField('Kommentar', blank=True)
class Meta:
verbose_name = 'Buchung'
verbose_name_plural = 'Buchungen'
def get_absolute_url(self):
return reverse('booking:detail', kwargs={'pk': self.pk})
def __str__(self):
return 'Buchung von {} am {}: {}-{} Uhr'.format(
self.user,
self.begin_time.strftime('%d.%m.%Y'),
self.begin_time.strftime('%H:%M'),
self.end_time.strftime('%H:%M') if self.end_time else '')
@property
def duration(self) -> 'int|None':
if self.end_time:
return round((self.end_time - self.begin_time).seconds / 60)
return None
@property
def calculated_price(self):
traits = self.user.traits_at_date(self.begin_time).values_list('trait')
traits = set(x[0] for x in traits)
return self.type.price_with_traits(self.duration or 0, traits)
@staticmethod
def latest_checkin_query(for_user: 'Person|OuterRef'):
objects = Booking.objects.filter(user=for_user)
return objects.order_by('-begin_time').values('begin_time')[:1]
@staticmethod
def currently_open_checkin(for_user: 'Person|OuterRef') -> 'Booking|None':
return Booking.objects.filter(
Q(user=for_user),
Q(begin_time__lte=timezone.now()),
Q(end_time=None),
Q(type__is_checkin=True)
).first()

36
app/base/models/booking_type.py Executable file
View File

@@ -0,0 +1,36 @@
from django.db import models
from app.base.forms.fields import CurrencyField
import math
from decimal import Decimal
from typing import Iterable
class BookingType(models.Model):
key = models.CharField('UUID', primary_key=True, max_length=20)
label = models.CharField('Bezeichnung', max_length=200)
price = CurrencyField('Preis (€)')
interval = models.IntegerField('Intervall (Min)', default=60)
is_checkin = models.BooleanField('Ist Eincheck-Option', default=False)
class Meta:
verbose_name = 'Buchungsart'
verbose_name_plural = 'Buchungsarten'
def __str__(self):
return self.label
# return f'{self.label} ({self.price}/{self.interval})'
def price_with_traits(self, duration: int, traits: Iterable[str]) \
-> Decimal:
# TODO: make this UI-configurable?
# people with "Abo" status
if 'abo' in traits or 'bckspc' in traits:
return Decimal(0)
# Members do not pay for basic subscription
if self.key == 'basic' and 'member' in traits:
return Decimal(0)
# TODO: if needed add more rules here
intervals_to_pay = math.ceil(duration / self.interval)
return Decimal(intervals_to_pay * self.price)

22
app/base/models/course.py Executable file
View File

@@ -0,0 +1,22 @@
from django.db import models
from django.urls import reverse
from app.base.forms.fields import TextField
from app.base.models.course_visit import CourseVisit
class Course(models.Model):
instructed: models.QuerySet[CourseVisit]
title = models.CharField('Titel', max_length=280)
mandatory = models.BooleanField('Braucht jeder?', default=False)
description = TextField('Beschreibung', blank=True)
class Meta:
verbose_name = 'Einweisung'
verbose_name_plural = 'Einweisungen'
def get_absolute_url(self):
return reverse('course:detail', kwargs={'pk': self.pk})
def __str__(self):
return self.title

35
app/base/models/course_visit.py Executable file
View File

@@ -0,0 +1,35 @@
from django.db import models
from django.urls import reverse
from app.base.forms.fields import DateField
from datetime import date
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.base.models.course import Course
from app.base.models.person import Person
class CourseVisit(models.Model):
course: 'models.ForeignKey[Course]' = models.ForeignKey(
'Course', on_delete=models.CASCADE,
related_name='visits', verbose_name='Einweisung')
participant: 'models.ForeignKey[Person]' = models.ForeignKey(
'Person', on_delete=models.CASCADE,
related_name='courses', verbose_name='Wer wurde eingewiesen?')
teacher: 'models.ForeignKey[Person]|models.ForeignKey[None]' =\
models.ForeignKey(
'Person', on_delete=models.SET_NULL, blank=True, null=True,
related_name='instructed', verbose_name='Durchgeführt von')
date = DateField('Datum', default=date.today)
class Meta:
verbose_name = 'Teilnahme'
verbose_name_plural = 'Teilnahmen'
def __str__(self):
return 'Teilnahme von {} an Einweisung „{}“ am {}'.format(
self.participant, self.course, self.date)
def get_absolute_url(self):
return reverse('course-visit:detail', kwargs={'pk': self.pk})

15
app/base/models/note.py Executable file
View File

@@ -0,0 +1,15 @@
from django.db import models
from app.base.forms.fields import TextField
class Note(models.Model):
user = models.OneToOneField('Person', on_delete=models.CASCADE)
text = TextField('Notiz', blank=True)
class Meta:
verbose_name = 'Notiz'
verbose_name_plural = 'Notizen'
def __str__(self):
return self.text

84
app/base/models/person.py Executable file
View File

@@ -0,0 +1,84 @@
from django.db import models
from django.db.models import Q
from django.urls import reverse
from app.base.forms.fields import DateField
from app.base.models.course import Course
from app.base.models.booking import Booking
from datetime import datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.base.models import Account, Note, CourseVisit, TraitMapping
class Person(models.Model):
uuid = models.CharField('Karten-ID', max_length=200, blank=True)
first_name = models.CharField('Vorname', max_length=200)
last_name = models.CharField('Nachname', max_length=200)
email = models.EmailField('Email', blank=True, null=True)
phone = models.CharField('Telefon', max_length=200, blank=True, null=True)
birth_date = DateField('Geburtsdatum')
zip_code = models.CharField('PLZ', max_length=10)
city = models.CharField('Stadt', max_length=200)
street = models.CharField('Straße', max_length=200)
house_nr = models.CharField('Hausnummer', max_length=10)
identified = models.BooleanField('Ausweis vorgezeigt', default=False)
agreed_to_terms_of_service = models.BooleanField(
'Nutzungsbedingungen zugestimmt', default=False)
# related_name
account: 'models.OneToOneField[Account]'
note: 'models.OneToOneField[Note]'
courses: 'models.QuerySet[CourseVisit]'
instructed: 'models.QuerySet[CourseVisit]'
traits: 'models.QuerySet[TraitMapping]'
class Meta:
verbose_name = 'Werkstattnutzer:in'
verbose_name_plural = 'Werkstattnutzer:innen'
# ordering = ('first_name', 'last_name')
# indexes = [
# models.Index(fields=['first_name', 'last_name']),
# models.Index(Latest,fields=["first_name"],name="first_name_idx"),
# ]
def get_absolute_url(self):
return reverse('person:detail', kwargs={'pk': self.pk})
def __str__(self):
return f'{self.first_name} {self.last_name}'
@property
def display_name(self):
return f'{self.first_name} {self.last_name}'
@property
def address(self):
return f'{self.street} {self.house_nr}, {self.zip_code} {self.city}'
@property
def missing_courses(self):
mandatory_courses = Course.objects.filter(mandatory=True)
completed = set(self.courses.values_list('course', flat=True))
return [x for x in mandatory_courses if x.pk not in completed]
@property
def attributes(self):
return self.traits_at_date(datetime.now()).values_list(
'pk', 'trait__key', 'trait__label')
def traits_at_date(self, date: datetime):
return self.traits.filter(
Q(valid_from__lte=date),
Q(valid_until__gte=date) | Q(valid_until=None))
def last_check_in(self) -> 'datetime|None':
obj = Booking.latest_checkin_query(self).first()
return obj['begin_time'] if obj else None
@property
def current_checkin(self):
return Booking.currently_open_checkin(self)

16
app/base/models/trait.py Executable file
View File

@@ -0,0 +1,16 @@
from django.db import models
from app.base.forms.fields import TextField
class Trait(models.Model):
key = models.CharField('UUID', primary_key=True, max_length=20)
label = models.CharField('Label', max_length=200)
description = TextField('Beschreibung', blank=True)
class Meta:
verbose_name = 'Attribut'
verbose_name_plural = 'Attribute'
def __str__(self):
return self.label

View File

@@ -0,0 +1,30 @@
from django.db import models
from django.urls import reverse
from app.base.forms.fields import DateField
from datetime import date
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.base.models.person import Person
from app.base.models.trait import Trait
class TraitMapping(models.Model):
user: 'models.ForeignKey[Person]' = models.ForeignKey(
'Person', on_delete=models.CASCADE, related_name='traits',
verbose_name='Werkstattnutzer:in')
trait: 'models.ForeignKey[Trait]' = models.ForeignKey(
'Trait', on_delete=models.CASCADE, verbose_name='Attribut')
valid_from = DateField('Gültig von', default=date.today)
valid_until = DateField('Gültig bis', blank=True, null=True)
class Meta:
verbose_name = 'Attributzuweisung'
verbose_name_plural = 'Attributzuweisungen'
def __str__(self):
return f'Attribut „{self.trait}“ für {self.user}'
def get_absolute_url(self):
return reverse('trait-mapping:detail', kwargs={'pk': self.pk})

26
app/base/models/transaction.py Executable file
View File

@@ -0,0 +1,26 @@
from django.db import models
from django.urls import reverse
from app.base.forms.fields import CurrencyField, DateTimeField
from app.base.models.account import Account
class Transaction(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE,
verbose_name='Konto')
amount = CurrencyField('Betrag')
booking = models.OneToOneField('Booking', on_delete=models.CASCADE,
verbose_name='Zugehörige Zeitbuchung',
null=True, blank=True, default=None)
description = models.CharField('Beschreibung', max_length=500)
time_stamp = DateTimeField('Datum / Uhrzeit', auto_now_add=True)
class Meta:
verbose_name = 'Transaktion'
verbose_name_plural = 'Transaktionen'
def get_absolute_url(self):
return reverse('transaction:detail', kwargs={'pk': self.pk})
def __str__(self):
return f'Transaktion über {self.amount}€ von {self.account}'