• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

d120 / pyfeedback / 10204065558

01 Aug 2024 03:16PM UTC coverage: 89.224% (-0.8%) from 89.993%
10204065558

push

github

web-flow
Merge pull request #304 from d120/removing_old_features

Removing old features

10 of 10 new or added lines in 2 files covered. (100.0%)

16 existing lines in 2 files now uncovered.

2542 of 2849 relevant lines covered (89.22%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.44
/src/feedback/models/base.py
1
# coding=utf-8
2

3

4
import random
1✔
5

6
from django.db import models
1✔
7
from django.utils.translation import ugettext_lazy as _
1✔
8
from django.db.utils import OperationalError
1✔
9
from django.urls import reverse
1✔
10
from django.core.exceptions import ValidationError
1✔
11
from django.db.models import Q
1✔
12

13
from feedback.tools import ean_checksum_calc, ean_checksum_valid
1✔
14
from django.contrib.auth.models import User
1✔
15

16
from django.utils import formats
1✔
17

18
import datetime
1✔
19

20

21
class Semester(models.Model):
1✔
22
    """Repräsentiert ein Semester der TUD."""
23
    FRAGEBOGEN_CHOICES = (
1✔
24
        ('2008', 'Fragebogen 2008'),
25
        ('2009', 'Fragebogen 2009'),
26
        ('2012', 'Fragebogen 2012'),
27
        ('2016', 'Fragebogen 2016'),
28
        ('2020', 'Fragebogen 2020'),
29

30
    )
31
    SICHTBARKEIT_CHOICES = (
1✔
32
        ('ADM', 'Administratoren'),
33
        ('VER', 'Veranstalter'),
34
        ('ALL', 'alle (öffentlich)'),
35
    )
36

37
    semester = models.IntegerField(help_text='Aufbau: YYYYS, wobei YYYY = Jahreszahl und S = Semester (0=SS, 5=WS).',
1✔
38
                                   unique=True)
39
    fragebogen = models.CharField(max_length=5, choices=FRAGEBOGEN_CHOICES,
1✔
40
                                  help_text='Verwendete Version des Fragebogens.')
41
    sichtbarkeit = models.CharField(max_length=3, choices=SICHTBARKEIT_CHOICES,
1✔
42
                                    help_text='Sichtbarkeit der Evaluationsergebnisse.<br /><em>' +
43
                                              SICHTBARKEIT_CHOICES[0][1] +
44
                                              ':</em> nur für Mitglieder des Feedback-Teams<br /><em>' +
45
                                              SICHTBARKEIT_CHOICES[1][1] +
46
                                              ':</em> Veranstalter und Mitglieder des Feedback-Teams<br /><em>' +
47
                                              SICHTBARKEIT_CHOICES[2][1] +
48
                                              ':</em> alle (beschränkt auf das Uninetz)<br />'
49
                                    )
50
    vollerhebung = models.BooleanField(default=False)
1✔
51
    standard_ergebnisversand = models.DateField(null=True, blank=True, verbose_name='Ergebnisversand', help_text='Standarddatum für den Ergebnisversand')
1✔
52

53
    def _format_generic(self, ss, ws, space, modulus):
1✔
54
        sem = self.semester // 10
1✔
55
        if (modulus > 0):
1✔
56
            sem = sem % modulus
1✔
57

58
        if self.semester % 10 == 0:
1✔
59
            return '%s%s%d' % (ss, space, sem)
1✔
60
        else:
61
            return '%s%s%d/%d' % (ws, space, sem, sem + 1)
1✔
62

63
    def _format(self, ss, ws):
1✔
64
        return self._format_generic(ss, ws, ' ', 0)
1✔
65

66
    def short(self):
1✔
67
        return self._format('SS', 'WS')
1✔
68

69
    def long(self):
1✔
70
        return self._format('Sommersemester', 'Wintersemester')
1✔
71

72
    def evasys(self):
1✔
73
        return self._format_generic('SS', 'WS', '', 100)
1✔
74

75
    def last_Auswertungstermin(self):
1✔
76
        """Letzter Tag der als Auswertungstermin angegeben werden kann"""
77
        year = 0
1✔
78
        month = 0
1✔
79
        day = 15
1✔
80
        # Sommersemester
81
        if self.semester % 10 == 0:
1✔
82
            year = self.semester / 10
1✔
83
            month = 10
1✔
84
        # Wintersemester
85
        else:
86
            year = (self.semester / 10) + 1
×
87
            month = 4
×
88

89
        return datetime.datetime(int(year), int(month), int(day))
1✔
90

91
    def last_Auswertungstermin_to_late_human(self):
1✔
92
        """Der erste Tag der nach dem letzten Auswertungstermin liegt formatiert"""
93
        toLateDate = self.last_Auswertungstermin() + datetime.timedelta(days=1)
×
94
        return formats.date_format(toLateDate, 'DATE_FORMAT')
×
95

96
    def auswertungstermin_years(self):
1✔
97
        """Die Jahre in denen der Auswertungstermin liegen kann"""
98
        return self.last_Auswertungstermin().year,
×
99

100
    def __str__(self):
1✔
101
        return self.long()
1✔
102

103
    class Meta:
1✔
104
        verbose_name = 'Semester'
1✔
105
        verbose_name_plural = 'Semester'
1✔
106
        ordering = ['-semester']
1✔
107
        app_label = 'feedback'
1✔
108

109
    @staticmethod
1✔
110
    def current():
1✔
111
        try:
1✔
112
            return Semester.objects.order_by('-semester')[0]
1✔
113
        except IndexError:
1✔
114
            return None
×
115
        except OperationalError:
1✔
116
            return None
1✔
117

118

119
class Fachgebiet(models.Model):
1✔
120
    """Repräsentiert ein Fachgebiet für das FB20"""
121
    name = models.CharField(max_length=80)
1✔
122
    kuerzel = models.CharField(max_length=10)
1✔
123

124
    @staticmethod
1✔
125
    def get_fachgebiet_from_email(email):
1✔
126
        """
127
        Gibt ein Fachgebiet anhand einer E-Mail Adresse zurück
128
        :param email: E-Mail String
129
        :return: Fachgebiet
130
        """
131
        try:
1✔
132
            suffix = email.split('@')[-1]
1✔
133
            return EmailEndung.objects.get(domain=suffix).fachgebiet
1✔
134
        except Exception:
1✔
135
            return None
1✔
136

137
    def __str__(self):
1✔
138
        return self.name
1✔
139

140
    class Meta:
1✔
141
        verbose_name = 'Fachgebiet'
1✔
142
        verbose_name_plural = 'Fachgebiete'
1✔
143
        app_label = 'feedback'
1✔
144

145

146
class EmailEndung(models.Model):
1✔
147
    """Repräsentiert alle Domains die für E-Mails von Veranstaltern verwendet werden"""
148
    fachgebiet = models.ForeignKey(Fachgebiet,
1✔
149
                                   blank=True,
150
                                   help_text="Hier soll der Domainname einer Email-Adresse eines Fachgebiets stehen.",
151
                                   on_delete=models.CASCADE)
152
    domain = models.CharField(max_length=150,
1✔
153
                              null=True)
154

155
    def __str__(self):
1✔
156
        return self.domain
×
157

158
    class Meta:
1✔
159
        verbose_name = 'Fachgebiet Emailendung'
1✔
160
        verbose_name_plural = 'Fachgebiet Emailendungen'
1✔
161
        app_label = 'feedback'
1✔
162

163

164
class FachgebietEmail(models.Model):
1✔
165
    """Repräsentiert die E-Mail Domänen für die jeweiligen Fachgebiete des FBs 20."""
166
    fachgebiet = models.ForeignKey(Fachgebiet, related_name='fachgebiet', on_delete=models.CASCADE)
1✔
167
    email_sekretaerin = models.EmailField(blank=True)
1✔
168

169
    class Meta:
1✔
170
        verbose_name = 'Fachgebiet Email'
1✔
171
        verbose_name_plural = 'Fachgebiet Emails'
1✔
172
        app_label = 'feedback'
1✔
173

174

175
class Person(models.Model):
1✔
176
    """Repräsentiert eine Person der TUD aus dem FB20."""
177
    GESCHLECHT_CHOICES = (
1✔
178
        ('', ''),
179
        ('m', 'Herr'),
180
        ('w', 'Frau'),
181
    )
182

183
    GESCHLECHT_EVASYS_XML = {
1✔
184
        '': '',
185
        'm': 'm',
186
        'w': 'f',
187
    }
188

189
    geschlecht = models.CharField(max_length=1, choices=GESCHLECHT_CHOICES, blank=True, verbose_name='Anrede')
1✔
190
    vorname = models.CharField(_('first name'), max_length=30, blank=True)
1✔
191
    nachname = models.CharField(_('last name'), max_length=30, blank=True)
1✔
192
    email = models.EmailField(_('E-Mail'), blank=True)
1✔
193
    anschrift = models.CharField(_('anschrift'), max_length=80, blank=True,
1✔
194
                                 help_text='Tragen Sie bitte nur die Anschrift ohne Namen ein, '
195
                                           'da der Name automatisch hinzugefügt wird.')
196
    fachgebiet = models.ForeignKey(Fachgebiet, null=True, blank=True, on_delete=models.CASCADE)
1✔
197

198
    def full_name(self):
1✔
199
        return '%s %s' % (self.vorname, self.nachname)
1✔
200

201
    def get_evasys_key(self):
1✔
202
        return "pe-%s" % self.id
1✔
203

204
    def get_evasys_geschlecht(self):
1✔
205
        return Person.GESCHLECHT_EVASYS_XML[self.geschlecht]
1✔
206

207
    def __str__(self):
1✔
208
        return '%s, %s' % (self.nachname, self.vorname)
1✔
209

210
    def printable(self) -> bool:
1✔
211
        """ Check whether the user has all required fields for printing (e.g. for generating the stickers)
212
        """
213
        return self.vorname != "" and self.nachname != "" and self.email != "" and self.anschrift != ""
1✔
214

215
    class Meta:
1✔
216
        verbose_name = 'Person'
1✔
217
        verbose_name_plural = 'Personen'
1✔
218
        ordering = 'nachname', 'vorname'
1✔
219
        app_label = 'feedback'
1✔
220

221
    @staticmethod
1✔
222
    def create_from_import_person(ip):
1✔
223
        """
224
        Erstellt Personen aus dem Import
225
        :param ip: die importierte Person
226
        :return: Person
227
        """
228
        # Prüfen, ob Benutzer existiert
229
        try:
1✔
230
            return Person.objects.filter(vorname=ip.vorname, nachname=ip.nachname)[0]
1✔
231
        except (Person.DoesNotExist, IndexError):
1✔
232
            try:
1✔
233
                return AlternativVorname.objects.filter(vorname=ip.vorname).get().person
1✔
234
            except (AlternativVorname.DoesNotExist, IndexError):
1✔
235
                return Person.objects.create(vorname=ip.vorname, nachname=ip.nachname)
1✔
236

237
    @staticmethod
1✔
238
    def persons_to_edit(semester=None):
1✔
239
        """
240
        Gibt die Personen zurück, die noch bearbeitet werden müssen.
241
        :param semester: bei None, das aktuelle Semester, ansonsten das Angegebene.
242
        :return: Person
243
        """
244
        if semester is None:
1✔
245
            semester = Semester.current()
1✔
246
        return Person.objects.filter(Q(geschlecht='') | Q(email=''), veranstaltung__semester=semester)\
1✔
247
            .order_by('id').distinct()
248

249
    @staticmethod
1✔
250
    def all_edited_persons():
1✔
251
        """
252
        Gibt alle Personen zurück, die schon bearbeitet wurden.
253
        :return: Person
254
        """
255
        return Person.objects.filter(~Q(geschlecht='') & ~Q(email='')).order_by('id').distinct()
1✔
256

257
    @staticmethod
1✔
258
    def persons_with_similar_names(vorname, nachname):
1✔
259
        """
260
        Gibt alle Personen zurück, die sich im Namen ähneln.
261
        :param vorname: String
262
        :param nachname: String
263
        :return: Person
264
        """
265
        return Person.all_edited_persons().filter(vorname__startswith=vorname, nachname=nachname)
1✔
266

267
    @staticmethod
1✔
268
    def veranstaltungen(person):
1✔
269
        """
270
        Gibt die Veranstaltungen einer Person zurück.
271
        :param person: Person
272
        :return: Veranstaltung
273
        """
274
        return Veranstaltung.objects.filter(veranstalter=person)
1✔
275

276
    @staticmethod
1✔
277
    def replace_veranstalter(new, old):
1✔
278
        """
279
        Ersetzt einen "alten" Veranstalter durch einen "neuen".
280
        Zum Beispiel wenn Dozenten sich beim Namen ähneln und dementsprechend identisch sind.
281
        Wenn dies der Fall ist, wird ersetzt und ein AlternativVorname erzeugt, der sich die ähnlichen Namen merkt.
282
        :param new: die neue Person
283
        :param old: die alte Person
284
        """
285
        veranstaltungen = Person.veranstaltungen(new)
1✔
286

287
        # replace every lecture held by 'new' with 'old'
288
        for v in veranstaltungen:
1✔
289
            v.veranstalter.set([old])
1✔
290
            v.save()
1✔
291

292
        # save the second name from 'new' as the alternative first name of 'old'
293
        av = AlternativVorname.objects.create(vorname=new.vorname, person=old)
1✔
294
        av.save()
1✔
295

296
    @staticmethod
1✔
297
    def is_veranstalter(person):
1✔
298
        """
299
        prüft, ob eine Person ein Veranstalter ist.
300
        :param person: Person
301
        :return: True, wenn Veranstalter, ansonsten False
302
        """
303
        return Person.veranstaltungen(person).count() > 0
1✔
304

305

306
class AlternativVorname(models.Model):
1✔
307
    """Repräsentiert einen alternativen Vornamen für eine Person."""
308
    vorname = models.CharField(_('first name'), max_length=30, blank=True)
1✔
309
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
1✔
310

311

312
class Veranstaltung(models.Model):
1✔
313
    """Repräsentiert eine Veranstaltung der TUD."""
314
    TYP_CHOICES = (
1✔
315
        ('v', 'Vorlesung'),
316
        ('vu', 'Vorlesung mit Übung'),
317
        ('pr', 'Praktikum'),
318
        ('se', 'Seminar'),
319
    )
320

321
    SPRACHE_CHOICES = (
1✔
322
        ('de', 'Deutsch'),
323
        ('en', 'Englisch'),
324
    )
325
    # 0    undefiniert
326
    # 1    Vorlesung
327
    # 2    Seminar
328
    # 3    Proseminar
329
    # 4    Übung
330
    # 5    Praktikum
331
    # 6    Tutorium
332
    # 7    Sonstige
333
    # 8    Projekt
334
    # 9    Vorlesung + Übung
335
    # 10    Ringvorlesung
336
    # 11    Vorlesung+Praktikum
337
    VORLESUNGSTYP = {
1✔
338
        'v': 1,
339
        'vu': 9,
340
        'pr': 5,
341
        'se': 2,
342
    }
343

344
    VORLESUNGSTYP_XML = {
1✔
345
        'v': 'Vorlesung',
346
        'vu': 'Vorlesung + Übung',
347
        'pr': 'Praktikum',
348
        'se': 'Seminar',
349
    }
350

351
    # Bögen 2015
352

353
    # Deutsch
354
    # PD FB20Pv1 2677
355
    # SD FB20Sv2 2681
356
    # ÜD FB20Üv1 2675
357
    # VD FB20Vv1 2679
358

359
    # Englisch
360
    # PE FB20Pv1e 2704
361
    # SE FB20Sv1e 2702
362
    # ÜE FB20Üv1e 2700
363
    # VE FB20Vv1e 2698
364

365
    EVASYS_BOGENKENNUNG_DE = {
1✔
366
        'pr': 'FB20Prd1',
367
        'se': 'FB20Sed1',
368
        'u': 'FB20Ud1',
369
        'v': 'FB20VLd2',
370
        'vu': 'FB20VLd2',
371
    }
372

373
    EVASYS_BOGENKENNUNG_EN = {
1✔
374
        'pr': 'FB20Prd1',
375
        'se': 'FB20Sed1',
376
        'u': 'FB20Ud1',
377
        'v': 'FB20VLd2',
378
        'vu': 'FB20VLd2',
379
    }
380

381
    BARCODE_BASE = 2 * 10 ** 11
1✔
382

383
    # Vorlesungsstatus
384
    STATUS_ANGELEGT = 100
1✔
385
    STATUS_BESTELLUNG_GEOEFFNET = 200
1✔
386
    STATUS_KEINE_EVALUATION = 300
1✔
387
    STATUS_KEINE_EVALUATION_FINAL = 310
1✔
388
    STATUS_BESTELLUNG_LIEGT_VOR = 500
1✔
389
    STATUS_BESTELLUNG_WIRD_VERARBEITET = 510
1✔
390
    STATUS_GEDRUCKT = 600
1✔
391
    STATUS_VERSANDT = 700
1✔
392
    STATUS_BOEGEN_EINGEGANGEN = 800
1✔
393
    STATUS_BOEGEN_GESCANNT = 900
1✔
394
    STATUS_ERGEBNISSE_VERSANDT = 1000
1✔
395

396
    STATUS_CHOICES = (
1✔
397
        (STATUS_ANGELEGT, 'Angelegt'),
398
        (STATUS_BESTELLUNG_GEOEFFNET, 'Bestellung geöffnet'),
399
        (STATUS_KEINE_EVALUATION, 'Keine Evaluation'),
400
        (STATUS_KEINE_EVALUATION_FINAL, 'Keine Evaluation final'),
401
        (STATUS_BESTELLUNG_LIEGT_VOR, 'Bestellung liegt vor'),
402
        (STATUS_BESTELLUNG_WIRD_VERARBEITET, 'Bestellung wird verarbeitet'),
403
        (STATUS_GEDRUCKT, 'Gedruckt'),
404
        (STATUS_VERSANDT, 'Versandt'),
405
        (STATUS_BOEGEN_EINGEGANGEN, 'Bögen eingegangen'),
406
        (STATUS_BOEGEN_GESCANNT, 'Bögen gescannt'),
407
        (STATUS_ERGEBNISSE_VERSANDT, 'Ergebnisse versandt'),
408
    )
409

410
    BOOL_CHOICES = (
1✔
411
        (True, 'Ja'),
412
        (False, 'Nein'),
413
    )
414

415
    # TODO: not the final version of status transition
416
    STATUS_UEBERGANG = {
1✔
417
        STATUS_ANGELEGT: (STATUS_GEDRUCKT, STATUS_BESTELLUNG_GEOEFFNET),
418
        STATUS_BESTELLUNG_GEOEFFNET: (STATUS_KEINE_EVALUATION, STATUS_BESTELLUNG_LIEGT_VOR),
419
        STATUS_KEINE_EVALUATION: (STATUS_BESTELLUNG_LIEGT_VOR,),
420
        STATUS_BESTELLUNG_WIRD_VERARBEITET: (STATUS_GEDRUCKT,),
421
        STATUS_BESTELLUNG_LIEGT_VOR: (STATUS_GEDRUCKT, STATUS_BESTELLUNG_LIEGT_VOR, STATUS_KEINE_EVALUATION),
422
        STATUS_GEDRUCKT: (STATUS_VERSANDT,),
423
        STATUS_VERSANDT: (STATUS_BOEGEN_EINGEGANGEN,),
424
        STATUS_BOEGEN_EINGEGANGEN: (STATUS_BOEGEN_GESCANNT,),
425
        STATUS_BOEGEN_GESCANNT: (STATUS_ERGEBNISSE_VERSANDT,)
426
    }
427

428
    DIGITALE_EVAL = [
1✔
429
        ('T', 'TANs'),
430
        ('L', 'Losung'),
431
    ]
432

433
    MIN_BESTELLUNG_ANZAHL = 5
1✔
434

435
    # Helfertext für Dozenten für den Veranstaltungstyp.
436
    vlNoEx = 'Wenn Ihre Vorlesung keine Übung hat wählen Sie bitte <i>%s</i> aus'
1✔
437
    for cur in TYP_CHOICES:
1✔
438
        if cur[0] == 'v':
1✔
439
            vlNoEx = vlNoEx % cur[1]
1✔
440
            break
1✔
441

442
    typ = models.CharField(max_length=2, choices=TYP_CHOICES, help_text=vlNoEx)
1✔
443
    name = models.CharField(max_length=150)
1✔
444
    semester = models.ForeignKey(Semester, on_delete=models.CASCADE)
1✔
445
    lv_nr = models.CharField(max_length=15, blank=True, verbose_name='LV-Nummer')
1✔
446
    status = models.IntegerField(choices=STATUS_CHOICES, default=STATUS_ANGELEGT)
1✔
447
    grundstudium = models.BooleanField()
1✔
448
    evaluieren = models.BooleanField(choices=BOOL_CHOICES, default=True)
1✔
449
    veranstalter = models.ManyToManyField(Person, blank=True,
1✔
450
                                          help_text='Alle Personen, die mit der Veranstaltung befasst sind und z.B. Fragebögen bestellen können sollen.')
451

452
    sprache = models.CharField(max_length=2, choices=SPRACHE_CHOICES, null=True, blank=True)
1✔
453
    anzahl = models.IntegerField(null=True, blank=True)
1✔
454
    verantwortlich = models.ForeignKey(Person, related_name='verantwortlich', null=True, blank=True, on_delete=models.CASCADE,
1✔
455
                                       help_text='Diese Person wird von uns bei Rückfragen kontaktiert und bekommt die Fragenbögen zugeschickt')
456
    ergebnis_empfaenger = models.ManyToManyField(Person, blank=True,
1✔
457
                                                 related_name='ergebnis_empfaenger',
458
                                                 verbose_name='Empfänger der Ergebnisse',
459
                                                 help_text='An diese Personen werden die Ergebnisse per E-Mail geschickt.')
460
    primaerdozent = models.ForeignKey(Person, related_name='primaerdozent', null=True, blank=True, on_delete=models.CASCADE,
1✔
461
                                      help_text='Die Person, die im Anschreiben erwähnt wird')
462
    auswertungstermin = models.DateField(null=True, blank=True,
1✔
463
                                         verbose_name='Auswertungstermin',
464
                                         help_text='An welchem Tag sollen Fragebögen für diese Veranstaltung ausgewerter werden? ' +
465
                                                   'Fragebögen die danach eintreffen werden nicht mehr ausgewertet.')
466
    bestelldatum = models.DateField(null=True, blank=True)
1✔
467
    access_token = models.CharField(max_length=16, blank=True)
1✔
468
    freiefrage1 = models.TextField(verbose_name='1. Freie Frage', blank=True)
1✔
469
    freiefrage2 = models.TextField(verbose_name='2. Freie Frage', blank=True)
1✔
470
    kleingruppen = models.TextField(verbose_name='Kleingruppen', blank=True)
1✔
471
    veroeffentlichen = models.BooleanField(default=True, choices=BOOL_CHOICES)
1✔
472
    digitale_eval = models.BooleanField(default=True, verbose_name='Digitale Evaluation',
1✔
473
                                        help_text='Die Evaluation soll digital durchgeführt werden. Die Studierenden füllen die Evaluation online aus.', blank=True)
474
    digitale_eval_type = models.CharField(
1✔
475
        default='T',
476
        choices=DIGITALE_EVAL,
477
        max_length=1,
478
        verbose_name='Digitaler Evaluationstyp',
479
        help_text='Es werden generell zwei Typen von Verteilungsmethoden angeboten: Bei TANs erhalten Sie eine Excel Datei mit einer Liste aller TANs, welche Sie beispielsweise mithilfe von moodle verteilen können (eine Anleitung dazu wird bereitgestellt). Beim losungsbasierten Verfahren erhalten Sie einen einfachen, mehrfachbenutzbaren Link zum Onlinefragebogen.'
480
    )
481

482
    def get_next_state(self):
1✔
483
        """
484
        Gibt den nächsten Status einer Veranstaltung zurück.
485
        :return: Status einer Veranstaltung
486
        """
487
        try:
1✔
488
            return self.STATUS_UEBERGANG[self.status][0]  # TODO: Sobald es mehrere Zustande gibt
1✔
489
        except KeyError:
1✔
490
            return None
1✔
491

492
    def set_next_state(self):
1✔
493
        """Setzt den nächsten Status einer Veranstaltung."""
494
        status = self.STATUS_UEBERGANG[self.status]
1✔
495

496
        if self.status == self.STATUS_BESTELLUNG_GEOEFFNET:
1✔
497
            if self.evaluieren:
1✔
498
                self.status = status[1]
1✔
499
            else:
500
                self.status = status[0]
1✔
501

502
        elif self.status == self.STATUS_BESTELLUNG_LIEGT_VOR:
1✔
503
            if self.evaluieren:
1✔
504
                self.status = status[1]
1✔
505
            else:
506
                self.status = status[2]
1✔
507

508
        else:
509
            self.status = status[0]
1✔
510

511
    def get_evasys_typ(self):
1✔
512
        return Veranstaltung.VORLESUNGSTYP[self.typ]
1✔
513

514
    def get_evasys_typ_xml(self):
1✔
515
        return Veranstaltung.VORLESUNGSTYP_XML[self.typ]
1✔
516

517
    def get_evasys_key(self):
1✔
518
        return 'lv-%s' % self.id
1✔
519

520
    def get_evasys_kennung(self):
1✔
521
        return "%s-%s" % (self.lv_nr, self.semester.evasys())
1✔
522

523
    def get_evasys_survery_key(self):
1✔
524
        return 'su-%s' % self.id
1✔
525

526
    def get_evasys_survery_key_uebung(self):
1✔
527
        return 'su-%s-u' % self.id
1✔
528

529
    # FIXME: bogen name sollte nicht statisch sein!
530
    def get_evasys_bogen(self):
1✔
531
        if self.sprache == 'de':
1✔
532
            return Veranstaltung.EVASYS_BOGENKENNUNG_DE[self.typ]
1✔
533
        elif self.sprache == 'en':
1✔
534
            return Veranstaltung.EVASYS_BOGENKENNUNG_EN[self.typ]
1✔
535
        else:
536
            return ''
1✔
537

538
    def get_evasys_bogen_uebung(self):
1✔
539
        """Kennung für den Übungsfragebogen"""
540
        if self.typ == 'vu':
1✔
541
            if self.sprache == 'de':
1✔
542
                return Veranstaltung.EVASYS_BOGENKENNUNG_DE['u']
1✔
543
            elif self.sprache == 'en':
×
544
                return Veranstaltung.EVASYS_BOGENKENNUNG_EN['u']
×
545

546
        return ''
×
547

548
    def get_evasys_umfragetyp(self):
1✔
549
        """Deckblatt oder Selbstdruck verfahren"""
550
        result = 'coversheet'  # Deckblatt verfahren
1✔
551
        if self.typ in ('se', 'pr'):
1✔
552
            result = 'hardcopy'  # Selbstdruck verfahren
×
553
        if self.digitale_eval:
1✔
554
            if self.digitale_eval_type == 'L':
1✔
555
                result = 'codeword'
×
556
            else:
557
                result = 'online'
1✔
558
        return result
1✔
559

560
    def get_barcode_number(self, tutorgruppe=0):
1✔
561
        """Barcode Nummer für diese Veranstaltung"""
562
        if tutorgruppe > 99:
1✔
563
            raise ValueError("Tutorgruppe muss kleiner 100 sein")
×
564

565
        if isinstance(tutorgruppe, int) == False:
1✔
566
            raise ValueError("Tutorgruppe muss eine ganze Zahl sein")
×
567

568
        base = Veranstaltung.BARCODE_BASE
1✔
569
        veranst = self.pk
1✔
570
        code_draft = base + (veranst * 100) + tutorgruppe
1✔
571
        checksum = ean_checksum_calc(code_draft)
1✔
572

573
        code = (code_draft * 10) + checksum
1✔
574

575
        return code
1✔
576

577
    def get_evasys_list_veranstalter(self):
1✔
578
        personen = []
1✔
579
        if self.primaerdozent is not None:
1✔
580
            personen.append(self.primaerdozent)
1✔
581
        for per in self.ergebnis_empfaenger.all():
1✔
582
            if per.pk != self.primaerdozent.pk:
1✔
583
                personen.append(per)
1✔
584
        return personen
1✔
585

586
    @staticmethod
1✔
587
    def decode_barcode(barcode):
1✔
588
        if (ean_checksum_valid(barcode) != True):
1✔
589
            raise ValueError("Der Barcode ist nicht valide")
1✔
590

591
        # entferne das Padding am Anfang
592
        information = barcode % Veranstaltung.BARCODE_BASE
1✔
593

594
        # entferne die checksumme
595
        information = information // 10
1✔
596

597
        # die letzten zwei Stellen sind die Uebungsgruppe
598
        tutorgroup = information % 100
1✔
599

600
        # Alle Stellen vor der Uebungsgruppe sind der PK der Veranstaltung
601
        veranstaltung = information // 100
1✔
602

603
        return {'veranstaltung': veranstaltung, 'tutorgroup': tutorgroup}
1✔
604

605
    def __str__(self):
1✔
606
        return "%s [%s] (%s)" % (self.name, self.typ, self.semester.short())
1✔
607

608
    def create_log(self, user, scanner, interface):
1✔
609
        """
610
        Erstellt einen Log wenn sich bei einer Veranstaltung etwas geändert hat.
611
        :param user: Über welche Benutzer die Änderung erfolgt ist.
612
        :param scanner: Über welchen Barcodescanner die Änderung erfolgt ist.
613
        :param interface: Über welches Interface die Änderung erfolgt ist.
614
        """
615
        Log.objects.create(veranstaltung=self, user=user, scanner=scanner, status=self.status, interface=interface)
1✔
616

617
    def log(self, interface, is_frontend=False):
1✔
618
        """
619
        Die Logging-Funktion
620
        :param interface: Über welches Interface die Änderung erfolgt ist.
621
        :param is_frontend: Checkt, ob die Änderung über das Frontend erfolgt ist.
622
        """
623
        if isinstance(interface, BarcodeScanner):
1✔
624
            self.create_log(None, interface, Log.SCANNER)
1✔
625
        elif isinstance(interface, User):
1✔
626
            if is_frontend:
1✔
627
                self.create_log(interface, None, Log.FRONTEND)
1✔
628
            else:
629
                self.create_log(interface, None, Log.ADMIN)
1✔
630

631
    def auwertungstermin_to_late_msg(self):
1✔
632
        toLateDate = self.semester.last_Auswertungstermin_to_late_human()
×
633
        return 'Der Auswertungstermin muss vor dem %s liegen.' % toLateDate
×
634

635
    def has_uebung(self):
1✔
636
        """Gibt True zurück wenn die Veranstaltung eine Übung hat sonst False"""
637
        result = False
1✔
638
        if self.typ == 'vu':
1✔
639
            result = True
1✔
640
        return result
1✔
641

642
    def veranstalter_list(self):
1✔
643
        """Eine Liste aller Veranstalter dieser Veranstaltung"""
644
        list = [x.full_name() for x in self.veranstalter.all()]
×
645
        return ', '.join(list)
×
646
    
647
    
648
    def anzahl_too_few_msg(self) :
1✔
649
        return f'Anzahl der Bestellungen muss mindestens {self.MIN_BESTELLUNG_ANZAHL} sein. Bei weniger als {self.MIN_BESTELLUNG_ANZAHL} Teilnehmenden ist eine Evaluation leider nicht möglich'
1✔
650

651

652
    def clean(self, *args, **kwargs):
1✔
653
        super(Veranstaltung, self).clean(*args, **kwargs)
1✔
654

655
        if self.auswertungstermin is not None and self.id is not None and self.auswertungstermin > self.semester.last_Auswertungstermin().date():
1✔
656
            raise ValidationError(self.auwertungstermin_to_late_msg())
×
657
        
658
        if self.anzahl is not None and self.anzahl < self.MIN_BESTELLUNG_ANZAHL :
1✔
659
            raise ValidationError(self.anzahl_too_few_msg())
1✔
660

661

662
    def save(self, *args, **kwargs):
1✔
663
        # beim Speichern Zugangsschlüssel erzeugen, falls noch keiner existiert
664
        if not self.access_token:
1✔
665
            self.access_token = '%016x' % random.randint(0, 16 ** 16 - 1)
1✔
666
        super(Veranstaltung, self).save(*args, **kwargs)
1✔
667

668
    def link_veranstalter(self):  # @see http://stackoverflow.com/a/17948593
1✔
669
        """Gibt die URL für die Bestellunng durch den Veranstalter zurück"""
670
        link_veranstalter = 'https://www.fachschaft.informatik.tu-darmstadt.de%s' % reverse('veranstalter-login')
1✔
671
        link_suffix_format = '?vid=%d&token=%s'
1✔
672
        if self.pk is not None and self.access_token is not None:
1✔
673
            return link_veranstalter + (link_suffix_format % (self.pk, self.access_token))
1✔
674
        else:
675
            return "Der Veranstalter Link wird erst nach dem Anlegen angezeigt"
×
676

677
    def allow_order(self):
1✔
678
        """Überprüft anhand des Status' der Veranstaltung, ob bestellt werden darf."""
679
        return self.status == Veranstaltung.STATUS_BESTELLUNG_LIEGT_VOR or \
1✔
680
            self.status == Veranstaltung.STATUS_BESTELLUNG_GEOEFFNET or \
681
            self.status == Veranstaltung.STATUS_KEINE_EVALUATION
682

683
    def csv_to_tutor(self, csv_content):
1✔
684
        """Erzeuge Tutoren Objekte aus der CSV Eingabe der Veranstalter"""
UNCOV
685
        input_clean = csv_content.strip()
×
UNCOV
686
        input_lines = input_clean.splitlines()
×
UNCOV
687
        nummer = 1
×
688

UNCOV
689
        Tutor.objects.filter(veranstaltung=self).delete()
×
UNCOV
690
        for l in input_lines:
×
691
            # skip empty lines
UNCOV
692
            if len(l.strip()) > 1:
×
UNCOV
693
                row = l.split(',', 3)
×
694
                # skip lines which are not well formated
UNCOV
695
                if len(row) > 1:
×
UNCOV
696
                    row = [x.strip() for x in row]
×
UNCOV
697
                    anmerkung_input = ''
×
UNCOV
698
                    if len(row) > 3:
×
UNCOV
699
                        anmerkung_input = row[3]
×
700

UNCOV
701
                    Tutor.objects.create(
×
702
                        veranstaltung=self,
703
                        nummer=nummer,
704
                        nachname=row[0],
705
                        vorname=row[1],
706
                        email=row[2],
707
                        anmerkung=anmerkung_input
708
                    )
709

UNCOV
710
                    nummer += 1
×
711

712
    class Meta:
1✔
713
        verbose_name = 'Veranstaltung'
1✔
714
        verbose_name_plural = 'Veranstaltungen'
1✔
715
        ordering = ['semester', 'typ', 'name']
1✔
716
        unique_together = ('name', 'lv_nr', 'semester')
1✔
717
        app_label = 'feedback'
1✔
718

719

720
class Tutor(models.Model):
1✔
721
    """Repräsentiert Tutoren für eine Veranstaltung."""
722
    nummer = models.PositiveSmallIntegerField()
1✔
723
    vorname = models.CharField(_('first name'), max_length=30)
1✔
724
    nachname = models.CharField(_('last name'), max_length=30)
1✔
725
    email = models.EmailField(_('e-mail address'))
1✔
726
    anmerkung = models.CharField(max_length=100)
1✔
727
    veranstaltung = models.ForeignKey(Veranstaltung, on_delete=models.CASCADE)
1✔
728

729
    def get_barcode_number(self):
1✔
730
        """Gibt die Barcodenummer anhand der Tutorennummer zurück."""
731
        return self.veranstaltung.get_barcode_number(tutorgruppe=self.nummer)
×
732

733
    def __str__(self):
1✔
734
        return '%s %s %d' % (self.vorname, self.nachname, self.nummer)
×
735

736
    class Meta:
1✔
737
        verbose_name = 'Tutor'
1✔
738
        verbose_name_plural = 'Tutoren'
1✔
739
        unique_together = (('nummer', 'veranstaltung'),)
1✔
740
        app_label = 'feedback'
1✔
741

742

743
class Mailvorlage(models.Model):
1✔
744
    """Repräsentiert eine Mailvorlage"""
745
    subject = models.CharField(max_length=100, unique=True)
1✔
746
    body = models.TextField()
1✔
747

748
    def __str__(self):
1✔
749
        return self.subject
1✔
750

751
    class Meta:
1✔
752
        verbose_name = 'Mailvorlage'
1✔
753
        verbose_name_plural = verbose_name + 'n'
1✔
754
        ordering = ['subject']
1✔
755
        app_label = 'feedback'
1✔
756

757

758
class BarcodeScanner(models.Model):
1✔
759
    """Repräsentiert einen Barcodescanner."""
760
    token = models.CharField(max_length=64, unique=True)
1✔
761
    description = models.TextField()
1✔
762

763
    def __str__(self):
1✔
764
        return self.description
×
765

766
    class Meta:
1✔
767
        verbose_name = 'Barcode Scanner'
1✔
768
        verbose_name_plural = 'Barcode Scanner'
1✔
769
        app_label = 'feedback'
1✔
770

771

772
class BarcodeAllowedState(models.Model):
1✔
773
    """Repräsentiert die erlaubten Zustände für einen Barcodescanner."""
774
    barcode_scanner = models.ForeignKey(BarcodeScanner, on_delete=models.CASCADE)
1✔
775
    allow_state = models.IntegerField(choices=Veranstaltung.STATUS_CHOICES, null=True)
1✔
776

777
    class Meta:
1✔
778
        verbose_name = 'Erlaubter Zustand'
1✔
779
        verbose_name_plural = 'Erlaubte Zustände'
1✔
780
        unique_together = (('barcode_scanner', 'allow_state'),)
1✔
781
        app_label = 'feedback'
1✔
782

783

784
class BarcodeScannEvent(models.Model):
1✔
785
    """Repräsentiert einen Scan-Event für einen Barcodescanner"""
786
    veranstaltung = models.ForeignKey(Veranstaltung, on_delete=models.CASCADE)
1✔
787
    scanner = models.ForeignKey(BarcodeScanner, on_delete=models.CASCADE)
1✔
788
    tutorgroup = models.ForeignKey(Tutor, null=True, blank=True, on_delete=models.CASCADE)
1✔
789
    barcode = models.PositiveIntegerField()
1✔
790
    timestamp = models.DateTimeField(auto_now_add=True)
1✔
791

792
    class Meta:
1✔
793
        verbose_name = 'BarcodeScannEvent'
1✔
794
        verbose_name_plural = 'BarcodeScannEvents'
1✔
795
        app_label = 'feedback'
1✔
796

797
    def save(self, *args, **kwargs):
1✔
798
        """Extrahiere die Veranstaltungsdaten aus dem Barcode teil zwei zum ModelForm"""
799
        barcode_decode = Veranstaltung.decode_barcode(self.barcode)
×
800
        verst_obj = Veranstaltung.objects.get(pk=barcode_decode['veranstaltung'])
×
801
        self.veranstaltung = verst_obj
×
802
        self.veranstaltung.log(self.scanner)
×
803

804
        if barcode_decode['tutorgroup'] >= 1:
×
805
            tutorgroup = Tutor.objects.get(veranstaltung=verst_obj, nummer=barcode_decode['tutorgroup'])
×
806
            self.tutorgroup = tutorgroup
×
807

808
        super(BarcodeScannEvent, self).save(*args, **kwargs)
×
809

810

811
class Log(models.Model):
1✔
812
    """Repräsentiert einen Logger für die Zustandsübergänge von Veranstaltungen."""
813
    FRONTEND = 'fe'
1✔
814
    SCANNER = 'bs'
1✔
815
    ADMIN = 'ad'
1✔
816

817
    INTERFACE_CHOICES = (
1✔
818
        (FRONTEND, 'Frontend'),
819
        (SCANNER, 'Barcodescanner'),
820
        (ADMIN, 'Admin')
821
    )
822

823
    veranstaltung = models.ForeignKey(Veranstaltung, null=True, related_name='veranstaltung', on_delete=models.CASCADE)
1✔
824
    user = models.ForeignKey(User, null=True, related_name='user', on_delete=models.CASCADE)
1✔
825
    scanner = models.ForeignKey(BarcodeScanner, null=True, related_name='scanner', on_delete=models.CASCADE)
1✔
826
    timestamp = models.DateTimeField(auto_now_add=True)
1✔
827
    status = models.IntegerField(choices=Veranstaltung.STATUS_CHOICES, default=Veranstaltung.STATUS_ANGELEGT)
1✔
828
    interface = models.CharField(max_length=2, choices=INTERFACE_CHOICES)
1✔
829

830
    class Meta:
1✔
831
        verbose_name = 'Log'
1✔
832
        verbose_name_plural = 'Logs'
1✔
833
        app_label = 'feedback'
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc