package themis import ( "context" "errors" "fmt" "time" "github.com/rs/zerolog/log" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" "go.opentelemetry.io/otel/trace" ) func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string) error { ctx, span := tracer.Start(ctx, "add_absence", trace.WithAttributes( semconv.DBSystemSqlite, semconv.DBSQLTable("absences"), semconv.DBOperation("insert"), attribute.String("user_id", userId), attribute.String("session_date", session.Format(time.DateOnly)), )) defer span.End() if session.Weekday() != time.Monday { log.Debug().Ctx(ctx).Msg(fmt.Sprintf("%s is not a monday", session)) span.RecordError(errors.New(fmt.Sprintf("%s is not a monday", session))) return fmt.Errorf("not a monday") } defer s.Audit(ctx, &AuditableEvent{ userId: userId, eventType: EventAbsence, }) tx, err := s.db.Begin() if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to begin transaction") return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Commit() //nolint:errcheck stmt, err := s.db.PrepareContext(ctx, "INSERT INTO absences (session_date, userid) VALUES (?, ?)") if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to prepare absence query") return fmt.Errorf("failed to prepare absence query: %w", err) } _, err = stmt.ExecContext(ctx, session.Format(time.DateOnly), userId) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to insert absence") return fmt.Errorf("failed to insert absence: %w", err) } return nil } func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string, error) { ctx, span := tracer.Start(ctx, "get_absentees", trace.WithAttributes( semconv.DBSystemSqlite, semconv.DBSQLTable("absences"), semconv.DBOperation("select"), attribute.String("session_date", session.Format(time.DateOnly)), )) defer span.End() log.Debug().Ctx(ctx).Time("session", session).Msg("getting list of absentees") tx, err := s.db.Begin() if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to begin transaction") return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Commit() //nolint:errcheck stmt, err := s.db.PrepareContext(ctx, `SELECT userid FROM absences WHERE session_date = ?`) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to prepare query") return nil, fmt.Errorf("failed to prepare query: %w", err) } rows, err := stmt.QueryContext(ctx, session.Format(time.DateOnly)) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to execute query") return nil, fmt.Errorf("failed to execute query: %w", err) } absentees := make([]string, 0) for rows.Next() { var abs string err = rows.Scan(&abs) if err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } absentees = append(absentees, abs) } return absentees, nil } // map session_date -> list of absentees type Schedule map[string][]string func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule, error) { ctx, span := tracer.Start(ctx, "get_schedule", trace.WithAttributes( semconv.DBSystemSqlite, semconv.DBSQLTable("absences"), semconv.DBOperation("select"), attribute.String("from", from.Format(time.DateOnly)), attribute.String("to", to.Format(time.DateOnly)), )) defer span.End() log.Debug().Ctx(ctx).Time("from", from).Time("to", to).Msg("getting next sessions schedule") schedule := make(Schedule) initSchedule(schedule, from, to) tx, err := s.db.Begin() if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to begin transaction") return nil, fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Commit() //nolint:errcheck stmt, err := s.db.PrepareContext(ctx, `SELECT session_date, userid FROM absences WHERE session_date BETWEEN ? AND ? ORDER BY session_date ASC`) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to prepare query") return nil, fmt.Errorf("failed to prepare query: %w", err) } rows, err := stmt.QueryContext(ctx, from.Format(time.DateOnly), to.Format(time.DateOnly)) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to execute query") return nil, fmt.Errorf("failed to execute query: %w", err) } for rows.Next() { var date string var user string err = rows.Scan(&date, &user) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "failed to scan row") return nil, fmt.Errorf("failed to scan row: %w", err) } if _, ok := schedule[date]; ok { schedule[date] = append(schedule[date], user) } else { schedule[date] = []string{user} } } return schedule, nil } func initSchedule(schedule Schedule, from, to time.Time) { for from.Before(to) || from.Equal(to) { schedule[from.Format(time.DateOnly)] = []string{} from = from.AddDate(0, 0, 7) } }