go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/kana-server/pkg/controller/quiz.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package controller 9 10 import ( 11 "net/http" 12 "time" 13 14 "go.charczuk.com/sdk/apputil" 15 "go.charczuk.com/sdk/uuid" 16 "go.charczuk.com/sdk/web" 17 18 "go.charczuk.com/projects/kana-server/pkg/config" 19 "go.charczuk.com/projects/kana-server/pkg/kana" 20 "go.charczuk.com/projects/kana-server/pkg/model" 21 "go.charczuk.com/projects/kana-server/pkg/types" 22 ) 23 24 // Quiz is the quiz controller. 25 type Quiz struct { 26 apputil.BaseController 27 28 Config config.Config 29 Model model.Manager 30 } 31 32 // Register adds the controller methods to the app. 33 func (q Quiz) Register(app *web.App) { 34 app.Get("/quiz.new", web.SessionRequired(q.getQuizNew)) 35 app.Post("/quiz.new", web.SessionRequired(q.postQuizNew)) 36 app.Get("/quiz/:id", web.SessionRequired(q.getQuizPrompt)) 37 app.Post("/quiz/:id/answer", web.SessionRequired(q.postQuizAnswer)) 38 } 39 40 // GET /quiz.new 41 func (q Quiz) getQuizNew(ctx web.Context) web.Result { 42 return ctx.Views().ViewStatus(http.StatusOK, "quiz_new", nil) 43 } 44 45 // POST /quiz.new 46 func (q Quiz) postQuizNew(ctx web.Context) web.Result { 47 userID := q.GetUserID(ctx) 48 49 maxQuestions, _ := web.FormValue[int](ctx, "maxQuestions") 50 maxPrompts, _ := web.FormValue[int](ctx, "maxPrompts") 51 maxRepeatHistory, _ := web.FormValue[int](ctx, "maxRepeatHistory") 52 53 includeHiragana, _ := web.FormValue[string](ctx, "hiragana") 54 includeKatakana, _ := web.FormValue[string](ctx, "katakana") 55 56 var inputs []map[string]string 57 if includeHiragana != "" { 58 inputs = append(inputs, kana.Hiragana) 59 } 60 if includeKatakana != "" { 61 inputs = append(inputs, kana.Katakana) 62 } 63 prompts := kana.SelectCount(kana.Merge(inputs...), maxPrompts) 64 promptWeights := kana.CreateWeights(prompts) 65 66 quiz := types.Quiz{ 67 ID: uuid.V4(), 68 UserID: userID, 69 CreatedUTC: time.Now().UTC(), 70 Hiragana: includeHiragana != "", 71 Katakana: includeKatakana != "", 72 MaxPrompts: maxPrompts, 73 MaxQuestions: maxQuestions, 74 MaxRepeatHistory: maxRepeatHistory, 75 Results: nil, 76 Prompts: prompts, 77 PromptWeights: promptWeights, 78 PromptHistory: nil, 79 } 80 if err := q.Model.CreateQuiz(ctx, quiz); err != nil { 81 return ctx.Views().InternalError(err) 82 } 83 return web.RedirectWithMethodf(http.MethodGet, "/quiz/%s", quiz.ID.String()) 84 } 85 86 // GET /quiz/:id 87 func (q Quiz) getQuizPrompt(ctx web.Context) web.Result { 88 userID := q.GetUserID(ctx) 89 quizID, err := web.RouteValue[uuid.UUID](ctx, "id") 90 if err != nil { 91 return ctx.Views().BadRequest(err) 92 } 93 quiz, found, err := q.Model.GetQuiz(ctx, quizID) 94 if err != nil { 95 return ctx.Views().InternalError(err) 96 } 97 if !found || !quiz.UserID.Equal(userID) { 98 return ctx.Views().NotFound() 99 } 100 101 // filter out the prompts (and weights) 102 // for which we have recent history 103 nonQueried := make(map[string]string) 104 for key, value := range quiz.Prompts { 105 nonQueried[key] = value 106 } 107 nonQueriedWeights := make(map[string]float64) 108 for key, value := range quiz.PromptWeights { 109 nonQueriedWeights[key] = value 110 } 111 for _, queried := range quiz.PromptHistory { 112 delete(nonQueried, queried) 113 delete(nonQueriedWeights, queried) 114 } 115 prompt, expected := kana.SelectWeighted(nonQueried, nonQueriedWeights) 116 return ctx.Views().ViewStatus(http.StatusOK, "quiz", types.QuizPrompt{ 117 Quiz: quiz, 118 CreatedUTC: time.Now().UTC(), 119 Prompt: prompt, 120 Expected: expected, 121 }) 122 } 123 124 // POST /quiz/:id/answer 125 func (q Quiz) postQuizAnswer(ctx web.Context) web.Result { 126 userID := q.GetUserID(ctx) 127 quizID, err := web.RouteValue[uuid.UUID](ctx, "id") 128 if err != nil { 129 return ctx.Views().BadRequest(err) 130 } 131 quiz, found, err := q.Model.GetQuiz(ctx, quizID) 132 if err != nil { 133 return ctx.Views().InternalError(err) 134 } 135 if !found || !quiz.UserID.Equal(userID) { 136 return ctx.Views().NotFound() 137 } 138 createdUTC, err := web.FormValue[int64](ctx, "createdUTC") 139 if err != nil { 140 return ctx.Views().BadRequest(err) 141 } 142 prompt, err := web.FormValue[string](ctx, "prompt") 143 if err != nil { 144 return ctx.Views().BadRequest(err) 145 } 146 expected, err := web.FormValue[string](ctx, "expected") 147 if err != nil { 148 return ctx.Views().BadRequest(err) 149 } 150 actual, err := web.FormValue[string](ctx, "actual") 151 if err != nil { 152 return ctx.Views().BadRequest(err) 153 } 154 155 quizResult := types.QuizResult{ 156 ID: uuid.V4(), 157 UserID: userID, 158 QuizID: quiz.ID, 159 CreatedUTC: time.Unix(0, createdUTC).UTC(), 160 AnsweredUTC: time.Now().UTC(), 161 Prompt: prompt, 162 Expected: expected, 163 Actual: actual, 164 } 165 if quizResult.Correct() { 166 kana.DecreaseWeight(quiz.PromptWeights, prompt) 167 } else { 168 kana.IncreaseWeight(quiz.PromptWeights, prompt) 169 } 170 quiz.LastAnsweredUTC = time.Now().UTC() 171 quiz.PromptHistory = kana.ListAddFixedLength(quiz.PromptHistory, prompt, quiz.MaxRepeatHistory) 172 if err := q.Model.UpdateQuiz(ctx, quiz); err != nil { 173 return ctx.Views().InternalError(err) 174 } 175 if err := q.Model.AddQuizResult(ctx, quizResult); err != nil { 176 return ctx.Views().InternalError(err) 177 } 178 179 if quiz.MaxQuestions > 0 { 180 if len(quiz.Results)+1 >= quiz.MaxQuestions { 181 return web.RedirectWithMethodf(http.MethodGet, "/home/%s", quiz.ID.String()) 182 } 183 } 184 185 return web.RedirectWithMethodf(http.MethodGet, "/quiz/%s", quiz.ID.String()) 186 }