Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"github.com/gin-gonic/gin"
_ "github.com/jackc/pgx/v5/stdlib"

"CodeSCE/internal/core/repositories/postgres"
"CodeSCE/internal/core/services"
"CodeSCE/internal/db"
"CodeSCE/internal/handlers"
)
Expand All @@ -32,6 +34,24 @@ func main() {
log.Fatalf("failed to initialize schema: %v", err)
}

assessmentRepo := postgres.NewAssessmentRepository(database)
questionRepo := postgres.NewQuestionRepository(database)
testCaseRepo := postgres.NewTestCaseRepository(database)
inviteRepo := postgres.NewInviteRepository(database)
attemptRepo := postgres.NewAttemptRepository(database)
answerRepo := postgres.NewAnswerRepository(database)
submissionRepo := postgres.NewSubmissionRepository(database)

handlers.SetAssessmentService(services.NewAssessmentService(assessmentRepo, questionRepo, testCaseRepo))
handlers.SetInviteService(services.NewInviteService(inviteRepo, attemptRepo, assessmentRepo))
handlers.SetAttemptService(services.NewAttemptService(
attemptRepo,
answerRepo,
questionRepo,
submissionRepo,
assessmentRepo,
))

r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
Expand Down
6 changes: 6 additions & 0 deletions internal/core/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ type AssessmentAttempt struct {
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
}

// AttemptWithCandidateEmail is an attempt row with the invitee email (list/admin views).
type AttemptWithCandidateEmail struct {
AssessmentAttempt
CandidateEmail string `json:"candidateEmail"`
}

type Answer struct {
ID int64 `json:"id" db:"id"`
AttemptID int64 `json:"attemptId" db:"attempt_id"`
Expand Down
9 changes: 9 additions & 0 deletions internal/core/repositories/postgres/assessment_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,12 @@ func (r *assessmentRepository) GetByID(ctx context.Context, id int64) (*models.A
}
return &t, nil
}

func (r *assessmentRepository) CountQuestionsByAssessmentID(ctx context.Context, assessmentID int64) (int64, error) {
const query = `SELECT COUNT(*) FROM questions WHERE assessment_template_id = $1`
var n int64
if err := r.db.QueryRowContext(ctx, query, assessmentID).Scan(&n); err != nil {
return 0, fmt.Errorf("count questions by assessment: %w", err)
}
return n, nil
}
124 changes: 124 additions & 0 deletions internal/core/repositories/postgres/attempt_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,90 @@ func (r *attemptRepository) GetByID(ctx context.Context, id int64) (*models.Asse
return &attempt, nil
}

func (r *attemptRepository) GetByInviteID(ctx context.Context, inviteID int64) (*models.AssessmentAttempt, error) {
const query = `
SELECT id, invite_id, started_at, completed_at, status, total_score, created_at, updated_at
FROM assessment_attempts
WHERE invite_id = $1
LIMIT 1
`
var attempt models.AssessmentAttempt
err := r.db.QueryRowContext(ctx, query, inviteID).Scan(
&attempt.ID,
&attempt.InviteID,
&attempt.StartedAt,
&attempt.CompletedAt,
&attempt.Status,
&attempt.TotalScore,
&attempt.CreatedAt,
&attempt.UpdatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, repositories.ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get assessment attempt by invite id: %w", err)
}
return &attempt, nil
}

func (r *attemptRepository) GetAssessmentTemplateIDForAttempt(ctx context.Context, attemptID int64) (int64, error) {
const query = `
SELECT i.assessment_template_id
FROM assessment_attempts a
JOIN assessment_invites i ON i.id = a.invite_id
WHERE a.id = $1
`
var assessmentID int64
err := r.db.QueryRowContext(ctx, query, attemptID).Scan(&assessmentID)
if errors.Is(err, sql.ErrNoRows) {
return 0, repositories.ErrNotFound
}
if err != nil {
return 0, fmt.Errorf("get assessment template id for attempt: %w", err)
}
return assessmentID, nil
}

func (r *attemptRepository) FinalizeAttempt(ctx context.Context, attemptID int64, totalScore int, completedAt time.Time, attemptStatus string) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("finalize attempt begin tx: %w", err)
}
defer func() { _ = tx.Rollback() }()

res, err := tx.ExecContext(ctx, `
UPDATE assessment_attempts
SET total_score = $2, completed_at = $3, status = $4, updated_at = NOW()
WHERE id = $1 AND status = 'in_progress'
`, attemptID, totalScore, completedAt, attemptStatus)
if err != nil {
return fmt.Errorf("finalize attempt update attempt: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("finalize attempt rows affected: %w", err)
}
if n == 0 {
return repositories.ErrNotFound
}

_, err = tx.ExecContext(ctx, `
UPDATE assessment_invites i
SET status = 'completed', updated_at = NOW()
FROM assessment_attempts a
WHERE i.id = a.invite_id AND a.id = $1
`, attemptID)
if err != nil {
return fmt.Errorf("finalize attempt update invite: %w", err)
}

if err := tx.Commit(); err != nil {
return fmt.Errorf("finalize attempt commit: %w", err)
}
return nil
}

func (r *attemptRepository) ListByAssessmentID(ctx context.Context, assessmentID int64) ([]models.AssessmentAttempt, error) {
// Attempts link to assessments via assessment_invites.assessment_template_id.
const query = `
Expand Down Expand Up @@ -105,6 +189,46 @@ func (r *attemptRepository) ListByAssessmentID(ctx context.Context, assessmentID
return attempts, nil
}

func (r *attemptRepository) ListWithCandidateEmailByAssessmentID(ctx context.Context, assessmentID int64) ([]models.AttemptWithCandidateEmail, error) {
const query = `
SELECT a.id, a.invite_id, a.started_at, a.completed_at, a.status,
a.total_score, a.created_at, a.updated_at,
i.candidate_email
FROM assessment_attempts a
JOIN assessment_invites i ON i.id = a.invite_id
WHERE i.assessment_template_id = $1
ORDER BY a.id
`
rows, err := r.db.QueryContext(ctx, query, assessmentID)
if err != nil {
return nil, fmt.Errorf("list assessment attempts with email: %w", err)
}
defer rows.Close()

var out []models.AttemptWithCandidateEmail
for rows.Next() {
var row models.AttemptWithCandidateEmail
if err := rows.Scan(
&row.ID,
&row.InviteID,
&row.StartedAt,
&row.CompletedAt,
&row.Status,
&row.TotalScore,
&row.CreatedAt,
&row.UpdatedAt,
&row.CandidateEmail,
); err != nil {
return nil, fmt.Errorf("scan attempt with email: %w", err)
}
out = append(out, row)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate attempts with email: %w", err)
}
return out, nil
}

func (r *attemptRepository) UpdateStatus(ctx context.Context, id int64, status string) error {
const query = `
UPDATE assessment_attempts
Expand Down
43 changes: 43 additions & 0 deletions internal/core/repositories/postgres/submission_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,49 @@ func (r *submissionRepository) GetByID(ctx context.Context, id int64) (*models.S
return &s, nil
}

func (r *submissionRepository) ListByAttemptID(ctx context.Context, attemptID int64) ([]models.Submission, error) {
const query = `
SELECT id, attempt_id, question_id, language, source_code, status,
stdout, stderr, execution_ms, memory_bytes, score_awarded,
created_at, updated_at
FROM submissions
WHERE attempt_id = $1
ORDER BY question_id, id DESC
`
rows, err := r.db.QueryContext(ctx, query, attemptID)
if err != nil {
return nil, fmt.Errorf("list submissions by attempt: %w", err)
}
defer rows.Close()

var list []models.Submission
for rows.Next() {
var s models.Submission
if err := rows.Scan(
&s.ID,
&s.AttemptID,
&s.QuestionID,
&s.Language,
&s.SourceCode,
&s.Status,
&s.Stdout,
&s.Stderr,
&s.ExecutionMS,
&s.MemoryBytes,
&s.ScoreAwarded,
&s.CreatedAt,
&s.UpdatedAt,
); err != nil {
return nil, fmt.Errorf("scan submission: %w", err)
}
list = append(list, s)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate submissions: %w", err)
}
return list, nil
}

func (r *submissionRepository) UpdateStatus(ctx context.Context, id int64, status string) error {
const query = `
UPDATE submissions
Expand Down
8 changes: 8 additions & 0 deletions internal/core/repositories/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,22 @@ type AssessmentRepository interface {
Create(ctx context.Context, assessment *models.AssessmentTemplate) error
List(ctx context.Context) ([]models.AssessmentTemplate, error)
GetByID(ctx context.Context, id int64) (*models.AssessmentTemplate, error)
CountQuestionsByAssessmentID(ctx context.Context, assessmentID int64) (int64, error)
}

type AttemptRepository interface {
Create(ctx context.Context, attempt *models.AssessmentAttempt) error
GetByID(ctx context.Context, id int64) (*models.AssessmentAttempt, error)
GetByInviteID(ctx context.Context, inviteID int64) (*models.AssessmentAttempt, error)
GetAssessmentTemplateIDForAttempt(ctx context.Context, attemptID int64) (int64, error)
ListByAssessmentID(ctx context.Context, assessmentID int64) ([]models.AssessmentAttempt, error)
// ListWithCandidateEmailByAssessmentID joins attempts to invites for candidate email.
ListWithCandidateEmailByAssessmentID(ctx context.Context, assessmentID int64) ([]models.AttemptWithCandidateEmail, error)
UpdateStatus(ctx context.Context, id int64, status string) error
SetScore(ctx context.Context, id int64, score int) error
SetCompletedAt(ctx context.Context, id int64, completedAt time.Time) error
// FinalizeAttempt sets score, completed_at, attempt status (e.g. graded) and marks the linked invite completed. Requires attempt row status in_progress.
FinalizeAttempt(ctx context.Context, attemptID int64, totalScore int, completedAt time.Time, attemptStatus string) error
}

type InviteRepository interface {
Expand All @@ -46,6 +53,7 @@ type QuestionRepository interface {
type SubmissionRepository interface {
Create(ctx context.Context, submission *models.Submission) error
GetByID(ctx context.Context, id int64) (*models.Submission, error)
ListByAttemptID(ctx context.Context, attemptID int64) ([]models.Submission, error)
UpdateStatus(ctx context.Context, id int64, status string) error
UpdateResult(ctx context.Context, id int64, result *models.SubmissionResult) error
}
Expand Down
Loading