227 lines
5.9 KiB
Go
227 lines
5.9 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// FilterDB returns the database the admin filter choices are read from. The
|
|
// plugin sets it at boot.
|
|
//
|
|
// TODO: requires SummerCMS change. pact.FilterOptions receives neither a
|
|
// context nor a database handle, so a model whose filter choices come from
|
|
// the database needs this package-level hook.
|
|
var FilterDB func() *gorm.DB
|
|
|
|
// Filter scopes of the admin lists.
|
|
const (
|
|
ScopeByQuiz = "filterByQuiz"
|
|
ScopeByCategory = "filterByCategory"
|
|
ScopeByType = "filterByType"
|
|
)
|
|
|
|
var (
|
|
_ pact.FilterScope = Question{}
|
|
_ pact.FilterOptions = Question{}
|
|
_ pact.FilterScope = Answer{}
|
|
_ pact.FilterOptions = Answer{}
|
|
_ pact.FilterScope = Attempt{}
|
|
_ pact.FilterOptions = Attempt{}
|
|
)
|
|
|
|
// FilterScopes lists the admin list scopes of questions.
|
|
func (Question) FilterScopes() []string { return []string{ScopeByQuiz, ScopeByCategory, ScopeByType} }
|
|
|
|
// FilterScope narrows the questions list to the chosen quizzes, categories
|
|
// or types.
|
|
func (Question) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
values := filterValues(value)
|
|
if db == nil || len(values) == 0 {
|
|
return db
|
|
}
|
|
switch name {
|
|
case ScopeByQuiz:
|
|
return db.Where("golem15_quizzes_questions.quiz_id IN ?", ids(values))
|
|
case ScopeByCategory:
|
|
cats := make([]string, 0, len(values))
|
|
for _, v := range values {
|
|
if c, ok := DecodeCategoryValue(v); ok {
|
|
cats = append(cats, c)
|
|
}
|
|
}
|
|
if len(cats) == 0 {
|
|
return db.Where("1 = 0")
|
|
}
|
|
return db.Where("golem15_quizzes_questions.category IN ?", cats)
|
|
case ScopeByType:
|
|
return db.Where("golem15_quizzes_questions.type IN ?", values)
|
|
}
|
|
return db
|
|
}
|
|
|
|
// FilterOptions serves the quizzes, the distinct categories and the types.
|
|
func (Question) FilterOptions(scope string) []pact.Option {
|
|
switch scope {
|
|
case ScopeByQuiz:
|
|
return quizOptions()
|
|
case ScopeByCategory:
|
|
return categoryOptions()
|
|
case ScopeByType:
|
|
return []pact.Option{
|
|
{Value: TypeSingle, Label: "golem15.quizzes::lang.questions.type_single"},
|
|
{Value: TypeMultiple, Label: "golem15.quizzes::lang.questions.type_multiple"},
|
|
{Value: TypeSelfCheck, Label: "golem15.quizzes::lang.questions.type_self_check"},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FilterScopes lists the admin list scopes of answers.
|
|
func (Answer) FilterScopes() []string { return []string{ScopeByQuiz} }
|
|
|
|
// FilterScope narrows the answers list to the questions of the chosen
|
|
// quizzes.
|
|
func (Answer) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
values := filterValues(value)
|
|
if db == nil || len(values) == 0 || name != ScopeByQuiz {
|
|
return db
|
|
}
|
|
return db.Where("golem15_quizzes_answers.question_id IN (SELECT id FROM golem15_quizzes_questions WHERE quiz_id IN ?)", ids(values))
|
|
}
|
|
|
|
// FilterOptions serves the quizzes.
|
|
func (Answer) FilterOptions(scope string) []pact.Option {
|
|
if scope == ScopeByQuiz {
|
|
return quizOptions()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FilterScopes lists the admin list scopes of attempts.
|
|
func (Attempt) FilterScopes() []string { return []string{ScopeByQuiz} }
|
|
|
|
// FilterScope narrows the attempts list to the chosen quizzes.
|
|
func (Attempt) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
values := filterValues(value)
|
|
if db == nil || len(values) == 0 || name != ScopeByQuiz {
|
|
return db
|
|
}
|
|
return db.Where("golem15_quizzes_attempts.quiz_id IN ?", ids(values))
|
|
}
|
|
|
|
// FilterOptions serves the quizzes.
|
|
func (Attempt) FilterOptions(scope string) []pact.Option {
|
|
if scope == ScopeByQuiz {
|
|
return quizOptions()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func filterDB() *gorm.DB {
|
|
if FilterDB == nil {
|
|
return nil
|
|
}
|
|
return FilterDB()
|
|
}
|
|
|
|
func quizOptions() []pact.Option {
|
|
db := filterDB()
|
|
if db == nil {
|
|
return nil
|
|
}
|
|
var quizzes []Quiz
|
|
if err := db.Select("id", "title").Order("title").Find(&quizzes).Error; err != nil {
|
|
return nil
|
|
}
|
|
out := make([]pact.Option, 0, len(quizzes))
|
|
for _, q := range quizzes {
|
|
out = append(out, pact.Option{Value: strconv.FormatUint(uint64(q.ID), 10), Label: q.Title})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func categoryOptions() []pact.Option {
|
|
db := filterDB()
|
|
if db == nil {
|
|
return nil
|
|
}
|
|
var cats []string
|
|
if err := db.Model(&Question{}).Where("category IS NOT NULL AND category <> ''").
|
|
Distinct("category").Order("category").Pluck("category", &cats).Error; err != nil {
|
|
return nil
|
|
}
|
|
out := make([]pact.Option, 0, len(cats))
|
|
for _, c := range cats {
|
|
out = append(out, pact.Option{Value: EncodeCategoryValue(c), Label: c})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EncodeCategoryValue turns a category into a filter value the admin list
|
|
// accepts.
|
|
//
|
|
// TODO: requires SummerCMS change. cabana passes a scope filter value only
|
|
// when it is an integer or an ASCII identifier, so a category with spaces or
|
|
// diacritics ("Teoria stosunków") travels hex-encoded as "c_<hex>".
|
|
func EncodeCategoryValue(category string) string {
|
|
return "c_" + hex.EncodeToString([]byte(category))
|
|
}
|
|
|
|
// DecodeCategoryValue reverses EncodeCategoryValue.
|
|
func DecodeCategoryValue(v string) (string, bool) {
|
|
raw, ok := strings.CutPrefix(v, "c_")
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
b, err := hex.DecodeString(raw)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return string(b), true
|
|
}
|
|
|
|
func filterValues(value any) []string {
|
|
switch v := value.(type) {
|
|
case string:
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
return []string{v}
|
|
case []string:
|
|
return v
|
|
case int64:
|
|
return []string{strconv.FormatInt(v, 10)}
|
|
case int:
|
|
return []string{strconv.Itoa(v)}
|
|
case []any:
|
|
out := make([]string, 0, len(v))
|
|
for _, x := range v {
|
|
switch s := x.(type) {
|
|
case string:
|
|
if s != "" {
|
|
out = append(out, s)
|
|
}
|
|
case float64:
|
|
out = append(out, strconv.FormatFloat(s, 'f', -1, 64))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ids keeps the numeric values; an empty result matches nothing.
|
|
func ids(values []string) []int64 {
|
|
out := []int64{0}
|
|
for _, v := range values {
|
|
if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
}
|