Skip to content

Commit

Permalink
feat(quiz): add code syntax highlight to questions
Browse files Browse the repository at this point in the history
Closes #211
  • Loading branch information
michaelcoll committed Sep 13, 2023
1 parent 7ee90e5 commit fbb5130
Show file tree
Hide file tree
Showing 20 changed files with 710 additions and 986 deletions.
12 changes: 8 additions & 4 deletions db/migrations/v1_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ CREATE TABLE quiz

CREATE TABLE quiz_question
(
sha1 TEXT PRIMARY KEY,
position INTEGER NOT NULL,
content TEXT NOT NULL
sha1 TEXT PRIMARY KEY,
position INTEGER NOT NULL,
content TEXT NOT NULL,
code TEXT,
code_language TEXT
);

CREATE TABLE quiz_question_quiz
Expand Down Expand Up @@ -218,8 +220,10 @@ SELECT qsv.session_uuid AS session_uuid
qsv.checked_answers AS checked_answers,
qsv.results AS results,
srv.question_sha1 AS question_sha1,
qq.content AS question_content,
qq.position AS question_position,
qq.content AS question_content,
qq.code AS question_code,
qq.code_language AS question_code_language,
srv.answer_sha1 AS answer_sha1,
qa.content AS answer_content,
CASE WHEN srv.checked IS NULL THEN 0 ELSE srv.checked END AS answer_checked,
Expand Down
32 changes: 17 additions & 15 deletions db/queries/quiz.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ REPLACE INTO quiz (sha1, name, filename, version, duration, created_at)
VALUES (?, ?, ?, ?, ?, ?);

-- name: CreateOrReplaceQuestion :exec
REPLACE INTO quiz_question (sha1, position, content)
VALUES (?, ?, ?);
REPLACE INTO quiz_question (sha1, position, content, code, code_language)
VALUES (?, ?, ?, ?, ?);

-- name: CreateOrReplaceAnswer :exec
REPLACE INTO quiz_answer (sha1, content, valid)
Expand All @@ -25,19 +25,21 @@ WHERE filename = ?
AND version <> ?;

-- name: FindQuizFullBySha1 :many
SELECT q.sha1 AS quiz_sha1,
q.filename AS quiz_filename,
q.name AS quiz_name,
q.version AS quiz_version,
q.created_at AS quiz_created_at,
q.duration AS quiz_duration,
q.active AS quiz_active,
qq.sha1 AS question_sha1,
qq.content AS question_content,
qq.position AS question_position,
qa.sha1 AS answer_sha1,
qa.content AS answer_content,
qa.valid AS answer_valid
SELECT q.sha1 AS quiz_sha1,
q.filename AS quiz_filename,
q.name AS quiz_name,
q.version AS quiz_version,
q.created_at AS quiz_created_at,
q.duration AS quiz_duration,
q.active AS quiz_active,
qq.sha1 AS question_sha1,
qq.content AS question_content,
qq.position AS question_position,
qq.code AS question_code,
qq.code_language AS question_code_language,
qa.sha1 AS answer_sha1,
qa.content AS answer_content,
qa.valid AS answer_valid
FROM quiz q
JOIN quiz_question_quiz qqq ON q.sha1 = qqq.quiz_sha1
JOIN quiz_question qq ON qq.sha1 = qqq.question_sha1
Expand Down
10 changes: 10 additions & 0 deletions doc/openapi/spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,16 @@ components:
description: The question content
nullable: false
example: 'Which of the following characters have used the title "Captain America" in the comics? (Select all that apply)'
code:
type: string
description: The question code of the content
nullable: true
example: 'some commands'
codeLanguage:
type: string
description: The question code language of the content
nullable: true
example: 'shell'
answers:
type: array
items:
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/bytedance/sonic v1.10.0 // indirect
github.com/bytedance/sonic v1.10.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
Expand All @@ -37,7 +37,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.3 // indirect
github.com/go-playground/validator/v10 v10.15.4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
Expand Down Expand Up @@ -67,7 +67,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/arch v0.4.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
Expand Down Expand Up @@ -122,8 +122,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
Expand Down Expand Up @@ -318,8 +318,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down
8 changes: 5 additions & 3 deletions internal/back/domain/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ func (q *Quiz) GetQuestions() map[string]QuizQuestion {
type QuizQuestion struct {
Sha1 string

Content string
Position int
Answers map[string]QuizQuestionAnswer
Content string
Code string
CodeLanguage string
Position int
Answers map[string]QuizQuestionAnswer
}

type QuizQuestionAnswer struct {
Expand Down
3 changes: 2 additions & 1 deletion internal/back/domain/quiz.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Parmi les choix, donner les VCS
---
Question with a `command` ?
```shell
command
command1
command2
```
- [x] Answer 1
- [X] Answer 2
Expand Down
48 changes: 32 additions & 16 deletions internal/back/domain/quizParseService.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ import (
"time"
)

var quizNameRegexp = regexp.MustCompile(`^# (?P<quizName>.*) \(duration: (?P<duration>[0-9]+)min\)`)
var quizQuestionRegexp = regexp.MustCompile(`^# .*\n`)
var quizquestionCodeRegexp = regexp.MustCompile("```(?P<language>.*)\\n(?s)(?P<code>.*?)\\n```")
var quizAnswersRegexp = regexp.MustCompile(`(- \[[ xX]] .*\n)+`)
var quizAnswerRegexp = regexp.MustCompile(`- \[[ xX]] .*`)
var quizValidAnswerRegexp = regexp.MustCompile(`- \[[xX]] .*`)

// Parse parse the content of a quiz file
func (s *QuizService) Parse(filename string, content string) (*Quiz, error) {

Expand Down Expand Up @@ -57,9 +64,7 @@ func getSha1(content string) string {
}

func extractQuizNameAndDuration(content string) (string, int, error) {
r := regexp.MustCompile(`^# (?P<quizName>.*) \(duration: (?P<duration>[0-9]+)min\)`)

subMatch := r.FindStringSubmatch(content)
subMatch := quizNameRegexp.FindStringSubmatch(content)

if len(subMatch) < 3 {
return "", 0, fmt.Errorf("quiz name or quiz duration not found. The first line must be '# <Name> (duration: <duration>min)")
Expand All @@ -75,9 +80,7 @@ func extractQuizNameAndDuration(content string) (string, int, error) {
}

func extractQuestions(content string) (map[string]QuizQuestion, error) {
r := regexp.MustCompile(`^# .*\n`)

quizName := r.FindString(content)
quizName := quizQuestionRegexp.FindString(content)
questionsStr := strings.ReplaceAll(content, quizName, "")
questionsUnParsed := strings.Split(questionsStr, "---\n")

Expand All @@ -98,33 +101,46 @@ func extractQuestions(content string) (map[string]QuizQuestion, error) {

func extractQuestion(content string) (QuizQuestion, error) {

r := regexp.MustCompile(`(- \[[ xX]] .*\n)+`)
answersStr := r.FindString(content)
answersStr := quizAnswersRegexp.FindString(content)

questionContent := strings.ReplaceAll(content, answersStr, "")
questionContent, code, language := extractQuestionCode(strings.ReplaceAll(content, answersStr, ""))

answers, err := extractAnswers(answersStr)
if err != nil {
return QuizQuestion{}, err
}

return QuizQuestion{
Sha1: getSha1(content),
Content: strings.Trim(questionContent, " \n"),
Answers: answers,
Sha1: getSha1(content),
Content: strings.Trim(questionContent, " \n"),
Code: code,
CodeLanguage: language,
Answers: answers,
}, nil
}

func extractQuestionCode(questionContent string) (content string, code string, language string) {
subMatch := quizquestionCodeRegexp.FindStringSubmatch(questionContent)

if len(subMatch) < 3 {
return questionContent, "", ""
}

language = subMatch[1]
code = subMatch[2]
content = strings.ReplaceAll(questionContent, subMatch[0], "")

return content, code, language
}

func extractAnswers(answersStr string) (map[string]QuizQuestionAnswer, error) {

r := regexp.MustCompile(`- \[[ xX]] .*`)
validTestRegex := regexp.MustCompile(`- \[[xX]] .*`)
answersStrSplit := r.FindAllString(answersStr, 10)
answersStrSplit := quizAnswerRegexp.FindAllString(answersStr, 10)

answers := map[string]QuizQuestionAnswer{}

for _, s := range answersStrSplit {
valid := validTestRegex.MatchString(s)
valid := quizValidAnswerRegexp.MatchString(s)
sha1Str := getSha1(s)

answers[sha1Str] = QuizQuestionAnswer{
Expand Down
4 changes: 3 additions & 1 deletion internal/back/domain/quizParseService_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func TestParse(t *testing.T) {
assert.Equal(t, true, actual.Questions["8e713df4a80094c5708dc4a1a2a1725643aa375f"].Answers["eb3352743a553af25829c32b2492c1a41a739f1e"].Valid)
assert.Equal(t, false, actual.Questions["0340bede2f41b9b2ce12b867cc5bf0cb1bd4eabd"].Answers["332b7ca50a406b2337e339332f66f3676d885fef"].Valid)
assert.Equal(t, false, actual.Questions["0340bede2f41b9b2ce12b867cc5bf0cb1bd4eabd"].Answers["22128893c69197141a17149b7de81419aca57e67"].Valid)
assert.Equal(t, "Question with a `command` ?\n```shell\ncommand \n```", actual.Questions["0340bede2f41b9b2ce12b867cc5bf0cb1bd4eabd"].Content)
assert.Equal(t, "Question with a `command` ?", actual.Questions["37f33e32ba7d312df5de7c0bcf03ced422a3413b"].Content)
assert.Equal(t, "command1\ncommand2", actual.Questions["37f33e32ba7d312df5de7c0bcf03ced422a3413b"].Code)
assert.Equal(t, "shell", actual.Questions["37f33e32ba7d312df5de7c0bcf03ced422a3413b"].CodeLanguage)
}

func Test_extractQuizNameAndDuration(t *testing.T) {
Expand Down
12 changes: 8 additions & 4 deletions internal/back/infrastructure/db/migration.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 34 additions & 28 deletions internal/back/infrastructure/quizRepositoryImpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,20 @@ func (r *QuizDBRepository) FindFullBySha1(ctx context.Context, sha1 string, user

if _, found := quiz.Questions[entity.QuestionSha1]; !found {
newQuestion := domain.QuizQuestion{
Sha1: entity.QuestionSha1,
Position: entity.QuestionPosition,
Content: entity.QuestionContent,
Answers: map[string]domain.QuizQuestionAnswer{},
Sha1: entity.QuestionSha1,
Position: entity.QuestionPosition,
Content: entity.QuestionContent,
Code: entity.QuestionCode.String,
CodeLanguage: entity.QuestionCodeLanguage.String,
Answers: map[string]domain.QuizQuestionAnswer{},
}
quiz.Questions[entity.QuestionSha1] = newQuestion
} else {
quiz.Questions[entity.QuestionSha1].Answers[entity.AnswerSha1] = domain.QuizQuestionAnswer{
Sha1: entity.AnswerSha1,
Content: entity.AnswerContent,
Valid: entity.AnswerValid,
}
}

quiz.Questions[entity.QuestionSha1].Answers[entity.AnswerSha1] = domain.QuizQuestionAnswer{
Sha1: entity.AnswerSha1,
Content: entity.AnswerContent,
Valid: entity.AnswerValid,
}
}

Expand Down Expand Up @@ -180,9 +182,11 @@ func (r *QuizDBRepository) Create(ctx context.Context, quiz *domain.Quiz) error

for _, question := range quiz.Questions {
err := r.q.CreateOrReplaceQuestion(ctx, sqlc.CreateOrReplaceQuestionParams{
Sha1: question.Sha1,
Position: int64(question.Position),
Content: question.Content,
Sha1: question.Sha1,
Position: int64(question.Position),
Content: question.Content,
Code: sql.NullString{String: question.Code, Valid: true},
CodeLanguage: sql.NullString{String: question.CodeLanguage, Valid: true},
})
if err != nil {
return err
Expand Down Expand Up @@ -373,24 +377,26 @@ func (r *QuizDBRepository) FindQuizSessionByUuid(ctx context.Context, sessionUui

if _, found := sessionDetail.Questions[entity.QuestionSha1]; !found {
newQuestion := domain.QuizQuestion{
Sha1: entity.QuestionSha1,
Content: entity.QuestionContent,
Position: entity.QuestionPosition,
Answers: map[string]domain.QuizQuestionAnswer{},
Sha1: entity.QuestionSha1,
Position: entity.QuestionPosition,
Content: entity.QuestionContent,
Code: entity.QuestionCode.String,
CodeLanguage: entity.QuestionCodeLanguage.String,
Answers: map[string]domain.QuizQuestionAnswer{},
}
sessionDetail.Questions[entity.QuestionSha1] = newQuestion
} else {
answerValid := false
if sessionDetail.RemainingSec == 0 {
answerValid = entity.AnswerValid
}
}

sessionDetail.Questions[entity.QuestionSha1].Answers[entity.AnswerSha1] = domain.QuizQuestionAnswer{
Sha1: entity.AnswerSha1,
Content: entity.AnswerContent,
Checked: entity.AnswerChecked,
Valid: answerValid,
}
answerValid := false
if sessionDetail.RemainingSec == 0 {
answerValid = entity.AnswerValid
}

sessionDetail.Questions[entity.QuestionSha1].Answers[entity.AnswerSha1] = domain.QuizQuestionAnswer{
Sha1: entity.AnswerSha1,
Content: entity.AnswerContent,
Checked: entity.AnswerChecked,
Valid: answerValid,
}
}

Expand Down
Loading

0 comments on commit fbb5130

Please sign in to comment.