From b74ab59b2cb9a624597c451345aec4461ec19c00 Mon Sep 17 00:00:00 2001 From: Oliver Braun Date: Wed, 25 Jun 2025 18:24:54 +0200 Subject: [PATCH] feat: emails published rooms and invigilations --- cmd/email.go | 12 ++ graph/generated/generated.go | 60 ++++++ graph/model/models_gen.go | 1 + graph/plan.graphqls | 1 + plexams/email.go | 4 + plexams/email_published.go | 190 +++++++++++++++++- plexams/plexams.go | 4 + plexams/tmpl/publishedEmailInvigilations.tmpl | 37 ++++ .../tmpl/publishedEmailInvigilationsHTML.tmpl | 40 ++++ plexams/tmpl/publishedEmailRooms.tmpl | 30 +++ plexams/tmpl/publishedEmailRoomsHTML.tmpl | 38 ++++ 11 files changed, 415 insertions(+), 2 deletions(-) create mode 100644 plexams/tmpl/publishedEmailInvigilations.tmpl create mode 100644 plexams/tmpl/publishedEmailInvigilationsHTML.tmpl create mode 100644 plexams/tmpl/publishedEmailRooms.tmpl create mode 100644 plexams/tmpl/publishedEmailRoomsHTML.tmpl diff --git a/cmd/email.go b/cmd/email.go index 9c20373..d5f5d88 100644 --- a/cmd/email.go +++ b/cmd/email.go @@ -20,7 +20,9 @@ constraints --- ask for constraints prepared --- announce exams to plan and constraints draft --- announce draft plan published-exams --- announce published exams +published-rooms --- announce published rooms invigilations --- send email requesting invigilations constraints +published-invigilations --- announce published invigilations nta-with-room-alone --- send emails to students with room alone before planning nta-planned --- send emails about rooms to all students with nta after planning `, @@ -63,6 +65,16 @@ nta-planned --- send emails about rooms to all students with nta after plann if err != nil { log.Fatalf("got error: %v\n", err) } + case "published-rooms": + err := plexams.SendEmailPublishedRooms(context.Background(), run) + if err != nil { + log.Fatalf("got error: %v\n", err) + } + case "published-invigilations": + err := plexams.SendEmailPublishedInvigilations(context.Background(), run) + if err != nil { + log.Fatalf("got error: %v\n", err) + } case "invigilations": err := plexams.SendEmailInvigilations(context.Background(), run) if err != nil { diff --git a/graph/generated/generated.go b/graph/generated/generated.go index 75504f0..e2a944e 100644 --- a/graph/generated/generated.go +++ b/graph/generated/generated.go @@ -113,6 +113,7 @@ type ComplexityRoot struct { Fs func(childComplexity int) int Lbas func(childComplexity int) int Profs func(childComplexity int) int + Sekr func(childComplexity int) int } EnhancedPrimussExam struct { @@ -1113,6 +1114,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Emails.Profs(childComplexity), true + case "Emails.sekr": + if e.complexity.Emails.Sekr == nil { + break + } + + return e.complexity.Emails.Sekr(childComplexity), true + case "EnhancedPrimussExam.conflicts": if e.complexity.EnhancedPrimussExam.Conflicts == nil { break @@ -4786,6 +4794,7 @@ type Emails { profs: String! lbas: String! fs: String! + sekr: String! } type SemesterConfig { @@ -9151,6 +9160,50 @@ func (ec *executionContext) fieldContext_Emails_fs(_ context.Context, field grap return fc, nil } +func (ec *executionContext) _Emails_sekr(ctx context.Context, field graphql.CollectedField, obj *model.Emails) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Emails_sekr(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Sekr, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Emails_sekr(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Emails", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _EnhancedPrimussExam_exam(ctx context.Context, field graphql.CollectedField, obj *model.EnhancedPrimussExam) (ret graphql.Marshaler) { fc, err := ec.fieldContext_EnhancedPrimussExam_exam(ctx, field) if err != nil { @@ -26986,6 +27039,8 @@ func (ec *executionContext) fieldContext_SemesterConfig_emails(_ context.Context return ec.fieldContext_Emails_lbas(ctx, field) case "fs": return ec.fieldContext_Emails_fs(ctx, field) + case "sekr": + return ec.fieldContext_Emails_sekr(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Emails", field.Name) }, @@ -33165,6 +33220,11 @@ func (ec *executionContext) _Emails(ctx context.Context, sel ast.SelectionSet, o if out.Values[i] == graphql.Null { out.Invalids++ } + case "sekr": + out.Values[i] = ec._Emails_sekr(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/graph/model/models_gen.go b/graph/model/models_gen.go index c70b3a8..da575a9 100644 --- a/graph/model/models_gen.go +++ b/graph/model/models_gen.go @@ -81,6 +81,7 @@ type Emails struct { Profs string `json:"profs"` Lbas string `json:"lbas"` Fs string `json:"fs"` + Sekr string `json:"sekr"` } type EnhancedPrimussExam struct { diff --git a/graph/plan.graphqls b/graph/plan.graphqls index 9d9ad33..3de5c84 100644 --- a/graph/plan.graphqls +++ b/graph/plan.graphqls @@ -21,6 +21,7 @@ type Emails { profs: String! lbas: String! fs: String! + sekr: String! } type SemesterConfig { diff --git a/plexams/email.go b/plexams/email.go index ae55b60..31e9ec4 100644 --- a/plexams/email.go +++ b/plexams/email.go @@ -35,6 +35,10 @@ import ( //go:embed tmpl/preparedEmailHTML.tmpl //go:embed tmpl/publishedEmailExams.tmpl //go:embed tmpl/publishedEmailExamsHTML.tmpl +//go:embed tmpl/publishedEmailRooms.tmpl +//go:embed tmpl/publishedEmailRoomsHTML.tmpl +//go:embed tmpl/publishedEmailInvigilations.tmpl +//go:embed tmpl/publishedEmailInvigilationsHTML.tmpl //go:embed tmpl/invigilationEmail.tmpl //go:embed tmpl/invigilationEmailHTML.tmpl var emailTemplates embed.FS diff --git a/plexams/email_published.go b/plexams/email_published.go index 1b83bb1..fa5044f 100644 --- a/plexams/email_published.go +++ b/plexams/email_published.go @@ -43,7 +43,7 @@ func (p *Plexams) SendEmailPublishedExams(ctx context.Context, run bool) error { FeedbackDate: feedbackDate, } - tmpl, err := template.ParseFS(emailTemplates, "tmpl/publishedEmail.tmpl") + tmpl, err := template.ParseFS(emailTemplates, "tmpl/publishedEmailExams.tmpl") if err != nil { return err } @@ -53,7 +53,7 @@ func (p *Plexams) SendEmailPublishedExams(ctx context.Context, run bool) error { return err } - tmpl, err = template.ParseFS(emailTemplates, "tmpl/publishedEmailHTML.tmpl") + tmpl, err = template.ParseFS(emailTemplates, "tmpl/publishedEmailExamsHTML.tmpl") if err != nil { return err } @@ -88,3 +88,189 @@ func (p *Plexams) SendEmailPublishedExams(ctx context.Context, run bool) error { true, ) } + +func (p *Plexams) SendEmailPublishedRooms(ctx context.Context, run bool) error { + cfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[69], + Suffix: aurora.Sprintf(aurora.Cyan(" sending email announcing published rooms")), + SuffixAutoColon: true, + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + StopFailMessage: "error happend", + StopFailCharacter: "✗", + StopFailColors: []string{"fgRed"}, + } + spinner, err := yacspin.New(cfg) + if err != nil { + log.Debug().Err(err).Msg("cannot create spinner") + } + err = spinner.Start() + if err != nil { + log.Debug().Err(err).Msg("cannot start spinner") + } + + feedbackDate := time.Now().Add(7 * 24 * time.Hour).Format("02.01.06") + + contraintsEmailData := &ConstraintsEmail{ + FromDate: p.semesterConfig.From.Format("02.01.06"), + FromFK07Date: p.semesterConfig.FromFk07.Format("02.01.06"), + UntilDate: p.semesterConfig.Until.Format("02.01.06"), + PlanerName: p.planer.Name, + FeedbackDate: feedbackDate, + } + + tmpl, err := template.ParseFS(emailTemplates, "tmpl/publishedEmailRooms.tmpl") + if err != nil { + return err + } + bufText := new(bytes.Buffer) + err = tmpl.Execute(bufText, contraintsEmailData) + if err != nil { + return err + } + + tmpl, err = template.ParseFS(emailTemplates, "tmpl/publishedEmailRoomsHTML.tmpl") + if err != nil { + return err + } + bufHTML := new(bytes.Buffer) + err = tmpl.Execute(bufHTML, contraintsEmailData) + if err != nil { + return err + } + + subject := fmt.Sprintf("[Prüfungsplanung %s] Räume veröffentlicht", + p.semester) + + err = spinner.Stop() + + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + + var to []string + if run { + to = []string{p.semesterConfig.Emails.Profs, p.semesterConfig.Emails.Lbas} + } else { + to = []string{"galority@gmail.com"} + } + + return p.sendMail(to, + nil, + subject, + bufText.Bytes(), + bufHTML.Bytes(), + nil, + true, + ) +} + +type InvigilationsEmail struct { + NoOfInvigilators int + InvigilationInRooms int + ReserveInvigilation int + OtherContributions int + TodoPerInvigilator int + MaxDeviation int + MinDeviation int + PlanerName string +} + +func (p *Plexams) SendEmailPublishedInvigilations(ctx context.Context, run bool) error { + cfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[69], + Suffix: aurora.Sprintf(aurora.Cyan(" sending email announcing published invigilations")), + SuffixAutoColon: true, + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + StopFailMessage: "error happend", + StopFailCharacter: "✗", + StopFailColors: []string{"fgRed"}, + } + spinner, err := yacspin.New(cfg) + if err != nil { + log.Debug().Err(err).Msg("cannot create spinner") + } + err = spinner.Start() + if err != nil { + log.Debug().Err(err).Msg("cannot start spinner") + } + + invigilationTodos, err := p.GetInvigilationTodos(ctx) + if err != nil { + return err + } + + maxDeviation, minDeviation := 0, 0 + + for _, invigilator := range invigilationTodos.Invigilators { + deviation := invigilator.Todos.TotalMinutes - invigilator.Todos.DoingMinutes + if deviation > 0 { + if deviation > maxDeviation { + maxDeviation = deviation + } + } else { + if deviation < minDeviation { + minDeviation = deviation + } + } + } + + contraintsEmailData := &InvigilationsEmail{ + PlanerName: p.planer.Name, + NoOfInvigilators: invigilationTodos.InvigilatorCount, + InvigilationInRooms: invigilationTodos.SumExamRooms, + ReserveInvigilation: invigilationTodos.SumReserve, + OtherContributions: invigilationTodos.SumOtherContributions, + TodoPerInvigilator: invigilationTodos.TodoPerInvigilatorOvertimeCutted, + MaxDeviation: maxDeviation, + MinDeviation: -minDeviation, + } + + tmpl, err := template.ParseFS(emailTemplates, "tmpl/publishedEmailInvigilations.tmpl") + if err != nil { + return err + } + bufText := new(bytes.Buffer) + err = tmpl.Execute(bufText, contraintsEmailData) + if err != nil { + return err + } + + tmpl, err = template.ParseFS(emailTemplates, "tmpl/publishedEmailInvigilationsHTML.tmpl") + if err != nil { + return err + } + bufHTML := new(bytes.Buffer) + err = tmpl.Execute(bufHTML, contraintsEmailData) + if err != nil { + return err + } + + subject := fmt.Sprintf("[Prüfungsplanung %s] Aufsichtenplanung im ZPA verfügbar", + p.semester) + + err = spinner.Stop() + + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + + var to []string + if run { + to = []string{p.semesterConfig.Emails.Profs, p.semesterConfig.Emails.Sekr} + } else { + to = []string{"galority@gmail.com"} + } + + return p.sendMail(to, + nil, + subject, + bufText.Bytes(), + bufHTML.Bytes(), + nil, + true, + ) +} diff --git a/plexams/plexams.go b/plexams/plexams.go index 2525e55..5569f6f 100644 --- a/plexams/plexams.go +++ b/plexams/plexams.go @@ -287,6 +287,10 @@ func (p *Plexams) setSemesterConfig() { if !ok { log.Error().Interface("emails", emailsMap).Msg("cannot get fs emails from config") } + emails.Sekr, ok = emailsMap["sekr"] + if !ok { + log.Error().Interface("emails", emailsMap).Msg("cannot get fs emails from config") + } p.semesterConfig = &model.SemesterConfig{ Days: days, diff --git a/plexams/tmpl/publishedEmailInvigilations.tmpl b/plexams/tmpl/publishedEmailInvigilations.tmpl new file mode 100644 index 0000000..bb2e15a --- /dev/null +++ b/plexams/tmpl/publishedEmailInvigilations.tmpl @@ -0,0 +1,37 @@ +[Antworten bitte nicht via E-Mail, sondern via JIRA (https://jira.cc.hm.edu/servicedesk/customer/portal/13)] + +Liebes Kollegium, + +ich habe gerade die Aufsichtenplanung abgeschlossen und ins ZPA gepusht. + +Unter https://zpa.cs.hm.edu/teacher/exam_plan/ sehen Sie nun alle Ihre Prüfungen und die Prüfungen bei denen Sie Aufsicht oder Reserveaufsicht sind. + +Sie sehen die Termine übrigens auch in Ihrem Wochenplan unter https://zpa.cs.hm.edu/teacher/week_plan/ und damit auch im iCal-Feed. + +Beachten Sie bitte als eingeteilte Aufsicht: +In Ihrem Raum können mehrere Prüfungen gleichzeitig stattfinden und die Studierenden können unterschiedliche Prüfungsdauern haben (z.B. Nachteilsausgleich). + +Beachten Sie bitte als Reserveaufsicht: Sie sind Reserve für alle Prüfungen die gleichzeitig stattfinden. Informieren Sie sich am Besten selbst ob alle eingeteilten Aufsichten da sind. + +@Sekretariat: Bitte ausdrucken und aushängen. Danke! + +Nachdem der Plan mit den Aufsichten im Raum mit den Postfächern ausgehängt wurde, repräsentiert der Aushang den gültigen Stand. + +Wenn Sie Aufsichten tauschen oder ein LBA selbst Aufsicht macht, tragen Sie das bitte in dem aushängenden Plan ein. + +Und wie immer ein bisschen Transparenz am Schluss: + +- {{ .NoOfInvigilators }} Aufsichten +- {{ .InvigilationInRooms }} Minuten Aufsichten in Räumen +- {{ .ReserveInvigilation }} Minuten Reserveaufsichten (eine Reserve rechnet mit 60 Minuten) +- {{ .OtherContributions }} Minuten anrechenbare Minuten durch Mastergespräche, Beisitzer, ... +- 100% zu leistende Aufsichten entsprechen {{ .TodoPerInvigilator }} Minuten, Teilzeit, halbes Freisemester, … entsprechend weniger +- Aufsicht bei eigener Prüfung rechnet nicht mit (genau wie eigene mündliche Prüfungen nicht anrechenbar sind) +- Die geleisteten Zeiten pro Person liegen zwischen {{ .MaxDeviation }} Minuten zu wenig und {{ .MinDeviation }} Minuten zu viel + +Mit freundlichen Grüßen +{{ .PlanerName }} +Prüfungsplaner der FK07 + +-- +Diese E-Mail wurde generiert und gesendet von https://github.com/obcode/plexams.go diff --git a/plexams/tmpl/publishedEmailInvigilationsHTML.tmpl b/plexams/tmpl/publishedEmailInvigilationsHTML.tmpl new file mode 100644 index 0000000..4c1f9a4 --- /dev/null +++ b/plexams/tmpl/publishedEmailInvigilationsHTML.tmpl @@ -0,0 +1,40 @@ +

[Antworten bitte nicht via E-Mail, +sondern via JIRA]

+ +

ich habe gerade die Aufsichtenplanung abgeschlossen und ins ZPA gepusht.

+

Unter https://zpa.cs.hm.edu/teacher/exam_plan/ +sehen Sie nun alle Ihre Prüfungen und die Prüfungen bei denen Sie Aufsicht oder Reserveaufsicht sind.

+

Sie sehen die Termine übrigens auch in Ihrem Wochenplan unter +https://zpa.cs.hm.edu/teacher/week_plan/ +und damit auch im iCal-Feed.

+

Beachten Sie bitte als eingeteilte Aufsicht: +In Ihrem Raum können mehrere Prüfungen gleichzeitig stattfinden und die Studierenden können + unterschiedliche Prüfungsdauern haben (z.B. Nachteilsausgleich).

+

Beachten Sie bitte als Reserveaufsicht: Sie sind Reserve für alle Prüfungen die gleichzeitig stattfinden. +Informieren Sie sich am Besten selbst ob alle eingeteilten Aufsichten da sind.

+

@Sekretariat: Bitte ausdrucken und aushängen. Danke!

+

Nachdem der Plan mit den Aufsichten im Raum mit den Postfächern ausgehängt wurde, +repräsentiert der Aushang den gültigen Stand.

+

Wenn Sie Aufsichten tauschen oder ein LBA selbst Aufsicht macht, tragen Sie das bitte in dem aushängenden Plan ein.

+ +

Und wie immer ein bisschen Transparenz am Schluss:

+ + + +

Mit freundlichen Grüßen

+

{{ .PlanerName }}
+Prüfungsplaner der FK07 +

+ +
+-- 
+Diese E-Mail wurde generiert und gesendet von https://github.com/obcode/plexams.go
+
\ No newline at end of file diff --git a/plexams/tmpl/publishedEmailRooms.tmpl b/plexams/tmpl/publishedEmailRooms.tmpl new file mode 100644 index 0000000..f308390 --- /dev/null +++ b/plexams/tmpl/publishedEmailRooms.tmpl @@ -0,0 +1,30 @@ +[Antworten bitte nicht via E-Mail, sondern via JIRA (https://jira.cc.hm.edu/servicedesk/customer/portal/13)] + +ich habe gerade die Räume ins ZPA gepusht. Bitte schauen Sie auf https://zpa.cs.hm.edu/teacher/exam_plan/ ob das so für Ihre Prüfungen passt. + +Ich habe versucht die Raumnutzung sehr zu optimieren, daher gibt es z.B. mehrere kleine (unter 10 Anmeldungen) Prüfungen in einem Raum oder ein Raum für eine Prüfung ist gleichzeitig Reserveraum für eine weitere Prüfung. + +Wenn das für Sie so absolut nicht in Ordnung ist, melden Sie sich bitte ASAP bei mir. + +Sie sehen nur was in den Räumen los ist, wenn Sie sich unter https://zpa.cs.hm.edu/teacher/exam_plan/ über die Auswahlbox z.B. alle Prüfungen an einem Tag anschauen. + +Anmerkungen zu den Räumen für Nachteilsausgleiche (markiert mit NTA) + +- Studierende mit Anspruch auf einen eigenen Raum habe ich i.d.R in R3.013 oder R3.014 eingeplant. +- Studierende ohne Anspruch auf einen eigenen Raum habe ich mit in die normalen Prüfungsräume eingeplant. + +ACHTUNG: Bitte setzen Sie Studierende mit einer Verlängerung von über 10% nicht einfach in einen anderen Raum als angegeben. Bei den angegebenen Räumen ist sichergestellt, dass keine Prüfung unmittelbar im Anschluss in dem gleichen Raum statt findet. Prüfungen beginnen immer um 08:30, 10:30, 12:30, 14:30 oder 16:30. Bei einem Nachteilsausgleich mit 20% würde der Raum z.B. von 08:30 bis 10:18 benötigt. Wenn dann um 10:30 die nächste Prüfung in dem Raum statt findet, wird das zu knapp. Bei 10% Verlängerung, also bei dem Beispiel 10:09, funktioniert das noch, wenn sich die Aufsicht darum kümmert, dass der Raum um 10:15 leer ist. + +Den roten Würfel und die blaue Tonne habe ich immer so geplant bzw. gebucht, dass er von 15 Minuten vor Prüfungsbeginn bis 15 Minuten nach dem Standard-Ende zur Verfügung steht, also bei einer 90-minütigen Prüfung um 08:30 von 08:15 bis 10:15. + +Reserveräume + +Die unter https://zpa.cs.hm.edu/teacher/exam_plan/ als Reserve markierten Räume werden den Studierenden unter https://zpa.cs.hm.edu/public/exam_plan/ nicht angezeigt. + +Mit freundlichen Grüßen + +{{ .PlanerName }} +Prüfungsplaner der FK07 + +-- +Diese E-Mail wurde generiert und gesendet von https://github.com/obcode/plexams.go diff --git a/plexams/tmpl/publishedEmailRoomsHTML.tmpl b/plexams/tmpl/publishedEmailRoomsHTML.tmpl new file mode 100644 index 0000000..91a896a --- /dev/null +++ b/plexams/tmpl/publishedEmailRoomsHTML.tmpl @@ -0,0 +1,38 @@ +

[Antworten bitte nicht via E-Mail, +sondern via JIRA]

+ +

ich habe gerade die Räume ins ZPA gepusht. Bitte schauen Sie auf https://zpa.cs.hm.edu/teacher/exam_plan/ +ob das so für Ihre Prüfungen passt.

+

Ich habe versucht die Raumnutzung sehr zu optimieren, daher gibt es z.B. mehrere kleine (unter 10 Anmeldungen) Prüfungen in einem Raum oder +ein Raum für eine Prüfung ist gleichzeitig Reserveraum für eine weitere Prüfung.

+

Wenn das für Sie so absolut nicht in Ordnung ist, melden Sie sich bitte ASAP bei mir.

+

Sie sehen nur was in den Räumen los ist, wenn Sie sich unter +https://zpa.cs.hm.edu/teacher/exam_plan/ + über die Auswahlbox z.B. alle Prüfungen an einem Tag anschauen.

+

Anmerkungen zu den Räumen für Nachteilsausgleiche (markiert mit NTA)

+ +

ACHTUNG: Bitte setzen Sie Studierende mit einer Verlängerung von über 10% nicht einfach in einen anderen Raum als angegeben. +Bei den angegebenen Räumen ist sichergestellt, dass keine Prüfung unmittelbar im Anschluss in dem gleichen Raum statt findet. +Prüfungen beginnen immer um 08:30, 10:30, 12:30, 14:30 oder 16:30. +Bei einem Nachteilsausgleich mit 20% würde der Raum z.B. von 08:30 bis 10:18 benötigt. +Wenn dann um 10:30 die nächste Prüfung in dem Raum statt findet, wird das zu knapp. +Bei 10% Verlängerung, also bei dem Beispiel 10:09, funktioniert das noch, wenn sich die Aufsicht darum kümmert, dass der Raum um 10:15 leer ist.

+

Den roten Würfel und die blaue Tonne habe ich immer so geplant bzw. gebucht, dass er von 15 Minuten vor Prüfungsbeginn bis 15 Minuten nach dem Standard-Ende zur Verfügung steht, +also bei einer 90-minütigen Prüfung um 08:30 von 08:15 bis 10:15.

+

Reserveräume

+

Die unter https://zpa.cs.hm.edu/teacher/exam_plan/ +als Reserve markierten Räume werden den Studierenden unter +https://zpa.cs.hm.edu/public/exam_plan/ nicht angezeigt.

+ +

Mit freundlichen Grüßen

+

{{ .PlanerName }}
+Prüfungsplaner der FK07 +

+ +
+-- 
+Diese E-Mail wurde generiert und gesendet von https://github.com/obcode/plexams.go
+
\ No newline at end of file