diff --git a/cmd/plan.go b/cmd/plan.go index 6a0c94f..9cfd4bb 100644 --- a/cmd/plan.go +++ b/cmd/plan.go @@ -6,6 +6,7 @@ import ( "log" "os" "strconv" + "time" "github.com/spf13/cobra" ) @@ -18,6 +19,7 @@ var ( pre-plan-exam ancode day slot --- move [ancode] to [day number] [slot number] pre-plan-room ancode roomname [mtknr/reserve] --- plan [room name] for [ancode] move-to ancode day slot --- move [ancode] to [day number] [slot number] + other-fk ancode time --- plan [ancode] from other faculty to [time] change-room ancode oldroom newroom --- change room for [ancode] from [oldroom] to [newroom] lock-exam ancode --- lock exam to slot unlock-exam ancode --- unlock / allow moving @@ -80,6 +82,33 @@ var ( log.Fatalf("got error: %v\n", err) } fmt.Println(str) + case "other-fk": + if len(args) < 3 { + log.Fatal("need ancode, time") + } + ancode, err := strconv.Atoi(args[1]) + if err != nil { + log.Fatalf("cannot convert %s to int", args[1]) + } + slottime, err := time.ParseInLocation("02.01.06,15:04", args[2], time.Local) + if err != nil { + log.Fatalf("cannot convert \"%s\" to time, must be in format \"02.01.06,15:04\"\n", args[2]) + } + + success, err := plexams.AddExamToSlottime(context.Background(), ancode, slottime) + + if err != nil { + fmt.Printf("error: %v\n", err) + os.Exit(1) + } + if success { + fmt.Printf("successfully moved exam %d to (%s)\n", ancode, slottime.String()) + } + str, err := plexams.ExamInfo(ancode) + if err != nil { + log.Fatalf("got error: %v\n", err) + } + fmt.Println(str) case "pre-plan-room": if len(args) < 3 { diff --git a/db/generatedExams.go b/db/generatedExams.go index fef1b8c..0e24951 100644 --- a/db/generatedExams.go +++ b/db/generatedExams.go @@ -79,7 +79,7 @@ func (db *DB) GetGeneratedExam(ctx context.Context, ancode int) (*model.Generate res := collection.FindOne(ctx, bson.D{{Key: "ancode", Value: ancode}}) if res.Err() != nil { - log.Error().Err(res.Err()).Int("ancode", ancode).Msg("cannot get generated exam") + log.Debug().Err(res.Err()).Int("ancode", ancode).Msg("cannot get generated exam") return nil, res.Err() } diff --git a/graph/generated/generated.go b/graph/generated/generated.go index b737d65..7a4a096 100644 --- a/graph/generated/generated.go +++ b/graph/generated/generated.go @@ -291,11 +291,12 @@ type ComplexityRoot struct { } PlanEntry struct { - Ancode func(childComplexity int) int - DayNumber func(childComplexity int) int - Locked func(childComplexity int) int - SlotNumber func(childComplexity int) int - Starttime func(childComplexity int) int + Ancode func(childComplexity int) int + DayNumber func(childComplexity int) int + ExternalTime func(childComplexity int) int + Locked func(childComplexity int) int + SlotNumber func(childComplexity int) int + Starttime func(childComplexity int) int } PlannedExam struct { @@ -1903,6 +1904,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.PlanEntry.DayNumber(childComplexity), true + case "PlanEntry.externalTime": + if e.complexity.PlanEntry.ExternalTime == nil { + break + } + + return e.complexity.PlanEntry.ExternalTime(childComplexity), true + case "PlanEntry.locked": if e.complexity.PlanEntry.Locked == nil { break @@ -4069,6 +4077,7 @@ type PlanEntry { dayNumber: Int! slotNumber: Int! starttime: Time! + externalTime: Time # only for exams from other faculties ancode: Int! locked: Boolean! } @@ -13860,6 +13869,47 @@ func (ec *executionContext) fieldContext_PlanEntry_starttime(_ context.Context, return fc, nil } +func (ec *executionContext) _PlanEntry_externalTime(ctx context.Context, field graphql.CollectedField, obj *model.PlanEntry) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_PlanEntry_externalTime(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.ExternalTime, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_PlanEntry_externalTime(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "PlanEntry", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _PlanEntry_ancode(ctx context.Context, field graphql.CollectedField, obj *model.PlanEntry) (ret graphql.Marshaler) { fc, err := ec.fieldContext_PlanEntry_ancode(ctx, field) if err != nil { @@ -14499,6 +14549,8 @@ func (ec *executionContext) fieldContext_PlannedExam_planEntry(_ context.Context return ec.fieldContext_PlanEntry_slotNumber(ctx, field) case "starttime": return ec.fieldContext_PlanEntry_starttime(ctx, field) + case "externalTime": + return ec.fieldContext_PlanEntry_externalTime(ctx, field) case "ancode": return ec.fieldContext_PlanEntry_ancode(ctx, field) case "locked": @@ -15251,6 +15303,8 @@ func (ec *executionContext) fieldContext_PreExam_planEntry(_ context.Context, fi return ec.fieldContext_PlanEntry_slotNumber(ctx, field) case "starttime": return ec.fieldContext_PlanEntry_starttime(ctx, field) + case "externalTime": + return ec.fieldContext_PlanEntry_externalTime(ctx, field) case "ancode": return ec.fieldContext_PlanEntry_ancode(ctx, field) case "locked": @@ -24530,6 +24584,8 @@ func (ec *executionContext) fieldContext_ZPAExamWithConstraints_planEntry(_ cont return ec.fieldContext_PlanEntry_slotNumber(ctx, field) case "starttime": return ec.fieldContext_PlanEntry_starttime(ctx, field) + case "externalTime": + return ec.fieldContext_PlanEntry_externalTime(ctx, field) case "ancode": return ec.fieldContext_PlanEntry_ancode(ctx, field) case "locked": @@ -29533,6 +29589,8 @@ func (ec *executionContext) _PlanEntry(ctx context.Context, sel ast.SelectionSet } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "externalTime": + out.Values[i] = ec._PlanEntry_externalTime(ctx, field, obj) case "ancode": out.Values[i] = ec._PlanEntry_ancode(ctx, field, obj) if out.Values[i] == graphql.Null { diff --git a/graph/model/plan.go b/graph/model/plan.go index 212f973..d7d3a6e 100644 --- a/graph/model/plan.go +++ b/graph/model/plan.go @@ -1,8 +1,11 @@ package model +import "time" + type PlanEntry struct { - DayNumber int `json:"dayNumber"` - SlotNumber int `json:"slotNumber"` - Ancode int `json:"ancode"` - Locked bool `json:"locked"` + DayNumber int `json:"dayNumber"` + SlotNumber int `json:"slotNumber"` + ExternalTime *time.Time `json:"externalTime"` + Ancode int `json:"ancode"` + Locked bool `json:"locked"` } diff --git a/graph/plan.graphqls b/graph/plan.graphqls index 8cab17a..7858875 100644 --- a/graph/plan.graphqls +++ b/graph/plan.graphqls @@ -57,6 +57,7 @@ type PlanEntry { dayNumber: Int! slotNumber: Int! starttime: Time! + externalTime: Time # only for exams from other faculties ancode: Int! locked: Boolean! } diff --git a/plexams/exam.go b/plexams/exam.go index 0b74eb7..39bc48d 100644 --- a/plexams/exam.go +++ b/plexams/exam.go @@ -243,19 +243,44 @@ func (p *Plexams) ConnectExam(ancode int, program string) error { func (p *Plexams) ExamInfo(ancode int) (string, error) { ctx := context.Background() + found := false exam, err := p.PlannedExam(ctx, ancode) + module, mainExamer := "", "" + var planEntry *model.PlanEntry if err != nil { - return "", err + exam, err := p.GetZpaExamByAncode(ctx, ancode) + if err != nil { + // TODO: maybe external exam? + } else { + found = true + module = exam.Module + mainExamer = exam.MainExamer + planEntry, err = p.dbClient.PlanEntry(ctx, ancode) + if err != nil { + log.Debug().Int("ancode", ancode). + Msg("not planned yet") + } + } + } else { + found = true + module = exam.ZpaExam.Module + mainExamer = exam.ZpaExam.MainExamer + planEntry = exam.PlanEntry } - if exam == nil { - return "", fmt.Errorf("no planned exam for ancode %d", ancode) + + if !found { + return fmt.Sprintf("no exam for ancode %d found", ancode), nil } + var sb strings.Builder - fmt.Fprintf(&sb, "%5d. %s (%s)", exam.Ancode, exam.ZpaExam.Module, exam.ZpaExam.MainExamer) - if exam.PlanEntry != nil { - starttime := p.getSlotTime(exam.PlanEntry.DayNumber, exam.PlanEntry.SlotNumber) - fmt.Fprintf(&sb, "\n Termin: %s (Tag %d / Slot %d)", starttime.Local().Format("02.01.06, 15:04 Uhr"), exam.PlanEntry.DayNumber, exam.PlanEntry.SlotNumber) + fmt.Fprintf(&sb, "%5d. %s (%s)", ancode, module, mainExamer) + if planEntry != nil { + starttime := p.getSlotTime(planEntry.DayNumber, planEntry.SlotNumber) + if planEntry.ExternalTime != nil { + starttime = *planEntry.ExternalTime + } + fmt.Fprintf(&sb, "\n Termin: %s (Tag %d / Slot %d)", starttime.Local().Format("02.01.06, 15:04 Uhr"), planEntry.DayNumber, planEntry.SlotNumber) } else { sb.WriteString("\n Termin: fehlt") } diff --git a/plexams/plan.go b/plexams/plan.go index ba40161..1a42fea 100644 --- a/plexams/plan.go +++ b/plexams/plan.go @@ -11,6 +11,48 @@ import ( "github.com/rs/zerolog/log" ) +func (p *Plexams) AddExamToSlottime(ctx context.Context, ancode int, time time.Time) (bool, error) { + exam, err := p.GetZpaExamByAncode(ctx, ancode) + duration := 90 // good default + if err != nil { + // TODO: maybe external exam? + } else { + // ZPA exam + constraints, err := p.ConstraintForAncode(ctx, ancode) + if err != nil { + log.Error().Err(err).Int("ancode", ancode). + Msg("error while trying to get constraints") + return false, err + } + if !constraints.NotPlannedByMe { + err := fmt.Errorf("add exam to slot time is only allowed for exams not planned by me") + return false, err + } + duration = exam.Duration + } + if exam == nil { + err = fmt.Errorf("exam with ancode %d not found", ancode) + return false, err + } + log.Debug().Str("module", exam.Module).Str("main-examer", exam.MainExamer). + Msg("found exam") + slot, err := p.getSlotForTime(time, duration) + if err != nil { + log.Error().Err(err).Time("slottime", time). + Msg("no slot for slottime found") + } + log.Debug().Int("day", slot.DayNumber).Int("slot", slot.SlotNumber). + Msg("found slot") + + return p.dbClient.AddExamToSlot(ctx, &model.PlanEntry{ + DayNumber: slot.DayNumber, + SlotNumber: slot.SlotNumber, + ExternalTime: &time, + Ancode: ancode, + Locked: false, + }) +} + func (p *Plexams) AddExamToSlot(ctx context.Context, ancode int, dayNumber int, timeNumber int, force bool) (bool, error) { var slot *model.Slot diff --git a/plexams/plannedExams.go b/plexams/plannedExams.go index 3802f45..cfc93f0 100644 --- a/plexams/plannedExams.go +++ b/plexams/plannedExams.go @@ -28,7 +28,7 @@ func (p *Plexams) PlanEntries(ctx context.Context) ([]*model.PlanEntry, error) { func (p *Plexams) PlannedExam(ctx context.Context, ancode int) (*model.PlannedExam, error) { exam, err := p.GeneratedExam(ctx, ancode) if err != nil { - log.Error().Err(err).Int("ancode", ancode).Msg("cannot get generated exam") + log.Debug().Err(err).Int("ancode", ancode).Msg("cannot get generated exam") return nil, err } diff --git a/plexams/plexams.go b/plexams/plexams.go index 9d3fe13..19689df 100644 --- a/plexams/plexams.go +++ b/plexams/plexams.go @@ -337,6 +337,39 @@ func (p *Plexams) getSlotTime(dayNumber, slotNumber int) time.Time { return time.Date(0, 0, 0, 0, 0, 0, 0, nil) } +func (p *Plexams) getSlotForTime(starttime time.Time, duration int) (*model.Slot, error) { + var slotWithStarttimeInSlot, slotWithEndtimeInSlot *model.Slot + endtime := starttime.Add(time.Duration(duration) * time.Minute) + for _, slot := range p.semesterConfig.Slots { + if starttime.After(slot.Starttime.Add(-1*time.Minute)) && + starttime.Before(slot.Starttime.Add(119*time.Minute)) { + slotWithStarttimeInSlot = slot + } + if endtime.After(slot.Starttime.Add(-1*time.Minute)) && + endtime.Before(slot.Starttime.Add(119*time.Minute)) { + slotWithEndtimeInSlot = slot + } + if slotWithStarttimeInSlot != nil && + slotWithEndtimeInSlot != nil { + break + } + } + + if slotWithStarttimeInSlot == nil { + return slotWithEndtimeInSlot, nil + } + if slotWithEndtimeInSlot == nil { + return slotWithStarttimeInSlot, nil + } + + minutesInEndtimeSlot := int(endtime.Sub(slotWithEndtimeInSlot.Starttime).Minutes()) + if minutesInEndtimeSlot > duration/2 { + return slotWithEndtimeInSlot, nil + } + + return slotWithStarttimeInSlot, nil +} + func (p *Plexams) PrintInfo() { fmt.Println(aurora.Sprintf(aurora.Magenta(" --- Planning Semester: %s --- \n"), p.semester)) }