Completed contest/question/answer MVC

This commit is contained in:
Andrea Fazzi 2019-11-14 12:55:22 +01:00
parent 71997176b7
commit 4263fcd035
17 changed files with 331 additions and 27 deletions

View file

@ -1,10 +1,10 @@
PHONY: all PHONY: all
run_with_docker: dockerized:
docker-compose -f compose/docker-compose.yml down docker-compose -f compose/docker-compose.yml down
docker-compose -f compose/docker-compose.yml up --build -d docker-compose -f compose/docker-compose.yml up --build -d
run_outside_docker: dev:
killall main || echo "Process was not running." killall main || echo "Process was not running."
docker-compose -f compose/docker-compose_outside_docker.yml down docker-compose -f compose/docker-compose_outside_docker.yml down
docker-compose -f compose/docker-compose_outside_docker.yml up -d db docker-compose -f compose/docker-compose_outside_docker.yml up -d db

View file

@ -28,6 +28,7 @@ var (
models = []interface{}{ models = []interface{}{
&orm.Question{}, &orm.Question{},
&orm.Answer{}, &orm.Answer{},
&orm.Contest{},
} }
) )

View file

@ -62,7 +62,7 @@ func (a *Answer) Read(args map[string]string, r *http.Request) (interface{}, err
func (a *Answer) ReadAll(args map[string]string, r *http.Request) (interface{}, error) { func (a *Answer) ReadAll(args map[string]string, r *http.Request) (interface{}, error) {
var answers []*Answer var answers []*Answer
if err := DB().Order("created_at").Find(&answers).Error; err != nil { if err := DB().Preload("Question").Order("created_at").Find(&answers).Error; err != nil {
return nil, err return nil, err
} }
return answers, nil return answers, nil
@ -90,6 +90,10 @@ func (a *Answer) Update(args map[string]string, r *http.Request) (interface{}, e
if err != nil { if err != nil {
return nil, err return nil, err
} }
// FIXME: Should not be hard set.
answer.(*Answer).Correct = false
err = renderer.Decode(answer, r) err = renderer.Decode(answer, r)
if err != nil { if err != nil {
return nil, err return nil, err

136
orm/contest.go Normal file
View file

@ -0,0 +1,136 @@
package orm
import (
"fmt"
"net/http"
"time"
"git.andreafazzi.eu/andrea/oef/renderer"
"github.com/jinzhu/gorm"
)
type Contest struct {
gorm.Model
Name string
Category string
Date *time.Time
StartTime *time.Time
EndTime *time.Time
Questions []*Question
}
func (c *Contest) GetID() uint { return c.ID }
func (c *Contest) String() string {
return c.Name
}
func (c *Contest) Create(args map[string]string, r *http.Request) (interface{}, error) {
if r.Method == "GET" {
return nil, nil
} else {
contest := new(Contest)
err := renderer.Decode(contest, r)
if err != nil {
return nil, err
}
contest, err = CreateContest(contest)
if err != nil {
return nil, err
}
return contest, nil
}
}
func (c *Contest) Read(args map[string]string, r *http.Request) (interface{}, error) {
var contest Contest
id := args["id"]
if err := DB().Preload("Questions").First(&contest, id).Error; err != nil {
return nil, err
}
return &contest, nil
}
func (c *Contest) ReadAll(args map[string]string, r *http.Request) (interface{}, error) {
var contests []*Contest
if err := DB().Order("created_at").Find(&contests).Error; err != nil {
return nil, err
}
return contests, nil
}
func (c *Contest) Update(args map[string]string, r *http.Request) (interface{}, error) {
if r.Method == "GET" {
result, err := c.Read(args, r)
if err != nil {
return nil, err
}
contest := result.(*Contest)
return contest, nil
} else {
contest, err := c.Read(args, nil)
if err != nil {
return nil, err
}
err = r.ParseForm()
if err != nil {
return nil, err
}
date := r.FormValue("Date")
startTime := r.FormValue("StartTime")
endTime := r.FormValue("EndTime")
r.PostForm.Set("Date", fmt.Sprintf("%sT%s:00+00:00", date, startTime))
r.PostForm.Set("StartTime", fmt.Sprintf("%sT%s:00+00:00", date, startTime))
r.PostForm.Set("EndTime", fmt.Sprintf("%sT%s:00+00:00", date, endTime))
err = r.ParseForm()
if err != nil {
return nil, err
}
r.Form["Date"][0] = fmt.Sprintf("%sT%s+00:00", date, startTime)
err = renderer.Decode(contest, r)
if err != nil {
return nil, err
}
_, err = SaveContest(contest)
if err != nil {
return nil, err
}
contest, err = c.Read(args, nil)
if err != nil {
return nil, err
}
return contest.(*Contest), nil
}
}
func (c *Contest) Delete(args map[string]string, r *http.Request) (interface{}, error) {
return nil, nil
}
func CreateContest(contest *Contest) (*Contest, error) {
if err := DB().Create(contest).Error; err != nil {
return nil, err
}
return contest, nil
}
func SaveContest(contest interface{}) (interface{}, error) {
if err := DB().Omit("Contests").Save(contest).Error; err != nil {
return nil, err
}
return contest, nil
}

View file

@ -10,9 +10,14 @@ import (
type Question struct { type Question struct {
gorm.Model gorm.Model
Text string Text string
Contest *Contest
ContestID uint `schema:"contest_id"`
Answers []*Answer Answers []*Answer
SelectedContest map[uint]string `gorm:"-"`
AllContests []*Contest `gorm:"-"`
} }
func (q *Question) GetID() uint { return q.ID } func (q *Question) GetID() uint { return q.ID }
@ -23,7 +28,11 @@ func (q *Question) String() string {
func (q *Question) Create(args map[string]string, r *http.Request) (interface{}, error) { func (q *Question) Create(args map[string]string, r *http.Request) (interface{}, error) {
if r.Method == "GET" { if r.Method == "GET" {
return nil, nil question := new(Question)
if err := DB().Find(&question.AllContests).Error; err != nil {
return nil, err
}
return question, nil
} else { } else {
question := new(Question) question := new(Question)
err := renderer.Decode(question, r) err := renderer.Decode(question, r)
@ -43,7 +52,7 @@ func (q *Question) Read(args map[string]string, r *http.Request) (interface{}, e
id := args["id"] id := args["id"]
if err := DB().Preload("Answers").Where("id = ?", id).Find(&question).Error; err != nil { if err := DB().Preload("Answers").Preload("Contest").First(&question, id).Error; err != nil {
return nil, err return nil, err
} }
@ -52,7 +61,7 @@ func (q *Question) Read(args map[string]string, r *http.Request) (interface{}, e
func (q *Question) ReadAll(args map[string]string, r *http.Request) (interface{}, error) { func (q *Question) ReadAll(args map[string]string, r *http.Request) (interface{}, error) {
var questions []*Question var questions []*Question
if err := DB().Order("created_at").Find(&questions).Error; err != nil { if err := DB().Preload("Contest").Order("created_at").Find(&questions).Error; err != nil {
return nil, err return nil, err
} }
return questions, nil return questions, nil
@ -60,7 +69,21 @@ func (q *Question) ReadAll(args map[string]string, r *http.Request) (interface{}
func (q *Question) Update(args map[string]string, r *http.Request) (interface{}, error) { func (q *Question) Update(args map[string]string, r *http.Request) (interface{}, error) {
if r.Method == "GET" { if r.Method == "GET" {
return q.Read(args, r) result, err := q.Read(args, r)
if err != nil {
return nil, err
}
question := result.(*Question)
if err := DB().Find(&question.AllContests).Error; err != nil {
return nil, err
}
question.SelectedContest = make(map[uint]string)
question.SelectedContest[question.ContestID] = "selected"
return question, nil
} else { } else {
question, err := q.Read(args, nil) question, err := q.Read(args, nil)
if err != nil { if err != nil {
@ -94,7 +117,7 @@ func CreateQuestion(question *Question) (*Question, error) {
} }
func SaveQuestion(question interface{}) (interface{}, error) { func SaveQuestion(question interface{}) (interface{}, error) {
if err := DB().Omit("Answers").Save(question).Error; err != nil { if err := DB().Omit("Answers", "Contest").Save(question).Error; err != nil {
return nil, err return nil, err
} }
return question, nil return question, nil

View file

@ -21,6 +21,8 @@ var (
funcMap = template.FuncMap{ funcMap = template.FuncMap{
"query": query, "query": query,
"convertDate": convertDate, "convertDate": convertDate,
"convertTime": convertTime,
"prettyDate": prettyDate,
"modelPath": modelPath, "modelPath": modelPath,
"dict": dict, "dict": dict,
"yaml": yaml, "yaml": yaml,
@ -113,6 +115,9 @@ func incr(value int) int {
func callString(value interface{}) string { func callString(value interface{}) string {
if value != nil { if value != nil {
if reflect.ValueOf(value).Kind() == reflect.String {
return value.(string)
}
return reflect.ValueOf(value).MethodByName("String").Interface().(func() string)() return reflect.ValueOf(value).MethodByName("String").Interface().(func() string)()
} else { } else {
return "" return ""
@ -185,13 +190,29 @@ func query(values ...string) template.URL {
} }
func convertDate(value interface{}) string { func convertDate(value interface{}) string {
t, ok := value.(time.Time) t, ok := value.(*time.Time)
if !ok { if !ok {
return "" return ""
} }
return fmt.Sprintf("%d-%02d-%02d", t.Year(), t.Month(), t.Day()) return fmt.Sprintf("%d-%02d-%02d", t.Year(), t.Month(), t.Day())
} }
func prettyDate(value interface{}) string {
t, ok := value.(*time.Time)
if !ok {
return ""
}
return fmt.Sprintf("%02d/%02d/%d", t.Day(), t.Month(), t.Year())
}
func convertTime(value interface{}) string {
t, ok := value.(*time.Time)
if !ok {
return ""
}
return fmt.Sprintf("%02d:%02d", t.Hour(), t.Minute())
}
func modelPath(model string, action string, id uint) string { func modelPath(model string, action string, id uint) string {
var q template.URL var q template.URL

View file

@ -280,9 +280,13 @@ func Decode(dst interface{}, r *http.Request) error {
if value == "" { if value == "" {
return reflect.ValueOf(time.Time{}) return reflect.ValueOf(time.Time{})
} }
if v, err := time.Parse("2006-01-02", value); err == nil {
if v, err := time.Parse(time.RFC3339, value); err == nil {
return reflect.ValueOf(v) return reflect.ValueOf(v)
} else {
log.Println(value, err)
} }
return reflect.Value{} return reflect.Value{}
} }

View file

@ -19,7 +19,7 @@
role="form" role="form"
{{" id={{$form}}>"}} {{" id={{$form}}>"}}
{{" {{$options := ` { cancelTitle: \"Annulla\", saveTitle: \"Salva\", model: "}}"{{.Model}}"{{" `}} "}} {{" {{$options := ` { cancelTitle: \"Annulla\", saveTitle: \"Salva\", model: "}}"{{.Model}}"{{" } `}} "}}
{{" {{template \"submit_cancel_buttons\" dict \"options\" ($options|yaml) \"id\" (.Data|field \"ID\") \"update\" $update}} "}} {{" {{template \"submit_cancel_buttons\" dict \"options\" ($options|yaml) \"id\" (.Data|field \"ID\") \"update\" $update}} "}}
</form> </form>

View file

@ -19,8 +19,11 @@
<span class="fa fa-reply"></span> <span class="fa fa-reply"></span>
{{$element|string}} {{$element|string}}
<div class="text-right"> <div class="text-right">
{{$options := `noElements: "no subelements"`}} {{$options := `noElements: "nessuna domanda"`}}
{{/*template "small" dict "options" ($options | yaml) "data" $element.SubElements*/}} {{template "small" dict "options" ($options | yaml) "data" $element.Question}}
{{if $element.Correct}}
{{template "small" dict "options" ($options | yaml) "data" "corretta"}}
{{end}}
</div> </div>
</a> </a>
{{end}} {{end}}

View file

@ -5,7 +5,7 @@
{{if $update}} {{if $update}}
{{template "breadcrumb" toSlice "Risposte" (all "Answer") (.Data|string) (.Data.ID|show "Answer") "Aggiorna" "current"}} {{template "breadcrumb" toSlice "Risposte" (all "Answer") (.Data|string|trim) (.Data.ID|show "Answer") "Aggiorna" "current"}}
{{else}} {{else}}
{{template "breadcrumb" toSlice "Risposte" (all "Answer") "Aggiungi" "current"}} {{template "breadcrumb" toSlice "Risposte" (all "Answer") "Aggiungi" "current"}}
{{end}} {{end}}
@ -25,6 +25,9 @@
{{$options := ` { name: "question_id", id: "question_id", label: "Domanda relativa a questa risposta", title: "Seleziona la domanda"}`}} {{$options := ` { name: "question_id", id: "question_id", label: "Domanda relativa a questa risposta", title: "Seleziona la domanda"}`}}
{{template "select" dict "options" ($options|yaml) "data" (.Data|field "AllQuestions") "selected" (.Data|field "Selected") "update" $update "form" $form}} {{template "select" dict "options" ($options|yaml) "data" (.Data|field "AllQuestions") "selected" (.Data|field "Selected") "update" $update "form" $form}}
{{$options := ` { name: "Correct",id: "answer_correct",label: "La risposta è corretta",placeholder: "",type: "checkbox",formClass: "form-check form-check-inline"} `}}
{{template "checkbox" dict "options" ($options|yaml) "value" (.Data|field "Correct") "update" $update}}
{{$options := ` { cancelTitle: "Annulla", saveTitle: "Salva", model: "Answer"} `}} {{$options := ` { cancelTitle: "Annulla", saveTitle: "Salva", model: "Answer"} `}}
{{template "submit_cancel_buttons" dict "options" ($options|yaml) "id" (.Data|field "ID") "update" $update}} {{template "submit_cancel_buttons" dict "options" ($options|yaml) "id" (.Data|field "ID") "update" $update}}

View file

@ -0,0 +1,33 @@
{{ define "content" }}
<div class="container">
{{$options := `
title: "Gare"
buttonTitle: "Crea nuova gara"
`}}
{{template "read_all_header" dict "options" ($options | yaml) "lengthData" (len .Data) "modelPath" (create "Contest")}}
{{template "search_input"}}
{{if not .}}
{{template "display_no_elements"}}
{{else}}
<div class="list-group" id="myUL">
{{range $element := .Data}}
<a class="list-group-item list-group-item-action" href={{$element.ID|show "Contest"}}>
<span class="fa fa-user"></span>
{{$element|string}}
<div class="text-right">
{{$options := `noElements: "nessuna data"`}}
{{template "small" dict "options" ($options | yaml) "data" ($element.Date|prettyDate)}}
</div>
</a>
{{end}}
{{end}}
</div>
</div>
{{ end }}

View file

@ -0,0 +1,46 @@
{{ define "content" }}
<div class="container">
{{$update := .Options.Get "update"}}
{{if $update}}
{{template "breadcrumb" toSlice "Contests" (all "Contest") (.Data|string) (.Data.ID|show "Contest") "Aggiorna" "current"}}
{{else}}
{{template "breadcrumb" toSlice "Contests" (all "Contest") "Aggiungi" "current"}}
{{end}}
{{template "add_update_header" dict "update" $update "addTitle" "Crea nuovo ELEMENTO" "updateTitle" (printf "Aggiorna gara %s" (.Data|string))}}
{{$form := "form_add_update"}}
<form
class="needs-validation"
action="{{if $update}}{{.Data.ID|update "Contest"}}{{else}}{{create "Contest"}}{{end}}"
method="POST"
role="form"
id={{$form}}>
{{$options := ` { name: "Name",id: "contest_name",label: "Nome della gara",placeholder: "Inserire il nome della gara",type: "text",required: "true"} `}}
{{template "input" dict "options" ($options|yaml) "value" (.Data|field "Name") "update" $update}}
<div class="form-row">
<div class="col">
{{$options := ` { name: "Date",id: "contest_date",label: "La gara si svolgerà il giorno",type: "date" } `}}
{{template "input" dict "options" ($options|yaml) "value" (.Data|field "Date"|convertDate) "update" $update}}
</div>
<div class="col">
{{$options := ` { name: "StartTime",id: "contest_start_time",label: "Dalle ore",type: "time" } `}}
{{template "input" dict "options" ($options|yaml) "value" (.Data|field "StartTime"|convertTime) "update" $update}}
</div>
<div class="col">
{{$options := ` { name: "EndTime",id: "contest_end_time",label: "Alle ore",type: "time" } `}}
{{template "input" dict "options" ($options|yaml) "value" (.Data|field "EndTime"|convertTime) "update" $update}}
</div>
</div>
{{$options := ` { cancelTitle: "Annulla", saveTitle: "Salva", model: "Contest" }`}}
{{template "submit_cancel_buttons" dict "options" ($options|yaml) "id" (.Data|field "ID") "update" $update}}
</form>
</div>
{{ end }}

View file

@ -0,0 +1,27 @@
{{ define "content" }}
<div class="container">
{{template "breadcrumb" toSlice "Gare" (all "Contest") (.Data|string) "current"}}
{{template "show_header" dict "title" (.Data|string) "updatePath" (.Data.ID|update "Contest") "deletePath" (.Data.ID|delete "Contest")}}
<h2 class="karmen-relation-header">Informazioni generali sulla gara</h2>
<p>La gara si svolgerà il giorno <strong>{{.Data.Date|prettyDate}}</strong> dalle ore <strong>{{.Data.StartTime|convertTime}}</strong> alle ore <strong>{{.Data.EndTime|convertTime}}</strong>.</p>
<div class="row">
<div class="col-md-12">
{{$options := `
title: "Domande"
model: "Question"
icon: "fa fa-question-circle"
`}}
{{$noElements := "nessuna domanda associata alla gara"}}
{{template "relation_list" dict "options" ($options|yaml) "data" .Data.Questions "noElements" $noElements}}
</div>
</div>
</div>
{{ end }}

View file

@ -12,7 +12,7 @@
<body> <body>
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary">
<a class="navbar-brand" href="/teachers?{{query "tpl_layout" "base" "tpl_content" "teachers"}}"> <a class="navbar-brand" href="{{all "Contest"}}">
<span class="fa fa-landmark"></span> <span class="fa fa-landmark"></span>
OEF 2020 OEF 2020
</a> </a>
@ -21,14 +21,15 @@
</button> </button>
<div id="navbar" class="collapse navbar-collapse"> <div id="navbar" class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<a class="nav-item nav-link {{.Options|active "Question"}}" href="{{all "Question"}}">Domande</a> <a class="nav-item nav-link {{.Options|active "Contest"}}" href="{{all "Contest"}}">Gare</a>
<a class="nav-item nav-link {{.Options|active "Answer"}}" href="{{all "Answers"}}">Risposte</a> <a class="nav-item nav-link {{.Options|active "Question"}}" href="{{all "Question"}}">Domande</a>
</ul> <a class="nav-item nav-link {{.Options|active "Answer"}}" href="{{all "Answers"}}">Risposte</a>
</ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li><a class="nav-link" href="/logout">Esci</a></li> <li><a class="nav-link" href="/logout">Esci</a></li>
</ul> </ul>
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</nav> </nav>

View file

@ -12,7 +12,7 @@
<body> <body>
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary"> <nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary">
<a class="navbar-brand" href="/teachers?{{query "tpl_layout" "base" "tpl_content" "teachers"}}"> <a class="navbar-brand" href="{{all "Contest"}}">
<span class="fa fa-landmark"></span> <span class="fa fa-landmark"></span>
OEF 2020 OEF 2020
</a> </a>

View file

@ -19,8 +19,8 @@
<span class="fa fa-question-circle"></span> <span class="fa fa-question-circle"></span>
{{$element|string}} {{$element|string}}
<div class="text-right"> <div class="text-right">
{{$options := `noElements: "no subelements"`}} {{$options := `noElements: "nessuna gara"`}}
{{/*template "small" dict "options" ($options | yaml) "data" $element.SubElements*/}} {{template "small" dict "options" ($options | yaml) "data" $element.Contest}}
</div> </div>
</a> </a>
{{end}} {{end}}

View file

@ -22,9 +22,11 @@
{{$options := ` { name: "Text",id: "question_text",label: "Testo della domanda",placeholder: "Inserire il testo della domanda",type: "text",required: "true"} `}} {{$options := ` { name: "Text",id: "question_text",label: "Testo della domanda",placeholder: "Inserire il testo della domanda",type: "text",required: "true"} `}}
{{template "input" dict "options" ($options|yaml) "value" (.Data|field "Text") "update" $update}} {{template "input" dict "options" ($options|yaml) "value" (.Data|field "Text") "update" $update}}
{{$options := ` { name: "contest_id", id: "contest_id", label: "Gara relativa a questa domanda", title: "Seleziona la gara"}`}}
{{template "select" dict "options" ($options|yaml) "data" (.Data|field "AllContests") "selected" (.Data|field "SelectedContest") "update" $update "form" $form}}
{{$options := ` { cancelTitle: "Annulla", saveTitle: "Salva", model: "Question"} `}} {{$options := ` { cancelTitle: "Annulla", saveTitle: "Salva", model: "Question"} `}}
{{template "submit_cancel_buttons" dict "options" ($options|yaml) "id" (.Data|field "ID") "update" $update}} {{template "submit_cancel_buttons" dict "options" ($options|yaml) "id" (.Data|field "ID") "update" $update}}
</form> </form>
</div> </div>