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  }