First commit.
This commit is contained in:
commit
cd7e27504a
24 changed files with 854 additions and 0 deletions
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Probo Collector
|
||||||
|
|
||||||
|
A backend to collect quizzes in a RESTful way.
|
27
client/client.go
Normal file
27
client/client.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "git.andreafazzi.eu/andrea/testhub/models"
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Content interface{} `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuizReadAllResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Content []*models.Quiz `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateQuestionRequest struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateAnswerRequest struct {
|
||||||
|
Text string
|
||||||
|
Correct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateQuizRequest struct {
|
||||||
|
Question *CreateQuestionRequest `json:"question"`
|
||||||
|
Answers []*CreateAnswerRequest `json:"answers"`
|
||||||
|
}
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module git.andreafazzi.eu/andrea/testhub
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/kr/pretty v0.2.1 // indirect
|
||||||
|
github.com/kr/text v0.1.0 // indirect
|
||||||
|
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
)
|
18
go.sum
Normal file
18
go.sum
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8 h1:nRDwTcxV9B3elxMt+1xINX0bwaPdpouqp5fbynexY8U=
|
||||||
|
github.com/remogatto/prettytest v0.0.0-20200211072524-6d385e11dcb8/go.mod h1:jOEnp79oIHy5cvQSHeLcgVJk1GHOOHJHQWps/d1N5Yo=
|
||||||
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
112
logger/logger.go
Normal file
112
logger/logger.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Disabled = iota
|
||||||
|
InfoLevel
|
||||||
|
WarningLevel
|
||||||
|
DebugLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// struct for holding response details
|
||||||
|
responseData struct {
|
||||||
|
status int
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
// our http.ResponseWriter implementation
|
||||||
|
loggingResponseWriter struct {
|
||||||
|
http.ResponseWriter // compose original http.ResponseWriter
|
||||||
|
responseData *responseData
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var loggerLevel int = InfoLevel
|
||||||
|
|
||||||
|
func (r *loggingResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
size, err := r.ResponseWriter.Write(b) // write response using original http.ResponseWriter
|
||||||
|
r.responseData.size += size // capture size
|
||||||
|
return size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *loggingResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
r.ResponseWriter.WriteHeader(statusCode) // write status code using original http.ResponseWriter
|
||||||
|
r.responseData.status = statusCode // capture status code
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogging(h http.Handler) http.Handler {
|
||||||
|
loggingFn := func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
responseData := &responseData{
|
||||||
|
status: 0,
|
||||||
|
size: 0,
|
||||||
|
}
|
||||||
|
lrw := loggingResponseWriter{
|
||||||
|
ResponseWriter: rw, // compose original http.ResponseWriter
|
||||||
|
responseData: responseData,
|
||||||
|
}
|
||||||
|
h.ServeHTTP(&lrw, req) // inject our implementation of http.ResponseWriter
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if loggerLevel >= InfoLevel {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"URI": req.RequestURI,
|
||||||
|
"Method": req.Method,
|
||||||
|
"Status": responseData.status,
|
||||||
|
"Duration": duration,
|
||||||
|
"Size": responseData.size,
|
||||||
|
}).Info("Completed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(loggingFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLevel(level int) {
|
||||||
|
loggerLevel = level
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(v ...interface{}) {
|
||||||
|
if loggerLevel >= InfoLevel {
|
||||||
|
log.Println(v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(format string, v ...interface{}) {
|
||||||
|
if loggerLevel >= InfoLevel {
|
||||||
|
log.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warning(v ...interface{}) {
|
||||||
|
if loggerLevel >= WarningLevel {
|
||||||
|
log.Println(v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warningf(format string, v ...interface{}) {
|
||||||
|
if loggerLevel >= WarningLevel {
|
||||||
|
log.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(v ...interface{}) {
|
||||||
|
if loggerLevel >= DebugLevel {
|
||||||
|
log.Println(v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(format string, v ...interface{}) {
|
||||||
|
if loggerLevel >= DebugLevel {
|
||||||
|
log.Printf(format, v...)
|
||||||
|
}
|
||||||
|
}
|
23
main.go
Normal file
23
main.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/logger"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/store"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const port = "3000"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger.SetLevel(logger.DebugLevel)
|
||||||
|
|
||||||
|
server := NewQuizHubCollectorServer(store.NewMemoryQuizHubCollectorStore())
|
||||||
|
|
||||||
|
addr := "localhost:" + port
|
||||||
|
logrus.WithField("addr", addr).Info("TestHub Collector server is listening.")
|
||||||
|
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port, server))
|
||||||
|
}
|
121
misc/logseq/.gitignore
vendored
Normal file
121
misc/logseq/.gitignore
vendored
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
.DS_Store
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
*~
|
21
misc/logseq/LICENSE.md
Normal file
21
misc/logseq/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
Copyright (c) 2022 Andrea Fazzi
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
4
misc/logseq/README.md
Normal file
4
misc/logseq/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# What's that?
|
||||||
|
|
||||||
|
A very basic boilerplate useful to start devoloping a
|
||||||
|
[Logseq](https://logseq.com/) plugin.
|
10
misc/logseq/icon.svg
Normal file
10
misc/logseq/icon.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hierarchy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="gray" fill="gray" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<circle cx="12" cy="5" r="2" />
|
||||||
|
<circle cx="5" cy="19" r="2" />
|
||||||
|
<circle cx="19" cy="19" r="2" />
|
||||||
|
<path d="M6.5 17.5l5.5 -4.5l5.5 4.5" />
|
||||||
|
<line x1="12" y1="7" x2="12" y2="13" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 471 B |
29
misc/logseq/package.json
Normal file
29
misc/logseq/package.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"logseq": {
|
||||||
|
"id": "logseq-helloworld-plugin",
|
||||||
|
"title": "logseq-helloworld-plugin",
|
||||||
|
"icon": "./icon.svg"
|
||||||
|
},
|
||||||
|
"name": "logseq-helloworld-plugin",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.html",
|
||||||
|
"targets": {
|
||||||
|
"main": false
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "parcel build --no-source-maps src/index.html --public-url ./"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Andrea Fazzi",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@logseq/libs": "^0.0.1-alpha.35",
|
||||||
|
"js-base64": "^3.7.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"parcel": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
13
misc/logseq/src/index.html
Normal file
13
misc/logseq/src/index.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="index.ts" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
101
misc/logseq/src/index.ts
Normal file
101
misc/logseq/src/index.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import "@logseq/libs";
|
||||||
|
import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user";
|
||||||
|
|
||||||
|
const endpoint = 'http://localhost:3000/quizzes';
|
||||||
|
|
||||||
|
const uniqueIdentifier = () =>
|
||||||
|
Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.replace(/[^a-z]+/g, "");
|
||||||
|
|
||||||
|
async function fetchQuizzes() {
|
||||||
|
const { status: status, content: quizzes } = await fetch(endpoint).then(res => res.json())
|
||||||
|
const ret = quizzes || []
|
||||||
|
|
||||||
|
return ret.map((quiz, i) => {
|
||||||
|
return `${i + 1}. ${quiz.Question.Text}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = () => {
|
||||||
|
console.log("logseq-quizhub-plugin LOADED!");
|
||||||
|
|
||||||
|
logseq.Editor.registerSlashCommand("Get All Quizzes", async () => {
|
||||||
|
const currBlock = await logseq.Editor.getCurrentBlock();
|
||||||
|
|
||||||
|
let blocks = await fetchQuizzes()
|
||||||
|
|
||||||
|
blocks = blocks.map((it: BlockEntity) => ({ content: it }))
|
||||||
|
|
||||||
|
await logseq.Editor.insertBatchBlock(currBlock.uuid, blocks, {
|
||||||
|
sibling: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
logseq.Editor.registerSlashCommand("Insert a batch of quizzes", async () => {
|
||||||
|
await logseq.Editor.insertAtEditingCursor(
|
||||||
|
`{{renderer :quizhub_${uniqueIdentifier()}}}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const currBlock = await logseq.Editor.getCurrentBlock();
|
||||||
|
await logseq.Editor.insertBlock(currBlock.uuid, 'Text of the question here...',
|
||||||
|
{
|
||||||
|
sibling: false,
|
||||||
|
before: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await logseq.Editor.exitEditingMode();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => {
|
||||||
|
const [type] = payload.arguments;
|
||||||
|
const id = type.split("_")[1]?.trim();
|
||||||
|
const quizhubId = `quizhub_${id}`;
|
||||||
|
|
||||||
|
logseq.provideModel({
|
||||||
|
async postQuiz() {
|
||||||
|
const parentBlock = await logseq.Editor.getBlock(payload.uuid, { includeChildren: true });
|
||||||
|
const quizzes = parentBlock.children.map((child: BlockEntity) => {
|
||||||
|
const question = { text: child.content }
|
||||||
|
const answers = child.children.map((answer: BlockEntity, i: number) => {
|
||||||
|
return { text: answer.content, correct: (i == 0) ? true : false }
|
||||||
|
})
|
||||||
|
return { question: question, answers: answers }
|
||||||
|
});
|
||||||
|
quizzes.forEach(async quiz => {
|
||||||
|
const res = await fetch(endpoint, { method: 'POST', body: JSON.stringify(quiz) })
|
||||||
|
const data = await res.json();
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logseq.provideStyle(`
|
||||||
|
.renderBtn {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 3px;
|
||||||
|
font-size: 80%;
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.renderBtn:hover {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
logseq.provideUI({
|
||||||
|
key: `${quizhubId}`,
|
||||||
|
slot,
|
||||||
|
reset: true,
|
||||||
|
template: `<button data-on-click="postQuiz" class="renderBtn">Save</button>`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
logseq.ready(main).catch(console.error);
|
6
models/answer.go
Normal file
6
models/answer.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Answer struct {
|
||||||
|
ID string
|
||||||
|
Text string
|
||||||
|
}
|
6
models/player.go
Normal file
6
models/player.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
Name string
|
||||||
|
Wins int
|
||||||
|
}
|
7
models/question.go
Normal file
7
models/question.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Question struct {
|
||||||
|
ID string
|
||||||
|
Text string
|
||||||
|
AnswerIDs []string
|
||||||
|
}
|
10
models/test.go
Normal file
10
models/test.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type Quiz struct {
|
||||||
|
ID uint
|
||||||
|
|
||||||
|
Question *Question
|
||||||
|
Answers []*Answer
|
||||||
|
Correct *Answer
|
||||||
|
Type int
|
||||||
|
}
|
68
server.go
Normal file
68
server.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/client"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/logger"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/models"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonContentType = "application/json"
|
||||||
|
|
||||||
|
type QuizHubCollectorServer struct {
|
||||||
|
store store.QuizHubCollectorStore
|
||||||
|
http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQuizHubCollectorServer(store store.QuizHubCollectorStore) *QuizHubCollectorServer {
|
||||||
|
ps := new(QuizHubCollectorServer)
|
||||||
|
ps.store = store
|
||||||
|
|
||||||
|
router := http.NewServeMux()
|
||||||
|
|
||||||
|
router.Handle("/quizzes", logger.WithLogging(http.HandlerFunc(ps.testHandler)))
|
||||||
|
|
||||||
|
ps.Handler = router
|
||||||
|
|
||||||
|
return ps
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *QuizHubCollectorServer) testHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
w.Header().Set("content-type", jsonContentType)
|
||||||
|
json.NewEncoder(w).Encode(ps.readAllQuizzes(w, r))
|
||||||
|
|
||||||
|
case http.MethodPost:
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
json.NewEncoder(w).Encode(ps.createQuiz(w, r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *QuizHubCollectorServer) readAllQuizzes(w http.ResponseWriter, r *http.Request) *client.Response {
|
||||||
|
tests, err := ps.store.ReadAllQuizzes()
|
||||||
|
if err != nil {
|
||||||
|
return &client.Response{Status: "error", Content: err.Error()}
|
||||||
|
}
|
||||||
|
return &client.Response{Status: "success", Content: tests}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *QuizHubCollectorServer) createQuiz(w http.ResponseWriter, r *http.Request) *models.Quiz {
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
createQuizReq := new(client.CreateQuizRequest)
|
||||||
|
err = json.Unmarshal(body, &createQuizReq)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdQuiz := ps.store.CreateQuiz(createQuizReq)
|
||||||
|
|
||||||
|
return createdQuiz
|
||||||
|
}
|
69
server_integration_test.go
Normal file
69
server_integration_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/client"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/models"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/store"
|
||||||
|
"github.com/remogatto/prettytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationTestSuite struct {
|
||||||
|
prettytest.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *integrationTestSuite) TestQuizCreateAndReadAll() {
|
||||||
|
server := NewQuizHubCollectorServer(store.NewMemoryQuizHubCollectorStore())
|
||||||
|
|
||||||
|
// POST a new question using a JSON payload
|
||||||
|
|
||||||
|
payload := `
|
||||||
|
{
|
||||||
|
"question": {"text": "Question 1"},
|
||||||
|
"answers": [
|
||||||
|
{"text": "Text of the answer 1", "correct": true},
|
||||||
|
{"text": "Text of the answer 2"},
|
||||||
|
{"text": "Text of the answer 3"},
|
||||||
|
{"text": "Text of the answer 4"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
request, _ := http.NewRequest(http.MethodPost, "/quizzes", strings.NewReader(payload))
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
server.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
returnedTest := new(models.Quiz)
|
||||||
|
|
||||||
|
err := json.Unmarshal(response.Body.Bytes(), returnedTest)
|
||||||
|
if err != nil {
|
||||||
|
t.True(err == nil, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Equal("Question 1", returnedTest.Question.Text)
|
||||||
|
t.Equal("Text of the answer 1", returnedTest.Answers[0].Text)
|
||||||
|
t.Equal("Text of the answer 1", returnedTest.Correct.Text)
|
||||||
|
|
||||||
|
t.True(returnedTest.ID != 0, "Test ID should not be 0")
|
||||||
|
t.True(returnedTest.Question.ID != "", "Question ID should not be empty")
|
||||||
|
t.True(returnedTest.Answers[0].ID != "", "Answer ID should not be empty")
|
||||||
|
|
||||||
|
request, _ = http.NewRequest(http.MethodGet, "/quizzes", nil)
|
||||||
|
response = httptest.NewRecorder()
|
||||||
|
|
||||||
|
server.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
decodedResponse := new(client.QuizReadAllResponse)
|
||||||
|
|
||||||
|
err = json.Unmarshal(response.Body.Bytes(), &decodedResponse)
|
||||||
|
if err != nil {
|
||||||
|
t.True(err == nil, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
t.True(len(decodedResponse.Content) == 1, "Length of returned tests should be 1")
|
||||||
|
t.Equal("Question 1", decodedResponse.Content[0].Question.Text)
|
||||||
|
}
|
90
server_test.go
Normal file
90
server_test.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/client"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/logger"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/models"
|
||||||
|
"github.com/remogatto/prettytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSuite struct {
|
||||||
|
prettytest.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
type StubTestHubCollectorStore struct {
|
||||||
|
tests []*models.Quiz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *StubTestHubCollectorStore) CreateQuiz(test *client.CreateQuizRequest) *models.Quiz {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *StubTestHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
||||||
|
return store.tests, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunner(t *testing.T) {
|
||||||
|
prettytest.Run(
|
||||||
|
t,
|
||||||
|
new(testSuite),
|
||||||
|
new(integrationTestSuite),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testSuite) BeforeAll() {
|
||||||
|
logger.SetLevel(logger.Disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testSuite) TestGETQuestions() {
|
||||||
|
expectedResult := &client.QuizReadAllResponse{
|
||||||
|
Status: "success",
|
||||||
|
Content: []*models.Quiz{
|
||||||
|
{
|
||||||
|
Question: &models.Question{ID: "1", Text: "Question 1"},
|
||||||
|
Answers: []*models.Answer{{}, {}, {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
store := &StubTestHubCollectorStore{[]*models.Quiz{
|
||||||
|
{
|
||||||
|
Question: &models.Question{ID: "1", Text: "Question 1"},
|
||||||
|
Answers: []*models.Answer{{}, {}, {}},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
server := NewQuizHubCollectorServer(store)
|
||||||
|
|
||||||
|
request, _ := http.NewRequest(http.MethodGet, "/quizzes", nil)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
|
||||||
|
server.ServeHTTP(response, request)
|
||||||
|
|
||||||
|
result := getResponse(response.Body)
|
||||||
|
|
||||||
|
t.True(result.Status == expectedResult.Status)
|
||||||
|
t.True(testsAreEqual(result, expectedResult))
|
||||||
|
|
||||||
|
t.Equal(http.StatusOK, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResponse(body io.Reader) (response *client.QuizReadAllResponse) {
|
||||||
|
err := json.NewDecoder(body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("Unable to parse response from server %q into slice of Test, '%v'", body, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func testsAreEqual(got, want *client.QuizReadAllResponse) bool {
|
||||||
|
return reflect.DeepEqual(got, want)
|
||||||
|
}
|
81
store/memory.go
Normal file
81
store/memory.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/client"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemoryQuizHubCollectorStore struct {
|
||||||
|
questions map[string]*models.Question
|
||||||
|
answers map[string]*models.Answer
|
||||||
|
tests map[uint]*models.Quiz
|
||||||
|
lastQuizID uint
|
||||||
|
|
||||||
|
questionAnswer map[string][]string
|
||||||
|
testQuestion map[string]uint
|
||||||
|
|
||||||
|
// A mutex is used to synchronize read/write access to the map
|
||||||
|
lock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryQuizHubCollectorStore() *MemoryQuizHubCollectorStore {
|
||||||
|
s := new(MemoryQuizHubCollectorStore)
|
||||||
|
|
||||||
|
s.questions = make(map[string]*models.Question)
|
||||||
|
s.answers = make(map[string]*models.Answer)
|
||||||
|
s.tests = make(map[uint]*models.Quiz)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) ReadAllQuizzes() ([]*models.Quiz, error) {
|
||||||
|
result := make([]*models.Quiz, 0)
|
||||||
|
for _, t := range s.tests {
|
||||||
|
result = append(result, t)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MemoryQuizHubCollectorStore) CreateQuiz(r *client.CreateQuizRequest) *models.Quiz {
|
||||||
|
questionID := hash(r.Question.Text)
|
||||||
|
test := new(models.Quiz)
|
||||||
|
q, ok := s.questions[questionID]
|
||||||
|
if !ok { // if the question is not in the store add it
|
||||||
|
s.questions[questionID] = &models.Question{
|
||||||
|
ID: questionID,
|
||||||
|
Text: r.Question.Text,
|
||||||
|
}
|
||||||
|
q = s.questions[questionID]
|
||||||
|
}
|
||||||
|
// Populate Question field
|
||||||
|
test.Question = q
|
||||||
|
for _, answer := range r.Answers {
|
||||||
|
// Calculate the hash from text
|
||||||
|
answerID := hash(answer.Text)
|
||||||
|
_, ok := s.answers[answerID]
|
||||||
|
if !ok { // if the answer is not in the store add it
|
||||||
|
s.answers[answerID] = &models.Answer{
|
||||||
|
ID: answerID,
|
||||||
|
Text: answer.Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if answer.Correct {
|
||||||
|
test.Correct = s.answers[answerID]
|
||||||
|
}
|
||||||
|
test.Answers = append(test.Answers, s.answers[answerID])
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastQuizID++
|
||||||
|
test.ID = s.lastQuizID
|
||||||
|
s.tests[s.lastQuizID] = test
|
||||||
|
|
||||||
|
return test
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(text string) string {
|
||||||
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(text)))
|
||||||
|
}
|
11
store/store.go
Normal file
11
store/store.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/client"
|
||||||
|
"git.andreafazzi.eu/andrea/testhub/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuizHubCollectorStore interface {
|
||||||
|
ReadAllQuizzes() ([]*models.Quiz, error)
|
||||||
|
CreateQuiz(r *client.CreateQuizRequest) *models.Quiz
|
||||||
|
}
|
10
tests/hurl/create_new_quiz.hurl
Normal file
10
tests/hurl/create_new_quiz.hurl
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
POST http://localhost:3000/quizzes
|
||||||
|
{
|
||||||
|
"question": {"text": "Text of Question 1"},
|
||||||
|
"answers": [
|
||||||
|
{"text": "Text of the answer 1", "correct": true},
|
||||||
|
{"text": "Text of the answer 2"},
|
||||||
|
{"text": "Text of the answer 3"},
|
||||||
|
{"text": "Text of the answer 4"}
|
||||||
|
]
|
||||||
|
}
|
1
tests/hurl/read_all_quiz.hurl
Normal file
1
tests/hurl/read_all_quiz.hurl
Normal file
|
@ -0,0 +1 @@
|
||||||
|
GET http://localhost:3000/quizzes
|
Loading…
Reference in a new issue