-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmodels.py
More file actions
235 lines (198 loc) · 9.15 KB
/
Copy pathmodels.py
File metadata and controls
235 lines (198 loc) · 9.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
"""
Modely - reprezentují entity z databáze.
"""
from typing import Any
from django.db import models, transaction
class AttendanceState(models.Model):
"""
Stav účasti klienta na lekci.
Navázán ke klientovi a lekci prostřednictvím účasti (Attendance).
MAZÁNÍ: Stav účasti lze smazat pouze pokud není používán v žádné účasti (Attendance).
ŘAZENÍ: Vzestupně dle názvu stavu účasti.
"""
default = models.BooleanField(
default=False, help_text="Označuje stav (právě jeden), který je výchozí"
) # metoda save() osetruje, aby bylo jen jedno True v DB
excused = models.BooleanField(
default=False, help_text="Označuje stav (právě jeden), který znamená omluven"
) # metoda save() osetruje, aby bylo jen jedno True v DB
name = models.TextField(help_text="Název stavu účasti (unikátní)")
visible = models.BooleanField(default=True, help_text="Indikátor viditelnosti stavu")
class Meta:
ordering = ["name"]
def make_true_value_unique_for_attr(self, value: bool, attr: str) -> None:
"""
Zařídí unikátnost hodnoty True (v proměnné value) pro daný atribut attr.
Má na starost OMEZENÍ O12.
"""
if value:
# vyber ostatni polozky s attr=True a zamkni je (zabraneni race condition) -
# vola se uvnitr transaction.atomic() v save()
qs = AttendanceState.objects.select_for_update().filter(**{attr: True})
# krome self (pokud self existuje)
if self.pk:
qs = qs.exclude(pk=self.pk)
# a nastav jim attr=False
qs.update(**{attr: False})
def make_all_true_values_unique(self) -> None:
"""
OMEZENÍ O12: Zařídí unikátnost True pro všechny potřebné boolean atributy.
"""
self.make_true_value_unique_for_attr(self.default, "default")
self.make_true_value_unique_for_attr(self.excused, "excused")
def reset_attrs_when_set_non_visible(self) -> None:
"""
OMEZENÍ O12: Vyresetuje všechny potřebné boolean atributy když je účast neviditelná.
"""
if not self.visible:
self.default = self.excused = False
def save(self, *args: Any, **kwargs: Any) -> None:
# transaction.atomic() zajisti, ze update ostatnich zaznamu a save tohoto zaznamu
# probehou atomicky - zabranuje race condition pri soubenych requestech.
with transaction.atomic():
self.reset_attrs_when_set_non_visible()
self.make_all_true_values_unique()
super().save(*args, **kwargs)
class Client(models.Model):
"""
Klient lektorky.
Může být zájemcem o nějaký kurz (přes Application), být členem nějaké skupiny (přes Membership).
Může mít nějakou lekci (přes Attendance).
OMEZENÍ: Neaktivním klientům nelze přiřadit nové lekce.
MAZÁNÍ: Klienta lze smazat pouze pokud se neúčastní žádné lekce (prostřednictvím Attendance).
ŘAZENÍ: Vzestupně dle příjmení a křestního jména klienta.
"""
active = models.BooleanField(default=True, help_text="Indikátor aktivity klienta")
email = models.EmailField(blank=True, help_text="Email klienta") # OMEZENÍ O10 (EmailField)
firstname = models.TextField(help_text="Křestní jméno klienta")
note = models.TextField(blank=True, help_text="Poznámka ke klientovi")
phone = models.TextField(blank=True, help_text="Telefonní číslo klienta")
surname = models.TextField(help_text="Příjmení klienta")
class Meta:
ordering = ["surname", "firstname"]
class Course(models.Model):
"""
Kurz, na který mohou klienti v rámci lekcí docházet.
Patří ke každé lekci, má jej každá skupina a mohou k němu být (přes Application) přiřazováni zájemci.
OMEZENÍ: K neviditelnému kurzu nelze přiřadit skupina nebo zájemce.
MAZÁNÍ: Kurz lze smazat pouze pokud k němu neexistuje žádná lekce a není přiřazena žádná skupina.
ŘAZENÍ: Vzestupně dle názvu kurzu.
"""
color = models.CharField(max_length=7, default="#000000", help_text="Kód barvy kurzu")
duration = models.PositiveIntegerField(help_text="Trvání kurzu (min.)")
name = models.TextField(help_text="Název kurzu (unikátní)")
visible = models.BooleanField(default=True, help_text="Indikátor viditelnosti kurzu")
class Meta:
ordering = ["name"]
class Application(models.Model):
"""
Žádost reprezentující zájem klienta o kurz.
OMEZENÍ: Každý klient může mít zájem o daný kurz nejvýše jednou.
ŘAZENÍ: Vzestupně dle příjmení a křestního jména klienta.
"""
note = models.TextField(blank=True, help_text="Poznámka k zájemci o kurz")
client = models.ForeignKey(
Client,
related_name="applications",
on_delete=models.CASCADE,
help_text="Klient, který má zájem o kurz",
)
course = models.ForeignKey(
Course,
related_name="applications",
on_delete=models.CASCADE,
help_text="Kurz, o který má klient zájem",
)
created_at = models.DateField(auto_now_add=True, help_text="Datum vytvoření zájemce o kurz")
class Meta:
ordering = ["client__surname", "client__firstname"]
class Group(models.Model):
"""
Skupina klientů nějakého kurzu.
Patří k ní její lekce, členové (prostřednictvím Membership).
OMEZENÍ: Neaktivním skupinám nelze přiřadit nové lekce.
ŘAZENÍ: Vzestupně dle názvu skupiny.
"""
active = models.BooleanField(default=True, help_text="Indikátor aktivity skupiny")
name = models.TextField(help_text="Název skupiny (unikátní)")
course = models.ForeignKey(
Course, on_delete=models.PROTECT, help_text="Kurz skupiny"
) # OMEZENÍ O15
class Meta:
ordering = ["name"]
class Lecture(models.Model):
"""
Konkrétní lekce klienta či skupiny náležící nějakému kurzu.
Účastníci lekce jsou navázáni k lekci prostřednictvím svých účastí (Attendance).
V případě skupinové lekce se k lekci váže i příslušná skupina.
OMEZENÍ: Neskupinová lekce má jen jednoho účastníka.
OMEZENÍ: Kurz skupiny musí být tentýž jako kurz příslušné lekce.
OMEZENÍ: Nezrušené lekce nesmí být vzhledem ke startu a trvání v časovém konfliktu.
AUTOMATICKY: Pro lekce s alespoň 1 účastníkem, kde jsou všichni omluveni, nastaví lekci jako zrušenou.
"""
canceled = models.BooleanField(help_text="Indikátor zrušení lekce ze strany lektorky")
duration = models.PositiveIntegerField(help_text="Trvání lekce (min.)")
start = models.DateTimeField(
null=True, help_text="Datum a čas začátku lekce, prázdné pro předplacené lekce bez termínu"
)
course = models.ForeignKey(
Course, on_delete=models.PROTECT, help_text="Kurz, ke kterému patří lekce"
) # OMEZENÍ O15
group = models.ForeignKey(
Group,
related_name="lectures",
on_delete=models.CASCADE,
null=True,
help_text="Skupina, které se tato lekce týká (v případě skupinové lekce)",
)
class Attendance(models.Model):
"""
Reprezentace účasti klienta na nějaké lekci.
Váže se k nějaké lekci.
OMEZENÍ: U předplacených lekcí nelze platba měnit a musí být u všech účastníků zaplaceno.
ŘAZENÍ: Vzestupně dle příjmení a křestního jména klienta.
"""
note = models.TextField(blank=True, help_text="Poznámka k účasti")
paid = models.BooleanField(help_text="Indikátor zaplacené lekce")
client = models.ForeignKey(
Client,
related_name="attendances",
on_delete=models.PROTECT,
help_text="Klient, který se účastní dané lekce",
) # OMEZENÍ O13 (on_delete): tedy lze smazat pouze klienta co nema zadne attendances
lecture = models.ForeignKey(
Lecture,
related_name="attendances",
on_delete=models.CASCADE,
help_text="Lekce, ke které tato účast náleží",
)
attendancestate = models.ForeignKey(
AttendanceState,
on_delete=models.PROTECT,
help_text="Stav účasti daného klienta na dané lekci",
) # OMEZENÍ O14
class Meta:
ordering = ["client__surname", "client__firstname"]
class Membership(models.Model):
"""
Reprezentuje členství klienta ve skupině.
AUTOMATICKY: Provádí se korekce počtu předplacených lekcí (viz AttendanceSerializer, LectureSerializer).
ŘAZENÍ: Vzestupně dle příjmení a křestního jména klienta.
"""
prepaid_cnt = models.PositiveIntegerField(
default=0, help_text="Počet předplacených lekcí klienta v rámci dané skupiny"
)
client = models.ForeignKey(
Client,
related_name="memberships",
on_delete=models.CASCADE,
help_text="Klient, který je členem dané skupiny",
)
group = models.ForeignKey(
Group,
related_name="memberships",
on_delete=models.CASCADE,
help_text="Skupina, jejímž členem je daný klient",
)
class Meta:
ordering = ["client__surname", "client__firstname"]