feat: quiz and rbac
This commit is contained in:
+19
-17
@@ -17,8 +17,10 @@ type AdminUser struct {
|
||||
Plan string `json:"plan"`
|
||||
EmailVerifiedAt *time.Time `json:"email_verified_at"`
|
||||
BannedAt *time.Time `json:"banned_at"`
|
||||
PostCount int `json:"post_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// AdminRole is the staff role key, nil for ordinary accounts.
|
||||
AdminRole *string `json:"admin_role,omitempty"`
|
||||
PostCount int `json:"post_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// Connected auth methods. HasPassword means email+password login is set up;
|
||||
// Providers lists linked OAuth providers (e.g. "google", "facebook").
|
||||
@@ -56,18 +58,18 @@ type AdminDisciple struct {
|
||||
|
||||
// AdminPost is a row in the admin posts table.
|
||||
type AdminPost struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Caption string `json:"caption"`
|
||||
SourceTitle string `json:"source_title"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Caption string `json:"caption"`
|
||||
SourceTitle string `json:"source_title"`
|
||||
SourceURL string `json:"source_url"`
|
||||
IsHidden bool `json:"is_hidden"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
AuthorName string `json:"author_name"`
|
||||
AuthorHandle string `json:"author_handle"`
|
||||
LikesCount int `json:"likes_count"`
|
||||
CommentsCount int `json:"comments_count"`
|
||||
AuthorName string `json:"author_name"`
|
||||
AuthorHandle string `json:"author_handle"`
|
||||
LikesCount int `json:"likes_count"`
|
||||
CommentsCount int `json:"comments_count"`
|
||||
}
|
||||
|
||||
// AdminCollection is a row in the admin collections table.
|
||||
@@ -85,18 +87,18 @@ type AdminCollection struct {
|
||||
|
||||
// AdminFeedback is a row in the admin feedback table (includes the reply).
|
||||
type AdminFeedback struct {
|
||||
ID string `json:"id"`
|
||||
Category string `json:"category"`
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
Category string `json:"category"`
|
||||
Message string `json:"message"`
|
||||
ContactEmail *string `json:"contact_email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
AcknowledgedAt *time.Time `json:"acknowledged_at"`
|
||||
ResolvedAt *time.Time `json:"resolved_at"`
|
||||
RepliedAt *time.Time `json:"replied_at"`
|
||||
ReplyMessage *string `json:"reply_message"`
|
||||
AuthorHandle *string `json:"author_handle"`
|
||||
AuthorName *string `json:"author_name"`
|
||||
AuthorEmail *string `json:"author_email"`
|
||||
ReplyMessage *string `json:"reply_message"`
|
||||
AuthorHandle *string `json:"author_handle"`
|
||||
AuthorName *string `json:"author_name"`
|
||||
AuthorEmail *string `json:"author_email"`
|
||||
}
|
||||
|
||||
// AdminReport is a unified projection over the three report tables
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Quiz target kinds.
|
||||
const (
|
||||
QuizChapter = "chapter"
|
||||
QuizLecture = "lecture"
|
||||
QuizConversation = "conversation"
|
||||
QuizStandalone = "standalone"
|
||||
)
|
||||
|
||||
// Attempt states, as seen by the person taking the quiz.
|
||||
const (
|
||||
QuizNotStarted = "not_started"
|
||||
QuizInProgress = "in_progress"
|
||||
QuizCompleted = "completed"
|
||||
)
|
||||
|
||||
// Quiz is a set of multiple-choice questions, optionally tied to a page.
|
||||
type Quiz struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
TargetKind string `json:"target_kind"`
|
||||
TargetPath *string `json:"target_path,omitempty"`
|
||||
TargetTitle string `json:"target_title"`
|
||||
PublishedAt *time.Time `json:"published_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
QuestionCount int `json:"question_count"`
|
||||
// Admin-only counters.
|
||||
AttemptCount int `json:"attempt_count,omitempty"`
|
||||
CompletedCount int `json:"completed_count,omitempty"`
|
||||
|
||||
Questions []QuizQuestion `json:"questions,omitempty"`
|
||||
}
|
||||
|
||||
// QuizQuestion is one question. Prompt and option bodies carry the same
|
||||
// formatting markers as the rest of the admin text.
|
||||
type QuizQuestion struct {
|
||||
ID string `json:"id"`
|
||||
Ord int `json:"ord"`
|
||||
Prompt string `json:"prompt"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
// Multi questions ask for every correct option ("select all that apply").
|
||||
Multi bool `json:"multi"`
|
||||
Options []QuizOption `json:"options"`
|
||||
}
|
||||
|
||||
// QuizOption is one choice. IsCorrect is nil whenever the reader must not
|
||||
// know it yet — every response to someone mid-attempt.
|
||||
type QuizOption struct {
|
||||
ID string `json:"id"`
|
||||
Ord int `json:"ord"`
|
||||
Body string `json:"body"`
|
||||
IsCorrect *bool `json:"is_correct,omitempty"`
|
||||
}
|
||||
|
||||
// QuizStatus is the caller's progress on a quiz.
|
||||
type QuizStatus struct {
|
||||
State string `json:"state"`
|
||||
Answered int `json:"answered"`
|
||||
Total int `json:"total"`
|
||||
Score *int `json:"score,omitempty"`
|
||||
}
|
||||
|
||||
// QuizCard is a quiz plus the caller's progress, for buttons and lists.
|
||||
type QuizCard struct {
|
||||
Quiz
|
||||
My QuizStatus `json:"my"`
|
||||
}
|
||||
|
||||
// QuizPlay is the next step of an attempt: the current question, or Done.
|
||||
type QuizPlay struct {
|
||||
QuizID string `json:"quiz_id"`
|
||||
Title string `json:"title"`
|
||||
Answered int `json:"answered"`
|
||||
Total int `json:"total"`
|
||||
Done bool `json:"done"`
|
||||
Question *QuizQuestion `json:"question,omitempty"`
|
||||
}
|
||||
|
||||
// QuizResultItem is one question on a results screen.
|
||||
type QuizResultItem struct {
|
||||
QuizQuestion
|
||||
ChosenOptionIDs []string `json:"chosen_option_ids"`
|
||||
IsCorrect bool `json:"is_correct"`
|
||||
ResponseMs *int `json:"response_ms,omitempty"`
|
||||
}
|
||||
|
||||
// QuizResult is a finished attempt.
|
||||
type QuizResult struct {
|
||||
Quiz Quiz `json:"quiz"`
|
||||
Score int `json:"score"`
|
||||
Total int `json:"total"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
TotalMs int `json:"total_ms"`
|
||||
Items []QuizResultItem `json:"items"`
|
||||
}
|
||||
|
||||
// QuizParticipant is one person's attempt, for the admin participants list.
|
||||
type QuizParticipant struct {
|
||||
UserID string `json:"user_id"`
|
||||
Handle string `json:"handle"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Answered int `json:"answered"`
|
||||
Score int `json:"score"`
|
||||
Total int `json:"total"`
|
||||
TotalMs int `json:"total_ms"`
|
||||
}
|
||||
|
||||
// QuizOptionStat is how often one option was chosen.
|
||||
type QuizOptionStat struct {
|
||||
QuizOption
|
||||
Picks int `json:"picks"`
|
||||
}
|
||||
|
||||
// QuizQuestionStat aggregates every response to one question.
|
||||
type QuizQuestionStat struct {
|
||||
ID string `json:"id"`
|
||||
Ord int `json:"ord"`
|
||||
Prompt string `json:"prompt"`
|
||||
Multi bool `json:"multi"`
|
||||
Answered int `json:"answered"`
|
||||
Correct int `json:"correct"`
|
||||
AvgMs int `json:"avg_ms"`
|
||||
MedianMs int `json:"median_ms"`
|
||||
Options []QuizOptionStat `json:"options"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// AdminRole is a named set of admin-panel permissions.
|
||||
type AdminRole struct {
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsSystem bool `json:"is_system"`
|
||||
Permissions []string `json:"permissions"`
|
||||
// MemberCount is how many staff hold the role, for the roles screen.
|
||||
MemberCount int `json:"member_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// StaffMember is one account with admin-panel access.
|
||||
type StaffMember struct {
|
||||
UserID string `json:"user_id"`
|
||||
Handle string `json:"handle"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||
RoleID string `json:"role_id"`
|
||||
RoleKey string `json:"role_key"`
|
||||
RoleName string `json:"role_name"`
|
||||
BlockedAt *time.Time `json:"blocked_at,omitempty"`
|
||||
}
|
||||
|
||||
// StaffAccess is what the admin middleware needs about the caller.
|
||||
type StaffAccess struct {
|
||||
RoleKey string
|
||||
RoleName string
|
||||
Permissions []string
|
||||
Blocked bool
|
||||
}
|
||||
Reference in New Issue
Block a user