- classes/review.go: review queue, detail and Confirm, shared by MCP and the admin screen; AnswerReviewed and AttemptReviewed after commit - /api/v1/quiz-reviews routes behind the quizzes permission - quizzes_review_list, quizzes_review_show, quizzes_review_confirm
176 lines
6.1 KiB
Go
176 lines
6.1 KiB
Go
package api
|
|
|
|
// The owner shapes of the review routes. They carry student emails, the
|
|
// model answer, the rubric and grading errors, and are never used by a
|
|
// student route; the student shapes live in json.go. Built field by field,
|
|
// so a new column never leaks.
|
|
|
|
import (
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
|
|
"git.golem15.com/golem15/sm-quizzes-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
)
|
|
|
|
type reviewQuizShape struct {
|
|
ID uint `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
type reviewQuestionShape struct {
|
|
ID uint `json:"id"`
|
|
Text string `json:"text"`
|
|
Points int `json:"points"`
|
|
}
|
|
|
|
type reviewDetailQuestionShape struct {
|
|
reviewQuestionShape
|
|
AnswerText *string `json:"answer_text"`
|
|
Rubric *string `json:"rubric"`
|
|
MaxLength int `json:"max_length"`
|
|
}
|
|
|
|
type reviewAttemptShape struct {
|
|
ID uint `json:"id"`
|
|
Completed bool `json:"completed"`
|
|
CompletedAt *wire.Time `json:"completed_at"`
|
|
}
|
|
|
|
type reviewStudentShape struct {
|
|
UserID uint `json:"user_id"`
|
|
Email string `json:"email"`
|
|
Name *string `json:"name"`
|
|
Surname *string `json:"surname"`
|
|
}
|
|
|
|
type reviewerShape struct {
|
|
UserID *uint `json:"user_id"`
|
|
AdminID *uint `json:"admin_id"`
|
|
}
|
|
|
|
// reviewRowShape is one row of GET /api/v1/quiz-reviews.
|
|
type reviewRowShape struct {
|
|
AnswerID uint `json:"answer_id"`
|
|
Status string `json:"status"`
|
|
Quiz reviewQuizShape `json:"quiz"`
|
|
Question reviewQuestionShape `json:"question"`
|
|
Attempt reviewAttemptShape `json:"attempt"`
|
|
Student reviewStudentShape `json:"student"`
|
|
Text string `json:"text"`
|
|
TextTruncated bool `json:"text_truncated"`
|
|
GraderPoints *float64 `json:"grader_points"`
|
|
Points float64 `json:"points"`
|
|
GradedAt *wire.Time `json:"graded_at"`
|
|
ReviewedAt *wire.Time `json:"reviewed_at"`
|
|
UpdatedAt wire.Time `json:"updated_at"`
|
|
}
|
|
|
|
// reviewDetailShape is GET /api/v1/quiz-reviews/{id} and every review
|
|
// write: the row keys with the whole text, plus the solution and the
|
|
// grading and review record.
|
|
type reviewDetailShape struct {
|
|
AnswerID uint `json:"answer_id"`
|
|
Status string `json:"status"`
|
|
Quiz reviewQuizShape `json:"quiz"`
|
|
Question reviewDetailQuestionShape `json:"question"`
|
|
Attempt reviewAttemptShape `json:"attempt"`
|
|
Student reviewStudentShape `json:"student"`
|
|
Text string `json:"text"`
|
|
GraderPoints *float64 `json:"grader_points"`
|
|
Points float64 `json:"points"`
|
|
GradedAt *wire.Time `json:"graded_at"`
|
|
ReviewedAt *wire.Time `json:"reviewed_at"`
|
|
UpdatedAt wire.Time `json:"updated_at"`
|
|
Grader *string `json:"grader"`
|
|
GraderFeedback *string `json:"grader_feedback"`
|
|
GradingError *string `json:"grading_error"`
|
|
GradingAttempts int `json:"grading_attempts"`
|
|
IsCorrect bool `json:"is_correct"`
|
|
ReviewComment *string `json:"review_comment"`
|
|
ReviewedBy reviewerShape `json:"reviewed_by"`
|
|
}
|
|
|
|
func wireTimePtr(t *time.Time) *wire.Time {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
w := wireTime(*t)
|
|
return &w
|
|
}
|
|
|
|
func reviewQuiz(r classes.ReviewRow) reviewQuizShape {
|
|
return reviewQuizShape{ID: r.QuizID, Slug: r.QuizSlug, Title: r.QuizTitle, Mode: quizMode(&models.Quiz{Mode: r.QuizMode})}
|
|
}
|
|
|
|
func reviewAttempt(r classes.ReviewRow) reviewAttemptShape {
|
|
return reviewAttemptShape{ID: r.AttemptID, Completed: r.AttemptCompletedAt != nil, CompletedAt: wireTimePtr(r.AttemptCompletedAt)}
|
|
}
|
|
|
|
func reviewStudent(r classes.ReviewRow) reviewStudentShape {
|
|
return reviewStudentShape{UserID: r.UserID, Email: r.Email, Name: r.Name, Surname: r.Surname}
|
|
}
|
|
|
|
// excerpt keeps the first n runes of s and reports whether it cut.
|
|
func excerpt(s string, n int) (string, bool) {
|
|
if utf8.RuneCountInString(s) <= n {
|
|
return s, false
|
|
}
|
|
return string([]rune(s)[:n]), true
|
|
}
|
|
|
|
// reviewRowJSON is one queue row; the text is cut to the first
|
|
// classes.ReviewExcerptRunes runes with text_truncated set.
|
|
func reviewRowJSON(r classes.ReviewRow) reviewRowShape {
|
|
text, cut := excerpt(r.Text, classes.ReviewExcerptRunes)
|
|
return reviewRowShape{
|
|
AnswerID: r.AnswerID,
|
|
Status: r.Status,
|
|
Quiz: reviewQuiz(r),
|
|
Question: reviewQuestionShape{ID: r.QuestionID, Text: r.QuestionText, Points: r.QuestionPoints},
|
|
Attempt: reviewAttempt(r),
|
|
Student: reviewStudent(r),
|
|
Text: text,
|
|
TextTruncated: cut,
|
|
GraderPoints: r.GraderPoints,
|
|
Points: r.Points,
|
|
GradedAt: wireTimePtr(r.GradedAt),
|
|
ReviewedAt: wireTimePtr(r.ReviewedAt),
|
|
UpdatedAt: wireTime(r.UpdatedAt),
|
|
}
|
|
}
|
|
|
|
// reviewDetailJSON is one answer for review, owner view.
|
|
func reviewDetailJSON(d classes.ReviewDetail) reviewDetailShape {
|
|
r := d.ReviewRow
|
|
return reviewDetailShape{
|
|
AnswerID: r.AnswerID,
|
|
Status: r.Status,
|
|
Quiz: reviewQuiz(r),
|
|
Question: reviewDetailQuestionShape{
|
|
reviewQuestionShape: reviewQuestionShape{ID: r.QuestionID, Text: r.QuestionText, Points: r.QuestionPoints},
|
|
AnswerText: d.AnswerText,
|
|
Rubric: d.Rubric,
|
|
MaxLength: d.MaxLength,
|
|
},
|
|
Attempt: reviewAttempt(r),
|
|
Student: reviewStudent(r),
|
|
Text: r.Text,
|
|
GraderPoints: r.GraderPoints,
|
|
Points: r.Points,
|
|
GradedAt: wireTimePtr(r.GradedAt),
|
|
ReviewedAt: wireTimePtr(r.ReviewedAt),
|
|
UpdatedAt: wireTime(r.UpdatedAt),
|
|
Grader: d.Grader,
|
|
GraderFeedback: d.GraderFeedback,
|
|
GradingError: d.GradingError,
|
|
GradingAttempts: d.GradingAttempts,
|
|
IsCorrect: d.IsCorrect,
|
|
ReviewComment: d.ReviewComment,
|
|
ReviewedBy: reviewerShape{UserID: d.ReviewedByUserID, AdminID: d.ReviewedByAdminID},
|
|
}
|
|
}
|