Compare commits
29 Commits
f60ebcfb36
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 348c737163 | |||
| c3d6510a7d | |||
| c7fada2c54 | |||
| c0f45d98c2 | |||
| 1a31006b21 | |||
| e8ac96a065 | |||
| 3a9e01a683 | |||
| 035e9c848f | |||
| 605e117586 | |||
| 68810d93a7 | |||
| e7c2832865 | |||
| 9d2efcc1c4 | |||
| 88a3cae4fa | |||
| 938d3cd307 | |||
| 97986063df | |||
| 251ecd94d4 | |||
| 6d1769ff24 | |||
| af4a4f7840 | |||
| e5dfccabbf | |||
| d5f76f4e07 | |||
| d4974b30ec | |||
| 8b8a7618a2 | |||
| 067c63baa8 | |||
| 5fd0861e2d | |||
| 728c8fa59e | |||
| 2c0564f68c | |||
| 48f5d80f7a | |||
| fbe9927ac3 | |||
| 1d4e8e10fb |
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
./assets/kurious binary
|
||||
*/**/*_templ.go binary
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
*.json
|
||||
bin
|
||||
./tags
|
||||
*.sqlite
|
||||
.zed
|
||||
*.log
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
with-expecter: true
|
||||
keeptree: True
|
||||
|
||||
|
||||
1
.task/checksum/generate
Normal file
1
.task/checksum/generate
Normal file
@ -0,0 +1 @@
|
||||
d65622032d35cb78ee1539f4ab4d875b
|
||||
42
Taskfile.yml
42
Taskfile.yml
@ -1,4 +1,4 @@
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
@ -10,34 +10,64 @@ vars:
|
||||
GIT_VERSION:
|
||||
sh: git tag | sort -r --version-sort | head -n1
|
||||
BUILD_TIME:
|
||||
sh: TZ=UTC date --iso-8601=seconds
|
||||
sh: TZ=UTC date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||
LDFLAGS:
|
||||
sh: echo '-X "{{.PROJECT}}.buildTime={{.BUILD_TIME}}" -X "{{.PROJECT}}.commit={{.GIT_COMMIT}}" -X "{{.PROJECT}}.version={{.GIT_VERSION}}"'
|
||||
|
||||
tasks:
|
||||
install_tools:
|
||||
cmds:
|
||||
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
|
||||
- "[[ ! -f $GOBIN/golangci-lint ]] && go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 || echo golang-ci lint installed"
|
||||
- "rm -rf $GOBIN/templ && go install github.com/a-h/templ/cmd/templ@v0.2.707 || echo templ installed"
|
||||
- "[[ ! -f $GOBIN/mockery ]] && go install github.com/vektra/mockery/v2@v2.42.1 || echo mockery installed"
|
||||
generate:
|
||||
run: once
|
||||
cmds:
|
||||
- "$GOBIN/templ generate"
|
||||
sources:
|
||||
- "internal/kurious/ports/http/templ/*.templ"
|
||||
- "internal/kurious/ports/http/bootstrap/*.templ"
|
||||
generates:
|
||||
- "internal/kurious/ports/http/templ/*.go"
|
||||
- "internal/kurious/ports/http/bootstrap/*.go"
|
||||
deps:
|
||||
- install_tools
|
||||
mocks:
|
||||
run: once
|
||||
cmd: "go generate ./internal/..."
|
||||
deps:
|
||||
- install_tools
|
||||
check:
|
||||
run: once
|
||||
cmds:
|
||||
- "$GOBIN/golangci-lint run ./..."
|
||||
deps:
|
||||
- generate
|
||||
test:
|
||||
run: once
|
||||
cmds:
|
||||
- go test ./internal/...
|
||||
deps:
|
||||
- generate
|
||||
build_web:
|
||||
cmds:
|
||||
- go build -o $GOBIN/kuriousweb -v -ldflags '{{.LDFLAGS}}' cmd/kuriweb/*.go
|
||||
deps: [check, test]
|
||||
build_background:
|
||||
cmds:
|
||||
- go build -o $GOBIN/sravnibackground -v -ldflags '{{.LDFLAGS}}' cmd/background/*.go
|
||||
- go build -o $GOBIN/kuriousbg -v -ldflags '{{.LDFLAGS}}' cmd/background/*.go
|
||||
deps: [check, test]
|
||||
build_dev_cli:
|
||||
cmds:
|
||||
- go build -o $GOBIN/sravnicli -v -ldflags '{{.LDFLAGS}}' cmd/dev/sravnicli/*.go
|
||||
- go build -o $GOBIN/sravnicli -v -ldflags '{{.LDFLAGS}}' cmd/dev/sravnicli/*.go
|
||||
deps: [check, test]
|
||||
build:
|
||||
cmds:
|
||||
- task: build_dev_cli
|
||||
- task: build_background
|
||||
- task: build_web
|
||||
|
||||
run:
|
||||
deps: [build]
|
||||
cmds:
|
||||
- $GOBIN/sravnicli
|
||||
- $GOBIN/kuriousweb
|
||||
|
||||
6
assets/kurious/about.txt
Normal file
6
assets/kurious/about.txt
Normal file
@ -0,0 +1,6 @@
|
||||
This favicon was generated using the following font:
|
||||
|
||||
- Font Title: Lemon
|
||||
- Font Author: Copyright 2011 The Lemon Project Authors (https://github.com/etunni/lemon) with Reserved Font Name "Lemon"
|
||||
- Font Source: http://fonts.gstatic.com/s/lemon/v17/HI_EiYEVKqRMq0jBSZXAQ4-d.ttf
|
||||
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))
|
||||
BIN
assets/kurious/android-chrome-192x192.png
Normal file
BIN
assets/kurious/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
assets/kurious/android-chrome-512x512.png
Normal file
BIN
assets/kurious/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/kurious/apple-touch-icon.png
Normal file
BIN
assets/kurious/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
13
assets/kurious/embed.go
Normal file
13
assets/kurious/embed.go
Normal file
@ -0,0 +1,13 @@
|
||||
package kurious
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed *
|
||||
var root embed.FS
|
||||
|
||||
func AsHTTPFileHandler() http.Handler {
|
||||
return http.FileServer(http.FS(root))
|
||||
}
|
||||
BIN
assets/kurious/favicon-16x16.png
Normal file
BIN
assets/kurious/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 343 B |
BIN
assets/kurious/favicon-32x32.png
Normal file
BIN
assets/kurious/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
BIN
assets/kurious/favicon.ico
Normal file
BIN
assets/kurious/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
2
assets/kurious/robots.txt
Normal file
2
assets/kurious/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent:
|
||||
Disallow: /
|
||||
1
assets/kurious/site.webmanifest
Normal file
1
assets/kurious/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
6
assets/kurious/static/style.css
Normal file
6
assets/kurious/static/style.css
Normal file
@ -0,0 +1,6 @@
|
||||
.btn.btn-primary {
|
||||
color: white;
|
||||
background-color: black;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@ -8,10 +8,20 @@ import (
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
)
|
||||
|
||||
type dbEngine string
|
||||
|
||||
const (
|
||||
DBEngineUnknown dbEngine = ""
|
||||
DBEngineYDB dbEngine = "ydb"
|
||||
DBEngineSqlite dbEngine = "sqlite"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Log config.Log `json:"log"`
|
||||
YDB config.YDB `json:"ydb"`
|
||||
SyncSravniCron string `json:"sync_sravni_cron"`
|
||||
Log config.Log `json:"log"`
|
||||
YDB config.YDB `json:"ydb"`
|
||||
Sqlite config.Sqlite `json:"sqlite"`
|
||||
DBEngine dbEngine `json:"db_engine"`
|
||||
SyncSravniCron string `json:"sync_sravni_cron"`
|
||||
|
||||
DebugHTTP bool `json:"debug_http"`
|
||||
}
|
||||
@ -37,5 +47,7 @@ func defaultConfig() Config {
|
||||
Level: config.LogLevelInfo,
|
||||
Format: config.LogFormatText,
|
||||
},
|
||||
// TODO: change to sqlite once it proven to be working
|
||||
DBEngine: DBEngineYDB,
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/common/xlog"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/ports"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
)
|
||||
@ -28,7 +29,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
const savingOrganizationIDInternalInsteadOfExternal = false
|
||||
|
||||
func app(ctx context.Context) error {
|
||||
if !savingOrganizationIDInternalInsteadOfExternal {
|
||||
panic("fix saving ogranization id as external id instead of internal")
|
||||
}
|
||||
|
||||
var cfgpath string
|
||||
if len(os.Args) > 1 {
|
||||
cfgpath = os.Args[1]
|
||||
@ -43,19 +50,49 @@ func app(ctx context.Context) error {
|
||||
|
||||
log := config.NewSLogger(cfg.Log)
|
||||
|
||||
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
||||
LogConfig: cfg.Log,
|
||||
YDB: cfg.YDB,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("making new application: %w", err)
|
||||
}
|
||||
|
||||
sravniClient, err := sravni.NewClient(ctx, log, cfg.DebugHTTP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making sravni client: %w", err)
|
||||
}
|
||||
|
||||
mainPageState, err := sravniClient.GetMainPageState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting main page state: %w", err)
|
||||
}
|
||||
|
||||
dictionaries := mainPageState.Props.InitialReduxState.Dictionaries.Data
|
||||
|
||||
dictFieldsAsMap := func(fields ...sravni.Field) map[string]string {
|
||||
out := make(map[string]string, len(fields))
|
||||
for _, field := range fields {
|
||||
out[field.Value] = field.Name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
courseThematcisMapped := dictFieldsAsMap(dictionaries.CourseThematics.Fields...)
|
||||
learningTypeMapped := dictFieldsAsMap(dictionaries.LearningType.Fields...)
|
||||
|
||||
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
|
||||
|
||||
var dbEngine service.RepositoryEngine
|
||||
switch cfg.DBEngine {
|
||||
case DBEngineSqlite:
|
||||
dbEngine = service.RepositoryEngineSqlite
|
||||
case DBEngineYDB:
|
||||
dbEngine = service.RepositoryEngineYDB
|
||||
}
|
||||
|
||||
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
||||
LogConfig: cfg.Log,
|
||||
YDB: cfg.YDB,
|
||||
Sqlite: cfg.Sqlite,
|
||||
Engine: dbEngine,
|
||||
}, mapper)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making new application: %w", err)
|
||||
}
|
||||
|
||||
bgProcess := ports.NewBackgroundProcess(ctx, log)
|
||||
err = bgProcess.RegisterSyncSravniHandler(ctx, app, sravniClient, cfg.SyncSravniCron)
|
||||
if err != nil {
|
||||
@ -72,13 +109,16 @@ func app(ctx context.Context) error {
|
||||
defer xcontext.LogInfo(ctx, log, "finished bprocess")
|
||||
|
||||
bgProcess.Run()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
xcontext.LogInfo(ctx, log, "running cancelation waiter")
|
||||
defer xcontext.LogInfo(ctx, log, "finished cancelation waiter")
|
||||
|
||||
<-egctx.Done()
|
||||
|
||||
sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*15)
|
||||
defer sdcancel()
|
||||
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
println("oh well")
|
||||
}
|
||||
@ -61,6 +61,7 @@ func setupCLI(ctx context.Context) cli.App {
|
||||
case "courses":
|
||||
out = state.Props.InitialReduxState.Dictionaries.Data.CourseThematics
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "loaded state", slog.Any("state", out))
|
||||
|
||||
return 0
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
|
||||
"github.com/teris-io/cli"
|
||||
)
|
||||
@ -44,7 +44,6 @@ func setupAPICommand(ctx context.Context) cli.Command {
|
||||
WithOption(learningSelectionOpt).
|
||||
WithAction(newProductsFilterCountAction(ctx))
|
||||
})
|
||||
|
||||
apiEducation := cli.NewCommand("education", "Education related category").
|
||||
WithCommand(apiEducationListProducts).
|
||||
WithCommand(apiEducationFilterCount)
|
||||
@ -136,11 +135,14 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err
|
||||
|
||||
func (a *listProductsAction) handle() error {
|
||||
params := sravni.ListEducationProductsParams{
|
||||
LearningType: a.params.learningType,
|
||||
CoursesThematics: []string{a.params.courseThematic},
|
||||
Limit: a.params.limit,
|
||||
Offset: a.params.offset,
|
||||
LearningType: a.params.learningType,
|
||||
Limit: a.params.limit,
|
||||
Offset: a.params.offset,
|
||||
}
|
||||
if a.params.courseThematic != "" {
|
||||
params.CoursesThematics = append(params.CoursesThematics, a.params.courseThematic)
|
||||
}
|
||||
|
||||
result, err := a.client.ListEducationalProducts(a.ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing education products: %w", err)
|
||||
@ -188,7 +190,7 @@ func (a *productsFilterCountAction) parse(args []string, options map[string]stri
|
||||
filterNotEmpty := func(value string) bool {
|
||||
return value != ""
|
||||
}
|
||||
a.params.courseThematic = xslice.Filter(
|
||||
a.params.courseThematic = xslices.Filter(
|
||||
strings.Split(options[courseThematicOptName], ","),
|
||||
filterNotEmpty,
|
||||
)
|
||||
|
||||
42
cmd/kuriweb/config.go
Normal file
42
cmd/kuriweb/config.go
Normal file
@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Log config.Log `json:"log"`
|
||||
YDB config.YDB `json:"ydb"`
|
||||
Sqlite config.Sqlite `json:"sqlite"`
|
||||
HTTP config.HTTP `json:"http"`
|
||||
Tracing config.Trace `json:"tracing"`
|
||||
DBEngine string `json:"db_engine"`
|
||||
}
|
||||
|
||||
func readFromFile(path string, defaultConfigF func() Config) (Config, error) {
|
||||
out := defaultConfigF()
|
||||
payload, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(payload, &out)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("decoding as json: %w", err)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Log: config.Log{
|
||||
Level: config.LogLevelInfo,
|
||||
Format: config.LogFormatText,
|
||||
},
|
||||
}
|
||||
}
|
||||
273
cmd/kuriweb/http.go
Normal file
273
cmd/kuriweb/http.go
Normal file
@ -0,0 +1,273 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/assets/kurious"
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/generator"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func makePathTemplate(params ...string) string {
|
||||
var sb strings.Builder
|
||||
for _, param := range params {
|
||||
sb.Grow(len(param) + 3)
|
||||
|
||||
sb.WriteRune('/')
|
||||
sb.WriteRune('{')
|
||||
sb.WriteString(param)
|
||||
sb.WriteRune('}')
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func setupCoursesHTTP(srv xhttp.Server, router *mux.Router, _ *slog.Logger) {
|
||||
coursesAPI := srv.Courses()
|
||||
|
||||
router.Handle("/", http.RedirectHandler("/courses", http.StatusPermanentRedirect))
|
||||
|
||||
coursesRouter := router.PathPrefix("/courses").Subrouter().StrictSlash(true)
|
||||
|
||||
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
||||
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
||||
|
||||
muxHandleFunc(coursesRouter, "index", "/", coursesAPI.Index).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, "list_learning", coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
||||
muxHandleFunc(coursesRouter, "list_full", coursesListFullPath, coursesAPI.List).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.Use(
|
||||
middlewareCustomWriterInjector(),
|
||||
mux.CORSMethodMiddleware(router),
|
||||
middlewareLogger(log),
|
||||
middlewareTrace(),
|
||||
middlewareMetrics(),
|
||||
)
|
||||
|
||||
setupCoursesHTTP(srv, router, log)
|
||||
|
||||
if cfg.MountLive {
|
||||
fs := http.FileServer(http.Dir("./assets/kurious/static/"))
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fs)).Methods(http.MethodGet)
|
||||
|
||||
registerFile := func(filepath string) {
|
||||
if !strings.HasPrefix(filepath, "/") {
|
||||
filepath = "/" + filepath
|
||||
}
|
||||
|
||||
relativePath := "./assets/kurious" + filepath
|
||||
router.HandleFunc(filepath, func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, relativePath)
|
||||
}).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
for _, file := range []string{
|
||||
"robots.txt",
|
||||
"android-chrome-192x192.png",
|
||||
"android-chrome-512x512.png",
|
||||
"apple-touch-icon.png",
|
||||
"favicon-16x16.png",
|
||||
"favicon-32x32.png",
|
||||
"favicon.ico",
|
||||
"site.webmanifest",
|
||||
} {
|
||||
registerFile(file)
|
||||
}
|
||||
} else {
|
||||
fs := kurious.AsHTTPFileHandler()
|
||||
router.PathPrefix("/*").Handler(fs).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
Handler: router,
|
||||
}
|
||||
}
|
||||
|
||||
func middlewareCustomWriterInjector() mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wr := wrapWithCustomWriter(w)
|
||||
next.ServeHTTP(wr, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type attributeStringKey string
|
||||
|
||||
func (k attributeStringKey) Value(value string) attribute.KeyValue {
|
||||
return attribute.String(string(k), value)
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func middlewareMetrics() mux.MiddlewareFunc {
|
||||
requestDuration := must(webmetric.Float64Histogram(
|
||||
semconv.HTTPServerRequestDurationName,
|
||||
metric.WithUnit(semconv.HTTPServerRequestDurationUnit),
|
||||
metric.WithDescription(semconv.HTTPServerRequestDurationDescription),
|
||||
))
|
||||
|
||||
f := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
route := mux.CurrentRoute(r)
|
||||
hpath, _ := route.GetPathTemplate()
|
||||
|
||||
attributes := make([]attribute.KeyValue, 0, 8)
|
||||
attributes = append(
|
||||
attributes,
|
||||
semconv.HTTPRequestMethodOriginal(r.Method),
|
||||
semconv.URLFull(hpath),
|
||||
semconv.URLPath(r.URL.Path),
|
||||
)
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
if wr, ok := w.(*customResponseWriter); ok {
|
||||
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
|
||||
attributes = append(attributes,
|
||||
semconv.HTTPResponseStatusCode(statusCode),
|
||||
semconv.HTTPResponseBodySize(wr.wroteBytes),
|
||||
)
|
||||
}
|
||||
elapsed := time.Since(start).Truncate(time.Millisecond)
|
||||
requestDuration.Record(
|
||||
r.Context(), elapsed.Seconds(), metric.WithAttributes(attributes...))
|
||||
})
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func middlewareTrace() mux.MiddlewareFunc {
|
||||
reqidAttr := attributeStringKey("http.request_id")
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
reqid := xcontext.GetRequestID(ctx)
|
||||
|
||||
var span trace.Span
|
||||
route := mux.CurrentRoute(r)
|
||||
hpath, _ := route.GetPathTemplate()
|
||||
ctx, span = webtracer.Start(
|
||||
ctx, r.Method+" "+hpath,
|
||||
trace.WithAttributes(
|
||||
reqidAttr.Value(reqid),
|
||||
semconv.HTTPRequestMethodOriginal(r.Method),
|
||||
semconv.URLFull(hpath),
|
||||
semconv.URLPath(r.URL.Path),
|
||||
semconv.URLQuery(r.URL.RawQuery),
|
||||
semconv.UserAgentOriginal(r.UserAgent()),
|
||||
),
|
||||
trace.WithSpanKind(trace.SpanKindServer),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
if wr, ok := w.(*customResponseWriter); ok {
|
||||
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
|
||||
span.SetAttributes(
|
||||
semconv.HTTPResponseStatusCode(statusCode),
|
||||
semconv.HTTPResponseBodySize(wr.wroteBytes),
|
||||
)
|
||||
if statusCode > 399 {
|
||||
span.SetStatus(codes.Error, "error during request")
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "request completed")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func middlewareLogger(log *slog.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
requestID := r.Header.Get("x-request-id")
|
||||
if requestID == "" {
|
||||
requestID = generator.RandomInt64ID()
|
||||
}
|
||||
|
||||
ctx = xcontext.WithLogFields(
|
||||
ctx,
|
||||
slog.String("request_id", requestID),
|
||||
)
|
||||
ctx = xcontext.WithRequestID(ctx, requestID)
|
||||
|
||||
xcontext.LogInfo(
|
||||
ctx, log, "incoming request",
|
||||
slog.String("method", r.Method),
|
||||
slog.String("path", r.URL.Path),
|
||||
)
|
||||
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
elapsed := slog.Duration("elapsed", time.Since(start).Truncate(time.Millisecond))
|
||||
|
||||
logfields := make([]slog.Attr, 0, 3)
|
||||
logfields = append(logfields, elapsed)
|
||||
if wr, ok := w.(*customResponseWriter); ok {
|
||||
statusCode := xdefault.WithFallback(wr.statusCode, http.StatusOK)
|
||||
logfields = append(
|
||||
logfields,
|
||||
slog.Int("status_code", statusCode),
|
||||
slog.Int("bytes_wrote", wr.wroteBytes),
|
||||
)
|
||||
}
|
||||
|
||||
xcontext.LogInfo(ctx, log, "request processed", logfields...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func wrapWithCustomWriter(origin http.ResponseWriter) *customResponseWriter {
|
||||
return &customResponseWriter{
|
||||
ResponseWriter: origin,
|
||||
}
|
||||
}
|
||||
|
||||
type customResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
statusCode int
|
||||
wroteBytes int
|
||||
}
|
||||
|
||||
func (w *customResponseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (w *customResponseWriter) Write(data []byte) (n int, err error) {
|
||||
n, err = w.ResponseWriter.Write(data)
|
||||
w.wroteBytes += n
|
||||
|
||||
return n, err
|
||||
}
|
||||
147
cmd/kuriweb/main.go
Normal file
147
cmd/kuriweb/main.go
Normal file
@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||
xhttp "git.loyso.art/frx/kurious/internal/kurious/ports/http"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
err := app(ctx)
|
||||
if err != nil {
|
||||
println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func app(ctx context.Context) error {
|
||||
var cfgpath string
|
||||
if len(os.Args) > 1 {
|
||||
cfgpath = os.Args[1]
|
||||
} else {
|
||||
cfgpath = "config.json"
|
||||
}
|
||||
|
||||
cfg, err := readFromFile(cfgpath, defaultConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading config from file: %w", err)
|
||||
}
|
||||
|
||||
log := config.NewSLogger(cfg.Log)
|
||||
|
||||
shutdownOtel, err := setupOtelSDK(ctx, cfg.Tracing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up otel sdk: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := shutdownOtel(ctx)
|
||||
if err != nil {
|
||||
xcontext.LogWithError(ctx, log, err, "shutting down sdk")
|
||||
}
|
||||
}()
|
||||
|
||||
sravniClient, err := sravni.NewClient(ctx, log, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to make new sravni client: %w", err)
|
||||
}
|
||||
|
||||
mainPageState, err := sravniClient.GetMainPageState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting main page state: %w", err)
|
||||
}
|
||||
|
||||
dictionaries := mainPageState.Props.InitialReduxState.Dictionaries.Data
|
||||
|
||||
dictFieldsAsMap := func(fields ...sravni.Field) map[string]string {
|
||||
out := make(map[string]string, len(fields))
|
||||
for _, field := range fields {
|
||||
out[field.Value] = field.Name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
courseThematcisMapped := dictFieldsAsMap(dictionaries.CourseThematics.Fields...)
|
||||
learningTypeMapped := dictFieldsAsMap(dictionaries.LearningType.Fields...)
|
||||
|
||||
mapper := adapters.NewMemoryMapper(courseThematcisMapped, learningTypeMapped)
|
||||
|
||||
var dbengine service.RepositoryEngine
|
||||
switch cfg.DBEngine {
|
||||
case "ydb":
|
||||
dbengine = service.RepositoryEngineYDB
|
||||
case "sqlite":
|
||||
dbengine = service.RepositoryEngineSqlite
|
||||
default:
|
||||
dbengine = service.RepositoryEngineUnknown
|
||||
}
|
||||
app, err := service.NewApplication(ctx, service.ApplicationConfig{
|
||||
LogConfig: cfg.Log,
|
||||
YDB: cfg.YDB,
|
||||
Sqlite: cfg.Sqlite,
|
||||
Engine: dbengine,
|
||||
}, mapper)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making new application: %w", err)
|
||||
}
|
||||
|
||||
httpAPI := xhttp.NewServer(app, log.With(slog.String("component", "http")))
|
||||
httpServer := setupHTTP(cfg.HTTP, httpAPI, log)
|
||||
|
||||
eg, egctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
xcontext.LogInfo(
|
||||
ctx, log, "serving http",
|
||||
slog.String("addr", httpServer.Addr),
|
||||
)
|
||||
|
||||
if err := httpServer.ListenAndServe(); err != nil {
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("listening http: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
<-egctx.Done()
|
||||
|
||||
xcontext.LogInfo(ctx, log, "trying to shutdown http")
|
||||
|
||||
sdctx, sdcancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer sdcancel()
|
||||
|
||||
err := httpServer.Shutdown(sdctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down the server: %w", err)
|
||||
}
|
||||
|
||||
xcontext.LogInfo(ctx, log, "server closed successfuly")
|
||||
|
||||
err = shutdownOtel(sdctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down sdk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
243
cmd/kuriweb/trace.go
Normal file
243
cmd/kuriweb/trace.go
Normal file
@ -0,0 +1,243 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"google.golang.org/grpc/encoding/gzip"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||
)
|
||||
|
||||
var (
|
||||
webtracer = otel.Tracer("kuriweb.http")
|
||||
webmetric = otel.Meter("kuriweb.http")
|
||||
)
|
||||
|
||||
type shutdownFunc func(context.Context) error
|
||||
|
||||
func setupOtelSDK(ctx context.Context, cfg config.Trace) (shutdown shutdownFunc, err error) {
|
||||
var shutdownFuncs []shutdownFunc
|
||||
|
||||
shutdown = func(ctx context.Context) error {
|
||||
var err error
|
||||
for _, f := range shutdownFuncs {
|
||||
err = errors.Join(err, f(ctx))
|
||||
}
|
||||
shutdownFuncs = nil
|
||||
return err
|
||||
}
|
||||
|
||||
handleError := func(inErr error) error {
|
||||
err = errors.Join(inErr, shutdown(ctx))
|
||||
return err
|
||||
}
|
||||
|
||||
resource, err := makeServiceResource(ctx)
|
||||
if err != nil {
|
||||
return shutdown, fmt.Errorf("making service resource: %w", err)
|
||||
}
|
||||
|
||||
prop := newPropagator()
|
||||
otel.SetTextMapPropagator(prop)
|
||||
|
||||
tracerProvider, err := newCommonTraceProvider(ctx, TraceProviderParams{
|
||||
Endpoint: cfg.Endpoint,
|
||||
Type: cfg.Type,
|
||||
AuthHeader: cfg.APIHeader,
|
||||
APIKey: cfg.APIKey,
|
||||
}, resource)
|
||||
if err != nil {
|
||||
return nil, handleError(err)
|
||||
}
|
||||
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
||||
otel.SetTracerProvider(tracerProvider)
|
||||
|
||||
if cfg.ShowMetrics {
|
||||
meterProvider, err := newCommonMeterProvider(ctx, meterProviderParams{
|
||||
Endpoint: cfg.Endpoint,
|
||||
Type: cfg.Type,
|
||||
AuthHeaderKey: cfg.APIHeader,
|
||||
AuthHeaderValue: cfg.APIKey,
|
||||
ReadInterval: time.Second * 15,
|
||||
}, resource)
|
||||
if err != nil {
|
||||
return nil, handleError(err)
|
||||
}
|
||||
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
|
||||
otel.SetMeterProvider(meterProvider)
|
||||
}
|
||||
|
||||
return shutdown, nil
|
||||
}
|
||||
|
||||
func newPropagator() propagation.TextMapPropagator {
|
||||
return propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
)
|
||||
}
|
||||
|
||||
type TraceProviderParams struct {
|
||||
Endpoint string
|
||||
APIKey string
|
||||
AuthHeader string
|
||||
Type config.TraceClientType
|
||||
}
|
||||
|
||||
func makeServiceResource(ctx context.Context) (*resource.Resource, error) {
|
||||
r, err := resource.New(
|
||||
ctx,
|
||||
resource.WithDetectors(
|
||||
resource.StringDetector(semconv.SchemaURL, semconv.ServiceNameKey, func() (string, error) {
|
||||
return "bigstats:kuriweb", nil
|
||||
}),
|
||||
),
|
||||
resource.WithHost(),
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName("bigstats:kuriweb"),
|
||||
semconv.DeploymentEnvironment("production"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making new resource: %w", err)
|
||||
}
|
||||
return resource.Merge(resource.Default(), r)
|
||||
}
|
||||
|
||||
func newCommonTraceProvider(ctx context.Context, params TraceProviderParams, r *resource.Resource) (tp *trace.TracerProvider, err error) {
|
||||
opts := make([]trace.TracerProviderOption, 0, 4)
|
||||
opts = append(
|
||||
opts,
|
||||
trace.WithSampler(trace.AlwaysSample()),
|
||||
trace.WithResource(r),
|
||||
)
|
||||
|
||||
if params.Type != config.TraceClientTypeUnset {
|
||||
var spanExporter trace.SpanExporter
|
||||
var headers map[string]string
|
||||
|
||||
if params.AuthHeader != "" {
|
||||
headers = make(map[string]string, 1)
|
||||
headers[params.AuthHeader] = params.APIKey
|
||||
}
|
||||
|
||||
switch params.Type {
|
||||
case config.TraceClientTypeGRPC:
|
||||
spanExporter, err = otlptracegrpc.New(
|
||||
ctx,
|
||||
otlptracegrpc.WithEndpointURL(params.Endpoint),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
otlptracegrpc.WithHeaders(headers),
|
||||
otlptracegrpc.WithCompressor(gzip.Name),
|
||||
)
|
||||
case config.TraceClientTypeHTTP:
|
||||
httpClient := otlptracehttp.NewClient(
|
||||
otlptracehttp.WithEndpointURL(params.Endpoint),
|
||||
otlptracehttp.WithHeaders(headers),
|
||||
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
||||
)
|
||||
spanExporter, err = otlptrace.New(
|
||||
ctx, httpClient,
|
||||
)
|
||||
case config.TraceClientTypeStdout:
|
||||
spanExporter, err = stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provider type")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making trace exporter: %w", err)
|
||||
}
|
||||
|
||||
opts = append(opts, trace.WithBatcher(spanExporter))
|
||||
}
|
||||
|
||||
tp = trace.NewTracerProvider(opts...)
|
||||
|
||||
return tp, nil
|
||||
}
|
||||
|
||||
type meterProviderParams struct {
|
||||
Endpoint string
|
||||
AuthHeaderKey string
|
||||
AuthHeaderValue string
|
||||
ReadInterval time.Duration
|
||||
Type config.TraceClientType
|
||||
}
|
||||
|
||||
func newCommonMeterProvider(ctx context.Context, params meterProviderParams, r *resource.Resource) (*metric.MeterProvider, error) {
|
||||
var exporter metric.Exporter
|
||||
var err error
|
||||
|
||||
switch params.Type {
|
||||
case config.TraceClientTypeGRPC:
|
||||
headers := make(map[string]string, 1)
|
||||
if params.AuthHeaderKey != "" {
|
||||
headers[params.AuthHeaderKey] = params.AuthHeaderValue
|
||||
}
|
||||
|
||||
exporter, err = otlpmetricgrpc.New(
|
||||
ctx,
|
||||
otlpmetricgrpc.WithEndpointURL(params.Endpoint),
|
||||
otlpmetricgrpc.WithHeaders(headers),
|
||||
otlpmetricgrpc.WithCompressor(gzip.Name),
|
||||
otlpmetricgrpc.WithTemporalitySelector(preferDeltaTemporalitySelector),
|
||||
otlpmetricgrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making grpc exporter: %w", err)
|
||||
}
|
||||
|
||||
case config.TraceClientTypeStdout:
|
||||
exporter, err = stdoutmetric.New()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making stdout exporter: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
reader := metric.NewPeriodicReader(
|
||||
exporter,
|
||||
metric.WithInterval(params.ReadInterval),
|
||||
)
|
||||
provider := metric.NewMeterProvider(
|
||||
metric.WithReader(reader),
|
||||
metric.WithResource(r),
|
||||
)
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route {
|
||||
// h := otelhttp.WithRouteTag(path, hf)
|
||||
return router.Handle(path, hf).Name(name)
|
||||
}
|
||||
|
||||
func preferDeltaTemporalitySelector(kind metric.InstrumentKind) metricdata.Temporality {
|
||||
switch kind {
|
||||
case metric.InstrumentKindCounter,
|
||||
metric.InstrumentKindObservableCounter,
|
||||
metric.InstrumentKindHistogram:
|
||||
return metricdata.DeltaTemporality
|
||||
default:
|
||||
return metricdata.CumulativeTemporality
|
||||
}
|
||||
}
|
||||
72
go.mod
72
go.mod
@ -1,37 +1,65 @@
|
||||
module git.loyso.art/frx/kurious
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/go-resty/resty/v2 v2.10.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/a-h/templ v0.2.707
|
||||
github.com/go-resty/resty/v2 v2.13.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/teris-io/cli v1.0.1
|
||||
golang.org/x/net v0.18.0
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.74.3
|
||||
github.com/ydb-platform/ydb-go-yc v0.12.1
|
||||
go.opentelemetry.io/otel v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0
|
||||
go.opentelemetry.io/otel/metric v1.30.0
|
||||
go.opentelemetry.io/otel/sdk v1.30.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0
|
||||
go.opentelemetry.io/otel/trace v1.30.0
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/grpc v1.66.1
|
||||
modernc.org/sqlite v1.30.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 // indirect
|
||||
github.com/ydb-platform/ydb-go-yc v0.12.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20240529120826-df2b24336f42 // indirect
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 // indirect
|
||||
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
|
||||
modernc.org/libc v1.53.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
||||
|
||||
186
go.sum
186
go.sum
@ -509,12 +509,16 @@ cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vf
|
||||
cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA=
|
||||
cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
|
||||
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
|
||||
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
@ -525,6 +529,8 @@ github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0I
|
||||
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||
@ -554,6 +560,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@ -572,8 +580,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
@ -584,12 +590,18 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo=
|
||||
github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
|
||||
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
|
||||
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
@ -627,7 +639,6 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
@ -649,6 +660,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@ -669,11 +682,13 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
@ -691,14 +706,22 @@ github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMd
|
||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||
@ -715,17 +738,28 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
|
||||
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
|
||||
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
@ -741,13 +775,19 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/rekby/fixenv v0.3.2/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
|
||||
github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=
|
||||
github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@ -756,8 +796,9 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
|
||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@ -767,21 +808,21 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/teris-io/cli v1.0.1 h1:J6jnVHC552uqx7zT+Ux0++tIvLmJQULqxVhCid2u/Gk=
|
||||
github.com/teris-io/cli v1.0.1/go.mod h1:V9nVD5aZ873RU/tQXLSXO8FieVPQhQvuNohsdsKXsGw=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20211115083454-9ca41db5ed9e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 h1:xCzizLC090MiLWEV3aziL5YIKrSVTRXX2DXlRGeQ6sA=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20240529120826-df2b24336f42 h1:l5Wu1kRcM34HqBR2FZI6tWc6QKyPziNj5fGZ4eXTCRI=
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20240529120826-df2b24336f42/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20221215182650-986f9d10542f/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20230528143953-42c825ace222/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd h1:dzWP1Lu+A40W883dK/Mr3xyDSM/2MggS8GtHT0qgAnE=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7 h1:nL8XwD6fSst7xFUirkaWJmE7kM0CdWRYgu6+YQer1d4=
|
||||
github.com/ydb-platform/ydb-go-genproto v0.0.0-20240528144234-5d5a685e41f7/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.44.0/go.mod h1:oSLwnuilwIpaF5bJJMAofnGgzPJusoI3zWMNb8I+GnM=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.47.3/go.mod h1:bWnOIcUHd7+Sl7DN+yhyY1H/I61z53GczvwJgXMgvj0=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2 h1:E0yUuuX7UmPxXm92+yQCjMveLFO3zfvYFIJVuAqsVRA=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2/go.mod h1:fjBLQ2TdQNl4bMjuWl9adoTGBypwUTPoGC+EqYqiIcU=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.74.3 h1:QHjqt1GA/f/O6kF5MNjmnSRG/sK1fmQivIFs1PeUgeo=
|
||||
github.com/ydb-platform/ydb-go-sdk/v3 v3.74.3/go.mod h1:ZXl2InwGFiZKojWCsj6tW/SGFbch7zaehQWX5CjXMEI=
|
||||
github.com/ydb-platform/ydb-go-yc v0.12.1 h1:qw3Fa+T81+Kpu5Io2vYHJOwcrYrVjgJlT6t/0dOXJrA=
|
||||
github.com/ydb-platform/ydb-go-yc v0.12.1/go.mod h1:t/ZA4ECdgPWjAb4jyDe8AzQZB5dhpGbi3iCahFaNwBY=
|
||||
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 h1:9E5q8Nsy2RiJMZDNVy0A3KUrIMBPakJ2VgloeWbcI84=
|
||||
@ -803,9 +844,37 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0 h1:WypxHH02KX2poqqbaadmkMYalGyy/vil4HE4PM4nRJc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0/go.mod h1:U79SV99vtvGSEBeeHnpgGJfTsnsdkWLpPN/CcHAzBSI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0 h1:/jlt1Y8gXWiHG9FBx6cJaIC5hYx5Fe64nC8w5Cylt/0=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.27.0/go.mod h1:bmToOGOBZ4hA9ghphIc1PAf66VA8KOtsuy3+ScStG20=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 h1:/0YaXu3755A/cFbtXp+21lkXgI0QE5avTWA2HjU9/WE=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0/go.mod h1:m7SFxp0/7IxmJPLIY3JhOcU9CoFzDaCPL6xxQIxhA+o=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792jZO1bo4BXkM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
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=
|
||||
@ -815,7 +884,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -873,6 +943,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -930,9 +1002,10 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -977,8 +1050,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -1056,9 +1129,10 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
@ -1067,7 +1141,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1084,16 +1159,15 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -1157,6 +1231,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -1360,12 +1436,10 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
|
||||
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
|
||||
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg=
|
||||
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@ -1406,8 +1480,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA
|
||||
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
|
||||
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@ -1425,9 +1499,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
@ -1452,13 +1525,23 @@ lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl
|
||||
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
|
||||
modernc.org/cc/v4 v4.21.3 h1:2mhBdWKtivdFlLR1ecKXTljPG1mfvbByX7QKztAIJl8=
|
||||
modernc.org/cc/v4 v4.21.3/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
|
||||
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
|
||||
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
|
||||
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
|
||||
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
|
||||
modernc.org/ccgo/v4 v4.18.1 h1:1zF5kPBFq/ZVTulBOKgQPQITdOzzyBUfC51gVYP62E4=
|
||||
modernc.org/ccgo/v4 v4.18.1/go.mod h1:ao1fAxf9a2KEOL15WY8+yP3wnpaOpP/QuyFOZ9HJolM=
|
||||
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
|
||||
@ -1467,19 +1550,34 @@ modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
|
||||
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
|
||||
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
|
||||
modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s=
|
||||
modernc.org/libc v1.53.3 h1:9O0aSLZuHPgp49we24NoFFteRgXNLGBAQ3TODrW3XLg=
|
||||
modernc.org/libc v1.53.3/go.mod h1:kb+Erju4FfHNE59xd2fNpv5CBeAeej6fHbx8p8xaiyI=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
|
||||
modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4=
|
||||
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
|
||||
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
0
htmlexamples/assets/style.css
Normal file
0
htmlexamples/assets/style.css
Normal file
152
htmlexamples/core.html
Normal file
152
htmlexamples/core.html
Normal file
@ -0,0 +1,152 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Test page</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body data-bs-theme="dark">
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Kurious</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link" aria-current="page" href="/index.html">Home</a>
|
||||
<a class="nav-link" href="/courses.html">Courses</a>
|
||||
<a class="nav-link active" href="/core.html">About us</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<nav style="--bs-breadcrumb-divider: '>'" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="#">Home</a></li>
|
||||
<li class="breadcrumb-item" aria-current="page">
|
||||
<a href="#">Course</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Theme</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<!-- <a href="#" class="btn btn-primary">Go somewhere</a> -->
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<a href="#" class="btn btn-primary">Buy for 399.99$</a>
|
||||
<small class="text-body-secondary"></small>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<small class="text-body-secondary col">399.99$</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<small class="text-body-secondary">399.99$</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<small class="text-body-secondary">399.99$</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<small class="text-body-secondary">399.99$</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<img src="https://placehold.co/128x128" class="card-img-top" alt="" />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim
|
||||
omnis vero, reiciendis obcaecati perferendis excepturi nostrum
|
||||
nobis itaque modi dignissimos ...
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<small class="text-body-secondary">399.99$</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
277
htmlexamples/courses.html
Normal file
277
htmlexamples/courses.html
Normal file
@ -0,0 +1,277 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test page</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
|
||||
<body data-bs-theme="dark">
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/index.html">Kurious</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mb-lg-0 mb-2 me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="/index.html"
|
||||
>Home</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link active"
|
||||
aria-current="page"
|
||||
href="/courses.html"
|
||||
>Courses</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/core.html">About us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<section class="row header">
|
||||
<nav
|
||||
class="mt-4"
|
||||
style="--bs-breadcrumb-divider: ">""
|
||||
aria-label="breadcrumb"
|
||||
>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="#">Main</a></li>
|
||||
<li class="breadcrumb-item" aria-current="page">
|
||||
<a href="#">Languages</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Japanese</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</section>
|
||||
|
||||
<section class="row filters">
|
||||
<div class="col-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Filter categories</span>
|
||||
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<option selected>All</option>
|
||||
<option value="1">Programming</option>
|
||||
<option value="2">Design</option>
|
||||
<option value="3">Business</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<option selected>All</option>
|
||||
<option value="1">Web development</option>
|
||||
<option value="2">Backend</option>
|
||||
<option value="3">Frontend</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
> Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="filter-content d-flex mb-3">
|
||||
<!-- School list -->
|
||||
<div class="p-2 col-auto">
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<option selected>Pick a school</option>
|
||||
<option value="1">First school in the row</option>
|
||||
<option value="2">Second but not the shortest named school</option>
|
||||
<option value="3">Third small</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Sort option -->
|
||||
<div class="col-auto">
|
||||
<div class="input-group flex-nowrap p-2">
|
||||
<select
|
||||
class="form-select"
|
||||
id="inputGroupSelect04"
|
||||
aria-label="Example select with button addon"
|
||||
>
|
||||
<option selected>Sort by</option>
|
||||
<option value="1">One</option>
|
||||
<option value="2">Two</option>
|
||||
<option value="3">Three</option>
|
||||
<option value="4">Threerrrrrrrrrrrrrrrrrr</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check p-2"
|
||||
name="options-base"
|
||||
id="option6"
|
||||
autocomplete="off"
|
||||
checked
|
||||
/>
|
||||
<label class="btn" for="option6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-sort-down"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check p-2"
|
||||
name="options-base"
|
||||
id="option5"
|
||||
autocomplete="off"
|
||||
checked
|
||||
/>
|
||||
<label class="btn" for="option5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-sort-up"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M3.5 12.5a.5.5 0 0 1-1 0V3.707L1.354 4.854a.5.5 0 1 1-.708-.708l2-1.999.007-.007a.5.5 0 0 1 .7.006l2 2a.5.5 0 1 1-.707.708L3.5 3.707zm3.5-9a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spacer -->
|
||||
<div class="col-4"></div>
|
||||
<!-- Promocodes button -->
|
||||
<div class="col-auto ms-auto p-2">
|
||||
<div class="btn btn-primary">Promocodes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<img
|
||||
src="https://placehold.co/128x128"
|
||||
class="card-img-top"
|
||||
alt="..."
|
||||
/>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Card title with a long naming</h5>
|
||||
<div class="input-group d-flex">
|
||||
<a href="#" class="btn text btn-outline-primary flex-grow-1"
|
||||
>Open ></a
|
||||
>
|
||||
<span class="input-group-text justify-content-end flex-fill"
|
||||
>500$</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maybe add spacing in a better way? -->
|
||||
<p class="p-2"></p>
|
||||
<footer class="text-center text-lg-start bg-body-tertiary text-muted">
|
||||
<section class="p-2">
|
||||
<div class="container text-center text-md-start mt-4">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-3 col-lg-4 col-xl-3 mx-auto mb-4">
|
||||
<h6 class="text-uppercase fw-bold mb-4">
|
||||
<i class="fas fa-gem me-3"></i>Courses
|
||||
</h6>
|
||||
<p>
|
||||
Welcome to Courses, your gateway to learning! Explore a diverse
|
||||
range of courses and advance your skills with us. Join our
|
||||
community and transform your life through education.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mb-4">
|
||||
<h6 class="text-uppercase fw-bold mb-4">Useful links</h6>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Pricing</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Settings</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Orders</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Help</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4">
|
||||
<h6 class="text-uppercase fw-bold mb-4">Contact</h6>
|
||||
<p><i class="fas fa-home me-3"></i> New York, NY 10012, US</p>
|
||||
<p>
|
||||
<i class="fas fa-envelope me-3"></i>
|
||||
info@example.com
|
||||
</p>
|
||||
<p><i class="fas fa-phone me-3"></i> + 01 234 567 88</p>
|
||||
<p><i class="fas fa-print me-3"></i> + 01 234 567 89</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
class="text-center p-4"
|
||||
style="background-color: rgba(0, 0, 0, 0.05)"
|
||||
>
|
||||
© 2024 Copyright:
|
||||
<a class="text-reset fw-bold" href="https://mdbootstrap.com/"
|
||||
>kursov.net</a
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
151
htmlexamples/index.html
Normal file
151
htmlexamples/index.html
Normal file
@ -0,0 +1,151 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Test page</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
</head>
|
||||
|
||||
<body data-bs-theme="dark" style="margin: 0">
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/index.html">Kurious</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mb-lg-0 mb-2 me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/index.html">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" aria-current="page" href="/courses.html">Courses</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/core.html">About us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div class="row upper mb-4 text-center" style="min-height: 4rem">
|
||||
<p class="justify-content-center">Here you can find course for any taste</p>
|
||||
</div>
|
||||
|
||||
<div class="container w-75 mb-4">
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<div class="col-12 col-md-8 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<ul>
|
||||
<li><span class="d-inline-block text-truncate col-8">web-development</span></li>
|
||||
<li>backend development</li>
|
||||
<li>frontend development</li>
|
||||
</ul>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Programming</h5>
|
||||
<hr/>
|
||||
<p>In this category you can find courses of types such as</p>
|
||||
<p>web-development, backend development, frontend developent</p>
|
||||
<p>This category contains <span>128</span> courses.</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">128 items</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link">Previous</a>
|
||||
</li>
|
||||
<li class="page-item active"><a class="page-link" href="#">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">3</a></li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/pkg/slices"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
@ -31,7 +31,7 @@ type Client interface {
|
||||
ListEducationalProducts(
|
||||
ctx context.Context,
|
||||
params ListEducationProductsParams,
|
||||
) (result listEducationProductsResponse, err error)
|
||||
) (result ListEducationProductsResponse, err error)
|
||||
ListEducationalProductsFilterCount(
|
||||
ctx context.Context,
|
||||
params ListEducationProductsParams,
|
||||
@ -52,8 +52,8 @@ func NewClient(ctx context.Context, log *slog.Logger, debug bool) (c *client, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
getQuerySet := func(fields []field) querySet {
|
||||
items := slices.Map(fields, func(f field) string {
|
||||
getQuerySet := func(fields []Field) querySet {
|
||||
items := xslices.Map(fields, func(f Field) string {
|
||||
return f.Value
|
||||
})
|
||||
|
||||
@ -177,7 +177,7 @@ type listEducationProductsRequest struct {
|
||||
SortDirection string `json:"sortDirection"`
|
||||
}
|
||||
|
||||
type listEducationProductsResponse struct {
|
||||
type ListEducationProductsResponse struct {
|
||||
Items []Course `json:"items"`
|
||||
Organizations map[string]Organization `json:"organizations"`
|
||||
|
||||
@ -188,7 +188,7 @@ type listEducationProductsResponse struct {
|
||||
func (c *client) ListEducationalProducts(
|
||||
ctx context.Context,
|
||||
params ListEducationProductsParams,
|
||||
) (result listEducationProductsResponse, err error) {
|
||||
) (result ListEducationProductsResponse, err error) {
|
||||
const urlPath = "/v1/education/products"
|
||||
const defaultLimit = 1
|
||||
const defaultSortProp = "advertising.position"
|
||||
|
||||
@ -39,7 +39,7 @@ type ReduxMetadata struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type field struct {
|
||||
type Field struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@ -51,7 +51,7 @@ type ReduxDictionaryContainer struct {
|
||||
UserID string `json:"userId"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Fields []field `json:"fields"`
|
||||
Fields []Field `json:"fields"`
|
||||
}
|
||||
|
||||
type ReduxDictionaries struct {
|
||||
|
||||
208
internal/common/client/sravni/mocks/Client.go
Normal file
208
internal/common/client/sravni/mocks/Client.go
Normal file
@ -0,0 +1,208 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
sravni "git.loyso.art/frx/kurious/internal/common/client/sravni"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Client is an autogenerated mock type for the Client type
|
||||
type Client struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Client_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Client) EXPECT() *Client_Expecter {
|
||||
return &Client_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetMainPageState provides a mock function with given fields:
|
||||
func (_m *Client) GetMainPageState() (*sravni.PageState, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetMainPageState")
|
||||
}
|
||||
|
||||
var r0 *sravni.PageState
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (*sravni.PageState, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() *sravni.PageState); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*sravni.PageState)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Client_GetMainPageState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMainPageState'
|
||||
type Client_GetMainPageState_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetMainPageState is a helper method to define mock.On call
|
||||
func (_e *Client_Expecter) GetMainPageState() *Client_GetMainPageState_Call {
|
||||
return &Client_GetMainPageState_Call{Call: _e.mock.On("GetMainPageState")}
|
||||
}
|
||||
|
||||
func (_c *Client_GetMainPageState_Call) Run(run func()) *Client_GetMainPageState_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_GetMainPageState_Call) Return(_a0 *sravni.PageState, _a1 error) *Client_GetMainPageState_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_GetMainPageState_Call) RunAndReturn(run func() (*sravni.PageState, error)) *Client_GetMainPageState_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListEducationalProducts provides a mock function with given fields: ctx, params
|
||||
func (_m *Client) ListEducationalProducts(ctx context.Context, params sravni.ListEducationProductsParams) (sravni.ListEducationProductsResponse, error) {
|
||||
ret := _m.Called(ctx, params)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListEducationalProducts")
|
||||
}
|
||||
|
||||
var r0 sravni.ListEducationProductsResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) (sravni.ListEducationProductsResponse, error)); ok {
|
||||
return rf(ctx, params)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) sravni.ListEducationProductsResponse); ok {
|
||||
r0 = rf(ctx, params)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sravni.ListEducationProductsResponse)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, sravni.ListEducationProductsParams) error); ok {
|
||||
r1 = rf(ctx, params)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Client_ListEducationalProducts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListEducationalProducts'
|
||||
type Client_ListEducationalProducts_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListEducationalProducts is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - params sravni.ListEducationProductsParams
|
||||
func (_e *Client_Expecter) ListEducationalProducts(ctx interface{}, params interface{}) *Client_ListEducationalProducts_Call {
|
||||
return &Client_ListEducationalProducts_Call{Call: _e.mock.On("ListEducationalProducts", ctx, params)}
|
||||
}
|
||||
|
||||
func (_c *Client_ListEducationalProducts_Call) Run(run func(ctx context.Context, params sravni.ListEducationProductsParams)) *Client_ListEducationalProducts_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(sravni.ListEducationProductsParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_ListEducationalProducts_Call) Return(result sravni.ListEducationProductsResponse, err error) *Client_ListEducationalProducts_Call {
|
||||
_c.Call.Return(result, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_ListEducationalProducts_Call) RunAndReturn(run func(context.Context, sravni.ListEducationProductsParams) (sravni.ListEducationProductsResponse, error)) *Client_ListEducationalProducts_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListEducationalProductsFilterCount provides a mock function with given fields: ctx, params
|
||||
func (_m *Client) ListEducationalProductsFilterCount(ctx context.Context, params sravni.ListEducationProductsParams) (sravni.ProductsFilterCount, error) {
|
||||
ret := _m.Called(ctx, params)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListEducationalProductsFilterCount")
|
||||
}
|
||||
|
||||
var r0 sravni.ProductsFilterCount
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) (sravni.ProductsFilterCount, error)); ok {
|
||||
return rf(ctx, params)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, sravni.ListEducationProductsParams) sravni.ProductsFilterCount); ok {
|
||||
r0 = rf(ctx, params)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sravni.ProductsFilterCount)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, sravni.ListEducationProductsParams) error); ok {
|
||||
r1 = rf(ctx, params)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Client_ListEducationalProductsFilterCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListEducationalProductsFilterCount'
|
||||
type Client_ListEducationalProductsFilterCount_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListEducationalProductsFilterCount is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - params sravni.ListEducationProductsParams
|
||||
func (_e *Client_Expecter) ListEducationalProductsFilterCount(ctx interface{}, params interface{}) *Client_ListEducationalProductsFilterCount_Call {
|
||||
return &Client_ListEducationalProductsFilterCount_Call{Call: _e.mock.On("ListEducationalProductsFilterCount", ctx, params)}
|
||||
}
|
||||
|
||||
func (_c *Client_ListEducationalProductsFilterCount_Call) Run(run func(ctx context.Context, params sravni.ListEducationProductsParams)) *Client_ListEducationalProductsFilterCount_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(sravni.ListEducationProductsParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_ListEducationalProductsFilterCount_Call) Return(result sravni.ProductsFilterCount, err error) *Client_ListEducationalProductsFilterCount_Call {
|
||||
_c.Call.Return(result, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Client_ListEducationalProductsFilterCount_Call) RunAndReturn(run func(context.Context, sravni.ListEducationProductsParams) (sravni.ProductsFilterCount, error)) *Client_ListEducationalProductsFilterCount_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Client {
|
||||
mock := &Client{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@ -12,6 +12,6 @@ func (NoopClient) GetMainPageState() (*PageState, error) {
|
||||
return nil, errors.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (listEducationProductsResponse, error) {
|
||||
return listEducationProductsResponse{}, errors.ErrNotImplemented
|
||||
func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (ListEducationProductsResponse, error) {
|
||||
return ListEducationProductsResponse{}, errors.ErrNotImplemented
|
||||
}
|
||||
|
||||
@ -2,4 +2,5 @@ package config
|
||||
|
||||
type HTTP struct {
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
MountLive bool `json:"mount_live"`
|
||||
}
|
||||
|
||||
8
internal/common/config/sqlite.go
Normal file
8
internal/common/config/sqlite.go
Normal file
@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
type Sqlite struct {
|
||||
DSN string `json:"dsn"`
|
||||
ShutdownTimeout time.Duration `json:"shutdown_timeout"`
|
||||
}
|
||||
38
internal/common/config/trace.go
Normal file
38
internal/common/config/trace.go
Normal file
@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import "errors"
|
||||
|
||||
type TraceClientType uint8
|
||||
|
||||
const (
|
||||
TraceClientTypeUnset TraceClientType = iota
|
||||
TraceClientTypeHTTP
|
||||
TraceClientTypeGRPC
|
||||
TraceClientTypeStdout
|
||||
)
|
||||
|
||||
func (t *TraceClientType) UnmarshalText(data []byte) error {
|
||||
dataStr := string(data)
|
||||
switch dataStr {
|
||||
case "http":
|
||||
*t = TraceClientTypeHTTP
|
||||
case "grpc":
|
||||
*t = TraceClientTypeGRPC
|
||||
case "stdout":
|
||||
*t = TraceClientTypeStdout
|
||||
case "":
|
||||
default:
|
||||
return errors.New("unsupported value " + dataStr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Trace struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
APIKey string `json:"api_key"`
|
||||
APIHeader string `json:"api_header"`
|
||||
Type TraceClientType `json:"type"`
|
||||
|
||||
ShowMetrics bool `json:"show_metrics"`
|
||||
}
|
||||
@ -27,6 +27,7 @@ type YDB struct {
|
||||
DSN string
|
||||
Auth YCAuth
|
||||
ShutdownDuration time.Duration
|
||||
DebugYDB bool
|
||||
}
|
||||
|
||||
func (ydb *YDB) UnmarshalJSON(data []byte) error {
|
||||
|
||||
@ -2,11 +2,24 @@ package decorator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
nameAttribute = attribute.Key("cq.name")
|
||||
argsAttribute = attribute.Key("cq.args")
|
||||
|
||||
apiTracer = otel.Tracer("api")
|
||||
)
|
||||
|
||||
type commandLoggingDecorator[T any] struct {
|
||||
@ -18,6 +31,17 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
|
||||
handlerName := getTypeName[T]()
|
||||
|
||||
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
|
||||
|
||||
var argsBuilder strings.Builder
|
||||
_ = json.NewEncoder(&argsBuilder).Encode(cmd)
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = apiTracer.Start(ctx, "command "+handlerName)
|
||||
span.SetAttributes(
|
||||
nameAttribute.String(handlerName),
|
||||
argsAttribute.String(argsBuilder.String()),
|
||||
)
|
||||
|
||||
xcontext.LogDebug(ctx, c.log, "executing command")
|
||||
start := time.Now()
|
||||
|
||||
@ -27,7 +51,9 @@ func (c commandLoggingDecorator[T]) Handle(ctx context.Context, cmd T) (err erro
|
||||
xcontext.LogInfo(ctx, c.log, "command executed successfuly", elapsed)
|
||||
} else {
|
||||
xcontext.LogError(ctx, c.log, "command execution failed", elapsed, slog.Any("err", err))
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
return c.base.Handle(ctx, cmd)
|
||||
@ -41,6 +67,17 @@ type queryLoggingDecorator[Q, U any] struct {
|
||||
func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entity U, err error) {
|
||||
handlerName := getTypeName[Q]()
|
||||
ctx = xcontext.WithLogFields(ctx, slog.String("handler", handlerName))
|
||||
|
||||
var argsBuilder strings.Builder
|
||||
_ = json.NewEncoder(&argsBuilder).Encode(query)
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = apiTracer.Start(ctx, "query "+handlerName)
|
||||
span.SetAttributes(
|
||||
nameAttribute.String(handlerName),
|
||||
argsAttribute.String(argsBuilder.String()),
|
||||
)
|
||||
|
||||
xcontext.LogDebug(ctx, q.log, "executing command")
|
||||
start := time.Now()
|
||||
|
||||
@ -50,7 +87,10 @@ func (q queryLoggingDecorator[Q, U]) Handle(ctx context.Context, query Q) (entit
|
||||
xcontext.LogInfo(ctx, q.log, "command executed successfuly", elapsed)
|
||||
} else {
|
||||
xcontext.LogError(ctx, q.log, "command execution failed", elapsed, slog.Any("err", err))
|
||||
span.RecordError(err)
|
||||
}
|
||||
now := time.Now()
|
||||
span.End(trace.WithTimestamp(now))
|
||||
}()
|
||||
|
||||
return q.base.Handle(ctx, query)
|
||||
|
||||
@ -6,6 +6,16 @@ import (
|
||||
)
|
||||
|
||||
type ctxLogKey struct{}
|
||||
type ctxRequestID struct{}
|
||||
|
||||
func WithRequestID(ctx context.Context, requestID string) context.Context {
|
||||
return context.WithValue(ctx, ctxRequestID{}, requestID)
|
||||
}
|
||||
|
||||
func GetRequestID(ctx context.Context) string {
|
||||
reqid, _ := ctx.Value(ctxRequestID{}).(string)
|
||||
return reqid
|
||||
}
|
||||
|
||||
type ctxLogAttrStore struct {
|
||||
attrs []slog.Attr
|
||||
@ -19,19 +29,19 @@ func WithLogFields(ctx context.Context, fields ...slog.Attr) context.Context {
|
||||
}
|
||||
|
||||
func LogDebug(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelDebug, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
log.LogAttrs(ctx, slog.LevelDebug, msg, append(getLogFields(ctx), attrs...)...)
|
||||
}
|
||||
|
||||
func LogInfo(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelInfo, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
log.LogAttrs(ctx, slog.LevelInfo, msg, append(getLogFields(ctx), attrs...)...)
|
||||
}
|
||||
|
||||
func LogWarn(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelWarn, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
log.LogAttrs(ctx, slog.LevelWarn, msg, append(getLogFields(ctx), attrs...)...)
|
||||
}
|
||||
|
||||
func LogError(ctx context.Context, log *slog.Logger, msg string, attrs ...slog.Attr) {
|
||||
log.LogAttrs(ctx, slog.LevelError, msg, append(attrs, getLogFields(ctx)...)...)
|
||||
log.LogAttrs(ctx, slog.LevelError, msg, append(getLogFields(ctx), attrs...)...)
|
||||
}
|
||||
|
||||
func LogWithWarnError(ctx context.Context, log *slog.Logger, err error, msg string, attrs ...slog.Attr) {
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
package xslice
|
||||
|
||||
func ForEach[T any](items []T, f func(T)) {
|
||||
for _, item := range items {
|
||||
f(item)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package xslice
|
||||
package xslices
|
||||
|
||||
func Filter[T any](values []T, f func(T) bool) []T {
|
||||
out := make([]T, 0, len(values))
|
||||
@ -1,9 +1,9 @@
|
||||
package xslice_test
|
||||
package xslices_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
)
|
||||
|
||||
func TestFilterInplace(t *testing.T) {
|
||||
@ -43,7 +43,7 @@ func TestFilterInplace(t *testing.T) {
|
||||
for _, tc := range tt {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotLen := xslice.FilterInplace(tc.in, tc.check)
|
||||
gotLen := xslices.FilterInplace(tc.in, tc.check)
|
||||
if gotLen != tc.expLen {
|
||||
t.Errorf("exp %d got %d", tc.expLen, gotLen)
|
||||
}
|
||||
29
internal/common/xslices/foreach.go
Normal file
29
internal/common/xslices/foreach.go
Normal file
@ -0,0 +1,29 @@
|
||||
package xslices
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func ForEach[T any](items []T, f func(T)) {
|
||||
for _, item := range items {
|
||||
f(item)
|
||||
}
|
||||
}
|
||||
|
||||
func AsMap[T any, U comparable](items []T, f func(T) U) map[U]struct{} {
|
||||
out := make(map[U]struct{}, len(items))
|
||||
ForEach(items, func(in T) {
|
||||
out[f(in)] = struct{}{}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func Shuffle[T any](items []T) {
|
||||
maxnum := big.NewInt(int64(len(items)))
|
||||
for i := range items {
|
||||
swapWith, _ := rand.Int(rand.Reader, maxnum)
|
||||
swapWithIdx := int(swapWith.Int64())
|
||||
items[i], items[swapWithIdx] = items[swapWithIdx], items[i]
|
||||
}
|
||||
}
|
||||
103
internal/common/xslices/lru.go
Normal file
103
internal/common/xslices/lru.go
Normal file
@ -0,0 +1,103 @@
|
||||
package xslices
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewLRU[K comparable, T any](capacity int) *lru[K, T] {
|
||||
return &lru[K, T]{
|
||||
items: make(map[K]*lruNode[K, T], capacity),
|
||||
capacity: capacity,
|
||||
}
|
||||
}
|
||||
|
||||
type lruNode[K comparable, T any] struct {
|
||||
key K
|
||||
value T
|
||||
|
||||
next *lruNode[K, T]
|
||||
prev *lruNode[K, T]
|
||||
}
|
||||
|
||||
type lru[K comparable, T any] struct {
|
||||
items map[K]*lruNode[K, T]
|
||||
length int
|
||||
capacity int
|
||||
|
||||
first *lruNode[K, T]
|
||||
last *lruNode[K, T]
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (l *lru[K, T]) Push(key K, value T) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
node, ok := l.items[key]
|
||||
if ok {
|
||||
l.bumpUnsafe(node)
|
||||
return
|
||||
}
|
||||
|
||||
node = &lruNode[K, T]{
|
||||
key: key,
|
||||
value: value,
|
||||
next: l.first,
|
||||
}
|
||||
|
||||
if l.first != nil {
|
||||
l.first.prev = node
|
||||
}
|
||||
if l.last == nil {
|
||||
l.last = node
|
||||
}
|
||||
|
||||
l.first = node
|
||||
l.items[key] = node
|
||||
|
||||
if l.length == l.capacity && l.last != nil {
|
||||
deletedNode := l.last
|
||||
delete(l.items, deletedNode.key)
|
||||
|
||||
l.last = l.last.prev
|
||||
return
|
||||
}
|
||||
|
||||
l.length++
|
||||
}
|
||||
|
||||
func (l *lru[K, T]) Get(key K) (T, bool) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
node, ok := l.items[key]
|
||||
if !ok {
|
||||
var t T
|
||||
return t, false
|
||||
}
|
||||
|
||||
out := node.value
|
||||
l.bumpUnsafe(node)
|
||||
|
||||
return out, true
|
||||
}
|
||||
|
||||
func (l *lru[K, T]) bumpUnsafe(node *lruNode[K, T]) {
|
||||
if l.first == node {
|
||||
return
|
||||
}
|
||||
|
||||
if node.next != nil {
|
||||
node.next.prev = node.prev
|
||||
}
|
||||
if node.prev != nil {
|
||||
node.prev.next = node.next
|
||||
}
|
||||
|
||||
node.next = l.first
|
||||
l.first.prev = node
|
||||
|
||||
l.first = node
|
||||
node.prev = nil
|
||||
}
|
||||
23
internal/common/xslices/lru_test.go
Normal file
23
internal/common/xslices/lru_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package xslices
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLRU(t *testing.T) {
|
||||
lru := NewLRU[int, string](3)
|
||||
for i := 0; i < 4; i++ {
|
||||
lru.Push(i, "v")
|
||||
}
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
_, found := lru.Get(i)
|
||||
if i == 0 {
|
||||
if found {
|
||||
t.Error("expected value to be flushed out of cache")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected value %d to be found", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package xslice
|
||||
package xslices
|
||||
|
||||
func Map[T, U any](in []T, f func(T) U) []U {
|
||||
out := make([]U, len(in))
|
||||
40
internal/kurious/adapters/adapters.go
Normal file
40
internal/kurious/adapters/adapters.go
Normal file
@ -0,0 +1,40 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var dbTracer = otel.Tracer("adapters.db")
|
||||
|
||||
var (
|
||||
dbSystemAttr = attribute.Key("db.system")
|
||||
dbNameAttr = attribute.Key("db.name")
|
||||
dbStatementAttr = attribute.Key("db.statement")
|
||||
dbOperationAttr = attribute.Key("db.operation")
|
||||
dbTableAttr = attribute.Key("db.sql.table")
|
||||
dbArgumentsAttr = attribute.Key("db.arguments")
|
||||
)
|
||||
|
||||
type domainer[T any] interface {
|
||||
AsDomain() T
|
||||
}
|
||||
|
||||
func asDomainFunc[T any, U domainer[T]](in U) (out T) {
|
||||
return in.AsDomain()
|
||||
}
|
||||
|
||||
func joinColumns(columns []string) string {
|
||||
return strings.Join(columns, ",")
|
||||
}
|
||||
|
||||
func namedArgColumns(columns []string) string {
|
||||
out := make([]string, len(columns))
|
||||
for i, col := range columns {
|
||||
out[i] = ":" + col
|
||||
}
|
||||
|
||||
return joinColumns(out)
|
||||
}
|
||||
111
internal/kurious/adapters/memory_mapper.go
Normal file
111
internal/kurious/adapters/memory_mapper.go
Normal file
@ -0,0 +1,111 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type inMemoryMapper struct {
|
||||
courseThematicsByID map[string]string
|
||||
learningTypeByID map[string]string
|
||||
|
||||
stats map[string]domain.LearningTypeStat
|
||||
courseThematicByLearningType map[string]string
|
||||
totalCount int
|
||||
}
|
||||
|
||||
func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
|
||||
return &inMemoryMapper{
|
||||
courseThematicsByID: courseThematics,
|
||||
learningTypeByID: learningType,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error {
|
||||
const batchSize = 1000
|
||||
|
||||
m.stats = map[string]domain.LearningTypeStat{}
|
||||
m.courseThematicByLearningType = map[string]string{}
|
||||
|
||||
var offset int
|
||||
for {
|
||||
result, err := cr.List(ctx, domain.ListCoursesParams{
|
||||
LearningType: "",
|
||||
CourseThematic: "",
|
||||
Limit: batchSize + 1,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing courses: %w", err)
|
||||
}
|
||||
|
||||
courses := result.Courses
|
||||
if len(result.Courses) >= batchSize {
|
||||
courses = result.Courses[:batchSize]
|
||||
}
|
||||
|
||||
m.totalCount += len(courses)
|
||||
|
||||
xslices.ForEach(courses, func(course domain.Course) {
|
||||
stat, ok := m.stats[course.LearningTypeID]
|
||||
stat.Count++
|
||||
if !ok {
|
||||
stat.CourseThematic = map[string]int{}
|
||||
}
|
||||
stat.CourseThematic[course.ThematicID]++
|
||||
m.stats[course.LearningTypeID] = stat
|
||||
m.courseThematicByLearningType[course.ThematicID] = course.LearningTypeID
|
||||
})
|
||||
|
||||
if len(result.Courses) != batchSize+1 {
|
||||
break
|
||||
}
|
||||
offset += batchSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int {
|
||||
if byCourseThematic != "" && byLearningType == "" {
|
||||
byLearningType = m.courseThematicByLearningType[byCourseThematic]
|
||||
}
|
||||
if byLearningType != "" {
|
||||
stat := m.stats[byLearningType]
|
||||
if byCourseThematic != "" {
|
||||
return stat.CourseThematic[byCourseThematic]
|
||||
}
|
||||
return stat.Count
|
||||
}
|
||||
return m.totalCount
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) GetStats(copyMap bool) map[string]domain.LearningTypeStat {
|
||||
if !copyMap {
|
||||
return m.stats
|
||||
}
|
||||
|
||||
out := make(map[string]domain.LearningTypeStat, len(m.stats))
|
||||
for learningType, stats := range m.stats {
|
||||
copiedStats := make(map[string]int, len(stats.CourseThematic))
|
||||
for courseThematic, count := range stats.CourseThematic {
|
||||
copiedStats[courseThematic] = count
|
||||
}
|
||||
out[learningType] = domain.LearningTypeStat{
|
||||
Count: stats.Count,
|
||||
CourseThematic: copiedStats,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) CourseThematicNameByID(id string) string {
|
||||
return m.courseThematicsByID[id]
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) LearningTypeNameByID(id string) string {
|
||||
return m.learningTypeByID[id]
|
||||
}
|
||||
60
internal/kurious/adapters/sqlite_basetest.go
Normal file
60
internal/kurious/adapters/sqlite_basetest.go
Normal file
@ -0,0 +1,60 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/migrations/sqlite"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type sqliteBaseSuite struct {
|
||||
suite.Suite
|
||||
|
||||
// TODO: make baseTestSuite that provides this kind of things
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
log *slog.Logger
|
||||
|
||||
connection *sqliteConnection
|
||||
}
|
||||
|
||||
func (s *sqliteBaseSuite) SetupSuite() {
|
||||
s.ctx, s.cancel = context.WithCancel(context.Background())
|
||||
s.log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
AddSource: false,
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
connection, err := NewSqliteConnection(s.ctx, config.Sqlite{
|
||||
DSN: ":memory:",
|
||||
ShutdownTimeout: time.Second * 3,
|
||||
}, s.log)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.connection = connection
|
||||
db := s.connection.db
|
||||
|
||||
err = sqlite.RunMigrations(s.ctx, db.DB, s.log.With(slog.String("component", "migrator")))
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *sqliteBaseSuite) TearDownSuite() {
|
||||
s.cancel()
|
||||
err := s.connection.Close()
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *sqliteBaseSuite) TearDownTest() {
|
||||
db := s.connection.db
|
||||
for _, query := range []string{
|
||||
"DELETE FROM learning_categories",
|
||||
"DELETE FROM courses",
|
||||
} {
|
||||
_, err := db.ExecContext(s.ctx, query)
|
||||
s.Require().NoError(err, "cleaning up database")
|
||||
}
|
||||
}
|
||||
54
internal/kurious/adapters/sqlite_connection.go
Normal file
54
internal/kurious/adapters/sqlite_connection.go
Normal file
@ -0,0 +1,54 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/migrations/sqlite"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type sqliteConnection struct {
|
||||
db *sqlx.DB
|
||||
shutdownTimeout time.Duration
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewSqliteConnection(ctx context.Context, cfg config.Sqlite, log *slog.Logger) (*sqliteConnection, error) {
|
||||
conn, err := sqlx.Open("sqlite", cfg.DSN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openning db connection: %w", err)
|
||||
}
|
||||
|
||||
err = sqlite.RunMigrations(ctx, conn.DB, log)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running migrations: %w", err)
|
||||
}
|
||||
|
||||
return &sqliteConnection{
|
||||
db: conn,
|
||||
log: log,
|
||||
shutdownTimeout: xdefault.WithFallback(cfg.ShutdownTimeout, defaultShutdownTimeout),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *sqliteConnection) Close() error {
|
||||
_, cancel := context.WithTimeout(context.Background(), c.shutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
return c.db.Close()
|
||||
}
|
||||
|
||||
func getSqliteBaseAttributes() []attribute.KeyValue {
|
||||
return []attribute.KeyValue{
|
||||
dbSystemAttr.String("sqlite"),
|
||||
dbNameAttr.String("courses"),
|
||||
}
|
||||
}
|
||||
565
internal/kurious/adapters/sqlite_course_repository.go
Normal file
565
internal/kurious/adapters/sqlite_course_repository.go
Normal file
@ -0,0 +1,565 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func (c *sqliteConnection) CourseRepository() *sqliteCourseRepository {
|
||||
return &sqliteCourseRepository{
|
||||
db: c.db,
|
||||
log: c.log.With(slog.String("repository", "course")),
|
||||
}
|
||||
}
|
||||
|
||||
type sqliteCourseRepository struct {
|
||||
db *sqlx.DB
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) List(
|
||||
ctx context.Context,
|
||||
params domain.ListCoursesParams,
|
||||
) (result domain.ListCoursesResult, err error) {
|
||||
const queryTemplate = `SELECT %s from courses WHERE 1=1`
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list courses.courses",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
||||
args := make([]any, 0, 6)
|
||||
if params.LearningType != "" {
|
||||
args = append(args, params.LearningType)
|
||||
query += " AND learning_type = ?"
|
||||
}
|
||||
if params.CourseThematic != "" {
|
||||
args = append(args, params.CourseThematic)
|
||||
query += " AND course_thematic = ?"
|
||||
}
|
||||
if params.OrganizationID != "" {
|
||||
args = append(args, params.OrganizationID)
|
||||
query += " AND organization_id = ?"
|
||||
}
|
||||
|
||||
if params.OrderBy == "" {
|
||||
params.OrderBy = "id"
|
||||
}
|
||||
|
||||
direction := "ASC"
|
||||
if !params.Ascending {
|
||||
direction = "DESC"
|
||||
}
|
||||
query += " ORDER BY " + params.OrderBy + " " + direction
|
||||
|
||||
if params.Limit > 0 {
|
||||
query += " LIMIT ?"
|
||||
args = append(args, params.Limit)
|
||||
}
|
||||
if params.Offset > 0 {
|
||||
query += " OFFSET ?"
|
||||
args = append(args, params.Offset)
|
||||
}
|
||||
|
||||
span.SetAttributes(dbStatementAttr.String(query))
|
||||
|
||||
scanF := func(s rowsScanner) (err error) {
|
||||
var cdb sqliteCourseDB
|
||||
err = s.StructScan(&cdb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.Courses = append(result.Courses, cdb.AsDomain())
|
||||
return nil
|
||||
}
|
||||
err = scanRows(ctx, r.db, scanF, query, args...)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if params.Limit > 0 && len(result.Courses) == params.Limit {
|
||||
lastIDx := len(result.Courses) - 1
|
||||
result.NextPageToken = result.Courses[lastIDx].ID
|
||||
}
|
||||
|
||||
result.Count, err = r.listCount(ctx, params)
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, r.log, err, "unable to list count")
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.Int("db.items_count", len(result.Courses)),
|
||||
attribute.Int("db.total_items", result.Count),
|
||||
)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) ListLearningTypes(
|
||||
ctx context.Context,
|
||||
) (result domain.ListLearningTypeResult, err error) {
|
||||
const query = "SELECT DISTINCT learning_type FROM courses"
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list_learning_types courses.courses",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
err = r.db.SelectContext(ctx, &result.LearningTypeIDs, query)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) ListCourseThematics(
|
||||
ctx context.Context,
|
||||
params domain.ListCourseThematicsParams,
|
||||
) (result domain.ListCourseThematicsResult, err error) {
|
||||
const queryTemplate = "SELECT DISTINCT course_thematic FROM courses WHERE 1=1"
|
||||
|
||||
query := queryTemplate
|
||||
args := make([]any, 0, 1)
|
||||
if params.LearningTypeID != "" {
|
||||
args = append(args, params.LearningTypeID)
|
||||
query += " AND learning_type = ?"
|
||||
}
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list courses.course_thematic",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
err = r.db.SelectContext(ctx, &result.CourseThematicIDs, query, args...)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) ListStatistics(
|
||||
ctx context.Context,
|
||||
params domain.ListStatisticsParams,
|
||||
) (result domain.ListStatisticsResult, err error) {
|
||||
const queryTemplate = `SELECT learning_type, course_thematic, organization_id, count(id) as count` +
|
||||
` FROM courses` +
|
||||
` WHERE 1=1`
|
||||
|
||||
query := queryTemplate
|
||||
args := make([]any, 0, 3)
|
||||
|
||||
if params.LearningTypeID != "" {
|
||||
query += ` AND learning_type = ?`
|
||||
args = append(args, params.LearningTypeID)
|
||||
}
|
||||
if params.CourseThematicID != "" {
|
||||
query += ` AND course_thematic = ?`
|
||||
args = append(args, params.CourseThematicID)
|
||||
}
|
||||
if params.OrganizaitonID != "" {
|
||||
query += ` AND organization_id = ?`
|
||||
args = append(args, params.OrganizaitonID)
|
||||
}
|
||||
|
||||
query += ` GROUP BY learning_type, course_thematic, organization_id`
|
||||
query += ` ORDER BY count(id) DESC`
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list courses.statistics",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
dbArgumentsAttr.StringSlice(argumentsAsStrings(args...)),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "finished")
|
||||
}
|
||||
|
||||
span.End()
|
||||
}()
|
||||
|
||||
var stats []sqliteCourseStatistic
|
||||
err = r.db.SelectContext(ctx, &stats, query, args...)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
result.LearningTypeStatistics = xslices.Map(stats, asDomainFunc)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) Get(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (course domain.Course, err error) {
|
||||
const queryTemplate = `SELECT %s FROM courses WHERE id = ?`
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "get courses.courses",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
var courseDB sqliteCourseDB
|
||||
err = r.db.GetContext(ctx, &courseDB, query, id)
|
||||
if err != nil {
|
||||
return course, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return courseDB.AsDomain(), nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) GetByExternalID(
|
||||
ctx context.Context, id string,
|
||||
) (course domain.Course, err error) {
|
||||
return course, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) CreateBatch(ctx context.Context, params ...domain.CreateCourseParams) error {
|
||||
tx, err := r.db.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelDefault})
|
||||
if err != nil {
|
||||
return fmt.Errorf("beginning tx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
var errTx error
|
||||
if err != nil {
|
||||
errTx = tx.Rollback()
|
||||
} else {
|
||||
errTx = tx.Commit()
|
||||
}
|
||||
|
||||
err = errors.Join(err, errTx)
|
||||
}()
|
||||
|
||||
const queryTempalate = `INSERT INTO courses` +
|
||||
` (%s) VALUES (%s)`
|
||||
|
||||
placeholders := strings.TrimSuffix(strings.Repeat("?,", len(coursesFields)), ",")
|
||||
query := fmt.Sprintf(queryTempalate, coursesFieldsStr, placeholders)
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing statement: %w", err)
|
||||
}
|
||||
|
||||
for _, param := range params {
|
||||
_, err := stmt.ExecContext(ctx, createCourseParamsAsValues(param)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing statement query: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) Create(ctx context.Context, params domain.CreateCourseParams) (domain.Course, error) {
|
||||
err := r.CreateBatch(ctx, params)
|
||||
return domain.Course{}, err
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) Delete(ctx context.Context, id string) error {
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) listCount(ctx context.Context, params domain.ListCoursesParams) (count int, err error) {
|
||||
const queryTemplate = `SELECT COUNT(id) FROM courses WHERE 1=1`
|
||||
|
||||
query := queryTemplate
|
||||
args := make([]any, 0, 6)
|
||||
|
||||
if params.LearningType != "" {
|
||||
args = append(args, params.LearningType)
|
||||
query += " AND learning_type = ?"
|
||||
}
|
||||
if params.CourseThematic != "" {
|
||||
args = append(args, params.CourseThematic)
|
||||
query += " AND course_thematic = ?"
|
||||
}
|
||||
if params.OrganizationID != "" {
|
||||
args = append(args, params.OrganizationID)
|
||||
query += " AND organization_id = ?"
|
||||
}
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list_count courses.courses",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
err = r.db.GetContext(ctx, &count, query, args...)
|
||||
if err != nil {
|
||||
return count, fmt.Errorf("sending query: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
type rowsScanner interface {
|
||||
sqlx.ColScanner
|
||||
|
||||
StructScan(dest any) error
|
||||
}
|
||||
|
||||
func scanRows(ctx context.Context, db *sqlx.DB, f func(rowsScanner) error, query string, args ...any) error {
|
||||
rows, err := db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("querying rows: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, rows.Close())
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
err = f(rows)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scanning row: %w", err)
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return fmt.Errorf("checking rows for errors: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createCourseParamsAsValues(params domain.CreateCourseParams) []any {
|
||||
now := time.Now()
|
||||
|
||||
return []any{
|
||||
params.ID,
|
||||
nullableValueAsString(params.ExternalID),
|
||||
mapSourceTypeFromDomain(params.SourceType),
|
||||
nullableValueAsString(params.SourceName),
|
||||
params.CourseThematic,
|
||||
params.LearningType,
|
||||
params.OrganizationID,
|
||||
params.OriginLink,
|
||||
params.ImageLink,
|
||||
params.Name,
|
||||
params.Description,
|
||||
params.FullPrice,
|
||||
params.Discount,
|
||||
params.Duration.Truncate(time.Second).Milliseconds() / 1000,
|
||||
params.StartsAt,
|
||||
now,
|
||||
now,
|
||||
sql.NullTime{},
|
||||
}
|
||||
}
|
||||
|
||||
type sqliteCourseDB struct {
|
||||
ID string `db:"id"`
|
||||
ExternalID sql.NullString `db:"external_id"`
|
||||
SourceType string `db:"source_type"`
|
||||
SourceName sql.NullString `db:"source_name"`
|
||||
ThematicID string `db:"course_thematic"`
|
||||
LearningTypeID string `db:"learning_type"`
|
||||
OrganizationID string `db:"organization_id"`
|
||||
OriginLink string `db:"origin_link"`
|
||||
ImageLink string `db:"image_link"`
|
||||
Name string `db:"name"`
|
||||
Description string `db:"description"`
|
||||
FullPrice float64 `db:"full_price"`
|
||||
Discount float64 `db:"discount"`
|
||||
Duration int64 `db:"duration"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
StartsAt sql.NullTime `db:"starts_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at"`
|
||||
}
|
||||
|
||||
type sqliteCourseStatistic struct {
|
||||
LearningTypeID string `db:"learning_type"`
|
||||
CourseThematicID string `db:"course_thematic"`
|
||||
OrganizationID string `db:"organization_id"`
|
||||
Count int `db:"count"`
|
||||
}
|
||||
|
||||
func (s sqliteCourseStatistic) AsDomain() domain.StatisticUnit {
|
||||
return domain.StatisticUnit(s)
|
||||
}
|
||||
|
||||
func nullStringAsDomain(s sql.NullString) nullable.Value[string] {
|
||||
if s.Valid {
|
||||
return nullable.NewValue(s.String)
|
||||
}
|
||||
|
||||
return nullable.Value[string]{}
|
||||
}
|
||||
|
||||
func nullTimeAsDomain(s sql.NullTime) nullable.Value[time.Time] {
|
||||
if s.Valid {
|
||||
return nullable.NewValue(s.Time)
|
||||
}
|
||||
|
||||
return nullable.Value[time.Time]{}
|
||||
}
|
||||
|
||||
func nullableValueAsString(v nullable.Value[string]) sql.NullString {
|
||||
return sql.NullString{
|
||||
Valid: v.Valid(),
|
||||
String: v.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
func nullableValueAsTime(v nullable.Value[time.Time]) sql.NullTime {
|
||||
return sql.NullTime{
|
||||
Valid: v.Valid(),
|
||||
Time: v.Value(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c sqliteCourseDB) AsDomain() domain.Course {
|
||||
return domain.Course{
|
||||
ID: c.ID,
|
||||
OrganizationID: c.OrganizationID,
|
||||
OriginLink: c.OriginLink,
|
||||
ImageLink: c.ImageLink,
|
||||
Name: c.Name,
|
||||
Description: c.Description,
|
||||
FullPrice: c.FullPrice,
|
||||
Discount: c.Discount,
|
||||
ThematicID: c.ThematicID,
|
||||
LearningTypeID: c.LearningTypeID,
|
||||
Duration: time.Second * time.Duration(c.Duration),
|
||||
StartsAt: c.StartsAt.Time,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
ExternalID: nullStringAsDomain(c.ExternalID),
|
||||
SourceType: mapSourceTypeToDomain(c.SourceType),
|
||||
SourceName: nullStringAsDomain(c.SourceName),
|
||||
DeletedAt: nullTimeAsDomain(c.DeletedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sqliteCourseDB) FromDomain(d domain.Course) {
|
||||
*c = sqliteCourseDB{
|
||||
ID: d.ID,
|
||||
OrganizationID: d.OrganizationID,
|
||||
OriginLink: d.OriginLink,
|
||||
ImageLink: d.ImageLink,
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
FullPrice: d.FullPrice,
|
||||
Discount: d.Discount,
|
||||
ThematicID: d.ThematicID,
|
||||
LearningTypeID: d.LearningTypeID,
|
||||
SourceType: mapSourceTypeFromDomain(d.SourceType),
|
||||
Duration: d.Duration.Truncate(time.Second).Milliseconds() / 1000,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
ExternalID: nullableValueAsString(d.ExternalID),
|
||||
SourceName: nullableValueAsString(d.SourceName),
|
||||
DeletedAt: nullableValueAsTime(d.DeletedAt),
|
||||
StartsAt: sql.NullTime{
|
||||
Time: d.StartsAt,
|
||||
Valid: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sqliteCourseRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
|
||||
outbase := append(
|
||||
getSqliteBaseAttributes(),
|
||||
dbTableAttr.String("courses"),
|
||||
)
|
||||
|
||||
return append(outbase, custom...)
|
||||
}
|
||||
|
||||
func argumentsAsStrings(args ...any) []string {
|
||||
out := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
out = append(out, fmt.Sprintf("%v", arg))
|
||||
}
|
||||
return out
|
||||
}
|
||||
107
internal/kurious/adapters/sqlite_course_repository_test.go
Normal file
107
internal/kurious/adapters/sqlite_course_repository_test.go
Normal file
@ -0,0 +1,107 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestSqliteCourseRepository(t *testing.T) {
|
||||
suite.Run(t, new(sqliteCourseRepositorySuite))
|
||||
}
|
||||
|
||||
type sqliteCourseRepositorySuite struct {
|
||||
sqliteBaseSuite
|
||||
}
|
||||
|
||||
func (s *sqliteCourseRepositorySuite) TestCreateCourse() {
|
||||
expcourse := domain.Course{
|
||||
ID: "test-id",
|
||||
ExternalID: nullable.NewValue("ext-id"),
|
||||
Name: "test-name",
|
||||
SourceType: domain.SourceTypeParsed,
|
||||
SourceName: nullable.NewValue("test-source"),
|
||||
ThematicID: "test-thematic",
|
||||
LearningTypeID: "test-learning",
|
||||
OrganizationID: "test-org-id",
|
||||
OriginLink: "test-link",
|
||||
ImageLink: "test-image-link",
|
||||
Description: "description",
|
||||
FullPrice: 123,
|
||||
Discount: 321,
|
||||
Duration: time.Second * 360,
|
||||
StartsAt: time.Date(2020, 10, 01, 11, 22, 33, 0, time.UTC),
|
||||
Thematic: "",
|
||||
LearningType: "",
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
DeletedAt: nullable.Value[time.Time]{},
|
||||
}
|
||||
|
||||
cr := s.connection.CourseRepository()
|
||||
_, err := cr.Create(s.ctx, domain.CreateCourseParams{
|
||||
ID: "test-id",
|
||||
ExternalID: nullable.NewValue("ext-id"),
|
||||
Name: "test-name",
|
||||
SourceType: domain.SourceTypeParsed,
|
||||
SourceName: nullable.NewValue("test-source"),
|
||||
CourseThematic: "test-thematic",
|
||||
LearningType: "test-learning",
|
||||
OrganizationID: "test-org-id",
|
||||
OriginLink: "test-link",
|
||||
ImageLink: "test-image-link",
|
||||
Description: "description",
|
||||
FullPrice: 123,
|
||||
Discount: 321,
|
||||
Duration: time.Second * 360,
|
||||
StartsAt: time.Date(2020, 10, 01, 11, 22, 33, 0, time.UTC),
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
gotCourse, err := cr.Get(s.ctx, expcourse.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().NotEmpty(gotCourse.CreatedAt)
|
||||
s.Require().NotEmpty(gotCourse.UpdatedAt)
|
||||
s.Require().Empty(gotCourse.DeletedAt)
|
||||
|
||||
expcourse.CreatedAt = gotCourse.CreatedAt
|
||||
expcourse.UpdatedAt = gotCourse.UpdatedAt
|
||||
|
||||
s.Require().Equal(expcourse, gotCourse)
|
||||
}
|
||||
|
||||
func (s *sqliteCourseRepositorySuite) TestListLimitOffset() {
|
||||
const coursecount = 9
|
||||
const listparts = 3
|
||||
basecourse := domain.CreateCourseParams{
|
||||
SourceType: domain.SourceTypeManual,
|
||||
}
|
||||
|
||||
cr := s.connection.CourseRepository()
|
||||
for i := 0; i < coursecount; i++ {
|
||||
basecourse.ID = strconv.Itoa(i)
|
||||
_, err := cr.Create(s.ctx, basecourse)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
params := domain.ListCoursesParams{
|
||||
Limit: coursecount / listparts,
|
||||
}
|
||||
for i := 0; i < listparts; i++ {
|
||||
result, err := cr.List(s.ctx, params)
|
||||
s.NoError(err)
|
||||
s.Len(result.Courses, listparts)
|
||||
|
||||
params.Offset += listparts
|
||||
}
|
||||
|
||||
result, err := cr.List(s.ctx, params)
|
||||
s.NoError(err)
|
||||
s.Empty(result.Courses)
|
||||
}
|
||||
174
internal/kurious/adapters/sqlite_learning_category_repository.go
Normal file
174
internal/kurious/adapters/sqlite_learning_category_repository.go
Normal file
@ -0,0 +1,174 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var (
|
||||
learningCategoryColumns = []string{
|
||||
"id",
|
||||
"logo",
|
||||
"courses_count",
|
||||
}
|
||||
|
||||
learningCategoryColumnsStr = joinColumns(learningCategoryColumns)
|
||||
)
|
||||
|
||||
type learningCategoryDB struct {
|
||||
ID string `db:"id"`
|
||||
Logo sql.NullString `db:"logo"`
|
||||
CoursesCount int `db:"courses_count"`
|
||||
}
|
||||
|
||||
func (l learningCategoryDB) AsDomain() domain.LearningCategory {
|
||||
return domain.LearningCategory{
|
||||
ID: l.ID,
|
||||
Logo: nullStringAsDomain(l.Logo),
|
||||
CoursesCount: l.CoursesCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sqliteConnection) LearningCategory() *sqliteLearingCategoryRepository {
|
||||
return &sqliteLearingCategoryRepository{
|
||||
db: c.db,
|
||||
log: c.log.With("repository", "learning_categories"),
|
||||
}
|
||||
}
|
||||
|
||||
type sqliteLearingCategoryRepository struct {
|
||||
db *sqlx.DB
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (r *sqliteLearingCategoryRepository) Upsert(ctx context.Context, c domain.LearningCategory) (err error) {
|
||||
const queryTemplate = "INSERT INTO learning_categories (%s)" +
|
||||
" VALUES (%s)" +
|
||||
" ON CONFLICT(id) DO UPDATE" +
|
||||
" SET id = excluded.id" +
|
||||
", logo = excluded.logo" +
|
||||
", courses_count = excluded.courses_count"
|
||||
|
||||
query := fmt.Sprintf(
|
||||
queryTemplate,
|
||||
learningCategoryColumnsStr,
|
||||
strings.TrimSuffix(strings.Repeat("?,", len(learningCategoryColumns)), ","),
|
||||
)
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "upsert courses.learning_categories",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("INSERT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
_, err = r.db.ExecContext(
|
||||
ctx, query,
|
||||
c.ID,
|
||||
nullableValueAsString(c.Logo),
|
||||
c.CoursesCount,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sqliteLearingCategoryRepository) List(ctx context.Context) (out []domain.LearningCategory, err error) {
|
||||
const queryTemplate = "SELECT %s FROM learning_categories;"
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr)
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list courses.learning_categories",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
var categories []learningCategoryDB
|
||||
err = r.db.SelectContext(ctx, &categories, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
out = xslices.Map(categories, asDomainFunc)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *sqliteLearingCategoryRepository) Get(ctx context.Context, id string) (category domain.LearningCategory, err error) {
|
||||
const queryTemplate = "SELECT %s FROM learning_categories WHERE id = ?;"
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, learningCategoryColumnsStr)
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "get courses.learning_categories",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
var cdb learningCategoryDB
|
||||
err = r.db.GetContext(ctx, &cdb, query, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.LearningCategory{}, domain.ErrNotFound
|
||||
}
|
||||
return domain.LearningCategory{}, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return cdb.AsDomain(), nil
|
||||
}
|
||||
|
||||
func (c *sqliteLearingCategoryRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
|
||||
outbase := append(
|
||||
getSqliteBaseAttributes(),
|
||||
dbTableAttr.String("learning_categories"),
|
||||
)
|
||||
|
||||
return append(outbase, custom...)
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestSqliteLearningCategories(t *testing.T) {
|
||||
suite.Run(t, new(sqliteLearningCategoriesRepositorySuite))
|
||||
}
|
||||
|
||||
type sqliteLearningCategoriesRepositorySuite struct {
|
||||
sqliteBaseSuite
|
||||
}
|
||||
|
||||
func (s *sqliteLearningCategoriesRepositorySuite) TestGet() {
|
||||
var cdb learningCategoryDB
|
||||
err := s.connection.db.GetContext(
|
||||
s.ctx, &cdb,
|
||||
"INSERT INTO learning_categories (id, logo, courses_count) VALUES (?,?,?) RETURNING *",
|
||||
"test-id", "test-url-logo", 42,
|
||||
)
|
||||
|
||||
s.Require().NoError(err)
|
||||
|
||||
expectedCategory := cdb.AsDomain()
|
||||
|
||||
lr := s.connection.LearningCategory()
|
||||
got, err := lr.Get(s.ctx, expectedCategory.ID)
|
||||
s.NoError(err)
|
||||
s.Equal(expectedCategory, got)
|
||||
}
|
||||
|
||||
func (s *sqliteLearningCategoriesRepositorySuite) TestList() {
|
||||
stmt, err := s.connection.db.PrepareContext(s.ctx, "INSERT INTO learning_categories (id, logo, courses_count) VALUES (?,?,?)")
|
||||
s.NoError(err)
|
||||
|
||||
for _, args := range [][]any{
|
||||
{"test-id-1", "test-url-1", 1},
|
||||
{"test-id-2", "test-url-2", 2},
|
||||
} {
|
||||
_, err = stmt.ExecContext(s.ctx, args...)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
gotCategories, err := s.connection.LearningCategory().List(s.ctx)
|
||||
s.NoError(err)
|
||||
s.Len(gotCategories, 2)
|
||||
|
||||
expCategories := []domain.LearningCategory{
|
||||
{
|
||||
ID: "test-id-1",
|
||||
Logo: nullable.NewValue("test-url-1"),
|
||||
CoursesCount: 1,
|
||||
},
|
||||
{
|
||||
ID: "test-id-2",
|
||||
Logo: nullable.NewValue("test-url-2"),
|
||||
CoursesCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
if gotCategories[0].ID != "test-id-1" {
|
||||
gotCategories[0], gotCategories[1] = gotCategories[1], gotCategories[0]
|
||||
}
|
||||
|
||||
s.ElementsMatch(expCategories, gotCategories)
|
||||
}
|
||||
|
||||
func (s *sqliteLearningCategoriesRepositorySuite) TestUpsert() {
|
||||
const categoryID = "test-id-1"
|
||||
repo := s.connection.LearningCategory()
|
||||
gotCategory, err := repo.Get(s.ctx, categoryID)
|
||||
s.ErrorIs(err, domain.ErrNotFound)
|
||||
s.Empty(gotCategory)
|
||||
|
||||
createdCategory := domain.LearningCategory{
|
||||
ID: categoryID,
|
||||
Logo: nullable.NewValue("test-url-1"),
|
||||
CoursesCount: 1,
|
||||
}
|
||||
err = repo.Upsert(s.ctx, createdCategory)
|
||||
s.NoError(err)
|
||||
|
||||
gotCategory, err = repo.Get(s.ctx, categoryID)
|
||||
s.NoError(err)
|
||||
s.Equal(createdCategory, gotCategory)
|
||||
|
||||
createdCategory.Logo = nullable.NewValue("test-url-2")
|
||||
|
||||
err = repo.Upsert(s.ctx, createdCategory)
|
||||
s.NoError(err)
|
||||
|
||||
gotCategory, err = repo.Get(s.ctx, categoryID)
|
||||
s.NoError(err)
|
||||
s.Equal(createdCategory, gotCategory)
|
||||
}
|
||||
329
internal/kurious/adapters/sqlite_organization_repository.go
Normal file
329
internal/kurious/adapters/sqlite_organization_repository.go
Normal file
@ -0,0 +1,329 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
organizationColumns = []string{
|
||||
"id",
|
||||
"external_id",
|
||||
"alias",
|
||||
"name",
|
||||
"site",
|
||||
"logo",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
}
|
||||
|
||||
organizationColumnsStr = joinColumns(organizationColumns)
|
||||
organizationColumnsArgsStr = namedArgColumns(organizationColumns)
|
||||
)
|
||||
|
||||
type organizationStatDB struct {
|
||||
ID string `db:"id"`
|
||||
ExternalID sql.NullString `db:"external_id"`
|
||||
Name string `db:"name"`
|
||||
CoursesCount uint64 `db:"courses_count"`
|
||||
}
|
||||
|
||||
func (s organizationStatDB) AsDomain() domain.OrganizationStat {
|
||||
return domain.OrganizationStat{
|
||||
ID: s.ID,
|
||||
ExternalID: nullStringAsDomain(s.ExternalID),
|
||||
Name: s.Name,
|
||||
CoursesCount: s.CoursesCount,
|
||||
}
|
||||
}
|
||||
|
||||
type organizationDB struct {
|
||||
ID string `db:"id"`
|
||||
ExternalID sql.NullString `db:"external_id"`
|
||||
Alias string `db:"alias"`
|
||||
Name string `db:"name"`
|
||||
Site string `db:"site"`
|
||||
Logo string `db:"logo"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at"`
|
||||
}
|
||||
|
||||
func (o organizationDB) AsDomain() domain.Organization {
|
||||
return domain.Organization{
|
||||
ID: o.ID,
|
||||
ExternalID: nullStringAsDomain(o.ExternalID),
|
||||
Alias: o.Alias,
|
||||
Name: o.Name,
|
||||
Site: o.Site,
|
||||
LogoLink: o.Logo,
|
||||
CreatedAt: o.CreatedAt,
|
||||
UpdatedAt: o.UpdatedAt,
|
||||
DeletedAt: nullTimeAsDomain(o.DeletedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *organizationDB) FromDomain(in domain.Organization) {
|
||||
*o = organizationDB{
|
||||
ID: in.ID,
|
||||
ExternalID: nullableValueAsString(in.ExternalID),
|
||||
Alias: in.Alias,
|
||||
Name: in.Name,
|
||||
Site: in.Site,
|
||||
Logo: in.LogoLink,
|
||||
CreatedAt: in.CreatedAt,
|
||||
UpdatedAt: in.UpdatedAt,
|
||||
DeletedAt: nullableValueAsTime(in.DeletedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sqliteConnection) Organization() domain.OrganizationRepository {
|
||||
return &sqliteOrganizationRepository{
|
||||
db: c.db,
|
||||
log: c.log.With("repository", "organization"),
|
||||
}
|
||||
}
|
||||
|
||||
type sqliteOrganizationRepository struct {
|
||||
db *sqlx.DB
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func (r *sqliteOrganizationRepository) ListStats(
|
||||
ctx context.Context,
|
||||
params domain.ListOrganizationsParams,
|
||||
) (out []domain.OrganizationStat, err error) {
|
||||
const queryTemplate = `WITH cte as (
|
||||
SELECT learning_type, course_thematic, organization_id, count(id) as courses_count
|
||||
FROM courses
|
||||
WHERE 1=1 {whereSuffix}
|
||||
GROUP BY learning_type, course_thematic, organization_id
|
||||
) SELECT o.id as id, o.external_id as external_id, o.name as name, cte.courses_count as courses_count
|
||||
FROM cte
|
||||
INNER JOIN organizations o ON o.id = cte.organization_id`
|
||||
|
||||
whereSuffix := ""
|
||||
|
||||
args := make([]any, 0, 3)
|
||||
if params.LearningTypeID != "" {
|
||||
whereSuffix += " AND learning_type = ?"
|
||||
args = append(args, params.LearningTypeID)
|
||||
}
|
||||
if params.CourseThematicID != "" {
|
||||
whereSuffix += " AND course_thematic = ?"
|
||||
args = append(args, params.CourseThematicID)
|
||||
}
|
||||
|
||||
query := strings.Replace(queryTemplate, "{whereSuffix}", whereSuffix, 1)
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list_stats courses",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
var stats []organizationStatDB
|
||||
err = r.db.SelectContext(ctx, &stats, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return xslices.Map(stats, asDomainFunc), nil
|
||||
}
|
||||
|
||||
func (r *sqliteOrganizationRepository) List(ctx context.Context, params domain.ListOrganizationsParams) (out []domain.Organization, err error) {
|
||||
const queryTemplate = `SELECT %s FROM organizations WHERE 1=1`
|
||||
query := fmt.Sprintf(queryTemplate, organizationColumnsStr)
|
||||
|
||||
args := make([]any, 0, len(params.IDs))
|
||||
if len(params.IDs) > 0 {
|
||||
args = append(
|
||||
args,
|
||||
xslices.Map(params.IDs, func(t string) any { return t })...,
|
||||
)
|
||||
queryParam := strings.TrimSuffix(strings.Repeat("?,", len(args)), ",")
|
||||
query += " AND id IN (" + queryParam + ")"
|
||||
}
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "list courses.organizations",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
organizations := make([]organizationDB, 0, 1<<8)
|
||||
err = r.db.SelectContext(ctx, &organizations, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return xslices.Map(organizations, asDomainFunc), nil
|
||||
}
|
||||
|
||||
func (r *sqliteOrganizationRepository) Get(ctx context.Context, params domain.GetOrganizationParams) (out domain.Organization, err error) {
|
||||
const queryTemplate = "SELECT %s FROM organizations WHERE 1=1"
|
||||
query := fmt.Sprintf(queryTemplate, organizationColumnsStr)
|
||||
args := make([]any, 0, 2)
|
||||
if params.ID.Valid() {
|
||||
args = append(args, params.ID.Value())
|
||||
query += " AND id = ?"
|
||||
}
|
||||
if params.ExternalID.Valid() {
|
||||
args = append(args, params.ExternalID.Value())
|
||||
query += " AND external_id = ?"
|
||||
}
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "get courses.organizations",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("SELECT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
var orgdb organizationDB
|
||||
err = r.db.GetContext(ctx, &orgdb, query, args...)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return out, domain.ErrNotFound
|
||||
}
|
||||
return out, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return orgdb.AsDomain(), nil
|
||||
}
|
||||
|
||||
func (r *sqliteOrganizationRepository) Create(ctx context.Context, params domain.CreateOrganizationParams) (out domain.Organization, err error) {
|
||||
const queryTemplate = `INSERT INTO organizations (%[1]s) VALUES (%[2]s) RETURNING %[1]s`
|
||||
query := fmt.Sprintf(queryTemplate, organizationColumnsStr, organizationColumnsArgsStr)
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "create courses.organizations",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("INSERT"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stmt, err := r.db.PrepareNamedContext(ctx, query)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("preparing statement: %w", err)
|
||||
}
|
||||
|
||||
var orgdb organizationDB
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
err = stmt.GetContext(ctx, &orgdb, organizationDB{
|
||||
ID: params.ID,
|
||||
ExternalID: nullableValueAsString(params.ExternalID),
|
||||
Alias: params.Alias,
|
||||
Name: params.Name,
|
||||
Site: params.Site,
|
||||
Logo: params.LogoLink,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DeletedAt: sql.NullTime{},
|
||||
})
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return orgdb.AsDomain(), nil
|
||||
}
|
||||
|
||||
func (r *sqliteOrganizationRepository) Delete(ctx context.Context, id string) (err error) {
|
||||
const query = `DELETE FROM organizations WHERE id = ?`
|
||||
|
||||
ctx, span := dbTracer.Start(
|
||||
ctx, "delete courses.organizations",
|
||||
trace.WithSpanKind(trace.SpanKindClient),
|
||||
trace.WithAttributes(
|
||||
r.mergeAttributes(
|
||||
dbOperationAttr.String("DELETE"),
|
||||
dbStatementAttr.String(query),
|
||||
)...,
|
||||
),
|
||||
)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
|
||||
result, err := r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return domain.ErrNotFound
|
||||
}
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
return domain.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sqliteOrganizationRepository) mergeAttributes(custom ...attribute.KeyValue) []attribute.KeyValue {
|
||||
outbase := append(
|
||||
getSqliteBaseAttributes(),
|
||||
dbTableAttr.String("organizaitons"),
|
||||
)
|
||||
|
||||
return append(outbase, custom...)
|
||||
}
|
||||
128
internal/kurious/adapters/sqlite_organization_repository_test.go
Normal file
128
internal/kurious/adapters/sqlite_organization_repository_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestSqilteOrganizations(t *testing.T) {
|
||||
suite.Run(t, new(sqliteOrganzationRepositorySuite))
|
||||
}
|
||||
|
||||
type sqliteOrganzationRepositorySuite struct {
|
||||
sqliteBaseSuite
|
||||
}
|
||||
|
||||
func (s *sqliteOrganzationRepositorySuite) TearDownTest() {
|
||||
_ = s.connection.db.MustExecContext(s.ctx, "DELETE FROM organizations")
|
||||
}
|
||||
|
||||
func (s *sqliteOrganzationRepositorySuite) TestList() {
|
||||
const itemscount = 3
|
||||
orgsdb := make([]domain.Organization, 0, itemscount)
|
||||
|
||||
baseOrg := domain.Organization{
|
||||
Alias: "test-alias",
|
||||
Name: "test-name",
|
||||
Site: "test-site",
|
||||
LogoLink: "test-logo",
|
||||
}
|
||||
for i := 0; i < itemscount; i++ {
|
||||
nextitem := baseOrg
|
||||
iStr := strconv.Itoa(i)
|
||||
nextitem.ID = "test-id-" + iStr
|
||||
nextitem.ExternalID.Set("test-ext-id-" + iStr)
|
||||
|
||||
gotOrg, err := s.connection.Organization().Create(s.ctx, domain.CreateOrganizationParams{
|
||||
ID: nextitem.ID,
|
||||
ExternalID: nextitem.ExternalID,
|
||||
Alias: nextitem.Alias,
|
||||
Name: nextitem.Name,
|
||||
Site: nextitem.Site,
|
||||
LogoLink: nextitem.LogoLink,
|
||||
})
|
||||
s.NoError(err)
|
||||
orgsdb = append(orgsdb, gotOrg)
|
||||
}
|
||||
|
||||
gotOrgs, err := s.connection.Organization().List(s.ctx, domain.ListOrganizationsParams{})
|
||||
s.NoError(err)
|
||||
|
||||
compareF := func(lhs, rhs domain.Organization) int {
|
||||
if lhs.ID < rhs.ID {
|
||||
return -1
|
||||
} else if lhs.ID > rhs.ID {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
slices.SortFunc(gotOrgs, compareF)
|
||||
|
||||
for i := range gotOrgs {
|
||||
s.NotEmpty(gotOrgs[i].CreatedAt)
|
||||
s.NotEmpty(gotOrgs[i].UpdatedAt)
|
||||
s.Empty(gotOrgs[i].DeletedAt)
|
||||
|
||||
orgsdb[i].CreatedAt = gotOrgs[i].CreatedAt
|
||||
orgsdb[i].UpdatedAt = gotOrgs[i].UpdatedAt
|
||||
orgsdb[i].DeletedAt = gotOrgs[i].DeletedAt
|
||||
}
|
||||
|
||||
s.ElementsMatch(orgsdb, gotOrgs)
|
||||
}
|
||||
|
||||
func (s *sqliteOrganzationRepositorySuite) TestGet() {
|
||||
var orgdb organizationDB
|
||||
err := s.connection.db.GetContext(
|
||||
s.ctx, &orgdb,
|
||||
`INSERT INTO organizations (`+organizationColumnsStr+`)`+
|
||||
` VALUES ("test-id", "test-ext-id", "test-alias", "test-name", "test-site", "test-logo", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)`+
|
||||
` RETURNING `+organizationColumnsStr,
|
||||
)
|
||||
s.NoError(err)
|
||||
|
||||
expectedOrganization := orgdb.AsDomain()
|
||||
|
||||
getParams := domain.GetOrganizationParams{
|
||||
ID: nullable.NewValue(orgdb.ID),
|
||||
}
|
||||
gotOrganization, err := s.connection.Organization().Get(s.ctx, getParams)
|
||||
s.NoError(err)
|
||||
|
||||
s.Equal(expectedOrganization, gotOrganization)
|
||||
}
|
||||
|
||||
func (s *sqliteOrganzationRepositorySuite) TestCreate() {
|
||||
expectedOrganization := domain.Organization{
|
||||
ID: "test-id",
|
||||
ExternalID: nullable.NewValue("test-ext-id"),
|
||||
Alias: "test-alias",
|
||||
Name: "test-name",
|
||||
Site: "test-site",
|
||||
LogoLink: "test-logo",
|
||||
}
|
||||
|
||||
gotOrganization, err := s.connection.Organization().Create(s.ctx, domain.CreateOrganizationParams{
|
||||
ID: expectedOrganization.ID,
|
||||
ExternalID: expectedOrganization.ExternalID,
|
||||
Alias: expectedOrganization.Alias,
|
||||
Name: expectedOrganization.Name,
|
||||
Site: expectedOrganization.Site,
|
||||
LogoLink: expectedOrganization.LogoLink,
|
||||
})
|
||||
s.NoError(err)
|
||||
s.NotEmpty(gotOrganization.CreatedAt)
|
||||
s.NotEmpty(gotOrganization.UpdatedAt)
|
||||
s.Empty(gotOrganization.DeletedAt)
|
||||
|
||||
expectedOrganization.CreatedAt = gotOrganization.CreatedAt
|
||||
expectedOrganization.UpdatedAt = gotOrganization.UpdatedAt
|
||||
s.Equal(expectedOrganization, gotOrganization)
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
@ -17,13 +18,38 @@ import (
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3"
|
||||
ydblog "github.com/ydb-platform/ydb-go-sdk/v3/log"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/options"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/result/named"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/table/types"
|
||||
"github.com/ydb-platform/ydb-go-sdk/v3/trace"
|
||||
yc "github.com/ydb-platform/ydb-go-yc"
|
||||
)
|
||||
|
||||
var coursesFields = []string{
|
||||
"id",
|
||||
"external_id",
|
||||
"source_type",
|
||||
"source_name",
|
||||
"course_thematic",
|
||||
"learning_type",
|
||||
"organization_id",
|
||||
"origin_link",
|
||||
"image_link",
|
||||
"name",
|
||||
"description",
|
||||
"full_price",
|
||||
"discount",
|
||||
"duration",
|
||||
"starts_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
}
|
||||
|
||||
var coursesFieldsStr = strings.Join(coursesFields, ",")
|
||||
|
||||
const (
|
||||
defaultShutdownTimeout = time.Second * 10
|
||||
)
|
||||
@ -36,7 +62,7 @@ type YDBConnection struct {
|
||||
}
|
||||
|
||||
func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*YDBConnection, error) {
|
||||
opts := make([]ydb.Option, 0, 2)
|
||||
opts := make([]ydb.Option, 0, 3)
|
||||
switch auth := cfg.Auth.(type) {
|
||||
case config.YCAuthIAMToken:
|
||||
opts = append(opts, ydb.WithAccessTokenCredentials(auth.Token))
|
||||
@ -46,6 +72,12 @@ func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*Y
|
||||
yc.WithServiceAccountKeyFileCredentials(auth.Path),
|
||||
)
|
||||
}
|
||||
if cfg.DebugYDB {
|
||||
opts = append(opts,
|
||||
ydb.WithLogger(ydblog.Default(os.Stdout, ydblog.WithMinLevel(ydblog.DEBUG)), trace.DetailsAll),
|
||||
)
|
||||
}
|
||||
|
||||
db, err := ydb.Open(
|
||||
ctx,
|
||||
cfg.DSN,
|
||||
@ -54,6 +86,14 @@ func NewYDBConnection(ctx context.Context, cfg config.YDB, log *slog.Logger) (*Y
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening connection: %w", err)
|
||||
}
|
||||
endpoints, err := db.Discovery().Discover(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("discovering endpoints: %w", err)
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
xcontext.LogInfo(ctx, log, "discovered endpoint", slog.String("value", endpoint.Address()))
|
||||
}
|
||||
|
||||
return &YDBConnection{
|
||||
Driver: db,
|
||||
@ -69,6 +109,14 @@ func (conn *YDBConnection) Close() error {
|
||||
return conn.Driver.Close(ctx)
|
||||
}
|
||||
|
||||
func (conn *YDBConnection) Organization() domain.OrganizationRepository {
|
||||
return domain.NotImplementedOrganizationRepository{}
|
||||
}
|
||||
|
||||
func (conn *YDBConnection) LearningCategory() domain.LearningCategoryRepository {
|
||||
return domain.NotImplementedLearningCategory{}
|
||||
}
|
||||
|
||||
func (conn *YDBConnection) CourseRepository() *ydbCourseRepository {
|
||||
return &ydbCourseRepository{
|
||||
db: conn.Driver,
|
||||
@ -87,59 +135,31 @@ func (r *ydbCourseRepository) List(
|
||||
) (result domain.ListCoursesResult, err error) {
|
||||
const limit = 1000
|
||||
const queryName = "list"
|
||||
// const query = `
|
||||
// DECLARE $limit AS Int32;
|
||||
// DECLARE $id AS Text;
|
||||
// SELECT
|
||||
// id,
|
||||
// external_id,
|
||||
// source_type,
|
||||
// source_name,
|
||||
// course_thematic,
|
||||
// learning_type,
|
||||
// organization_id,
|
||||
// origin_link,
|
||||
// image_link,
|
||||
// name,
|
||||
// description,
|
||||
// full_price,
|
||||
// discount,
|
||||
// duration,
|
||||
// starts_at,
|
||||
// created_at,
|
||||
// updated_at,
|
||||
// deleted_at
|
||||
// FROM
|
||||
// courses
|
||||
// WHERE
|
||||
// id > $id
|
||||
// ORDER BY id
|
||||
// LIMIT $limit;`
|
||||
//
|
||||
const fields = `id, external_id, source_type, source_name, course_thematic, learning_type, organization_id, origin_link, image_link, name, description, full_price, discount, duration, starts_at, created_at, updated_at, deleted_at`
|
||||
|
||||
if params.Limit == 0 {
|
||||
params.Limit = limit
|
||||
}
|
||||
|
||||
qtParams := queryTemplateParams{
|
||||
Fields: fields,
|
||||
Fields: coursesFieldsStr,
|
||||
Table: "courses",
|
||||
Suffix: "ORDER BY id\nLIMIT $limit",
|
||||
Declares: []queryTemplateDeclaration{{
|
||||
Name: "limit",
|
||||
Type: "Int32",
|
||||
}, {
|
||||
Name: "id",
|
||||
Type: "Text",
|
||||
}},
|
||||
Suffix: "ORDER BY learning_type,course_thematic,id\nLIMIT $limit",
|
||||
Declares: []queryTemplateDeclaration{
|
||||
{
|
||||
Name: "limit",
|
||||
Type: "Int32",
|
||||
},
|
||||
{
|
||||
Name: "id",
|
||||
Type: "Text",
|
||||
},
|
||||
},
|
||||
Conditions: []string{
|
||||
"id > $id",
|
||||
},
|
||||
}
|
||||
|
||||
options := make([]table.ParameterOption, 0, 4)
|
||||
appendParams := func(name string, value string) {
|
||||
opts := make([]table.ParameterOption, 0, 4)
|
||||
appendTextParam := func(name string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
@ -151,18 +171,22 @@ func (r *ydbCourseRepository) List(
|
||||
}
|
||||
qtParams.Declares = append(qtParams.Declares, d)
|
||||
qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg())
|
||||
options = append(options, table.ValueParam(d.Arg(), ydbvalue))
|
||||
opts = append(opts, table.ValueParam(d.Arg(), ydbvalue))
|
||||
}
|
||||
appendParams("course_thematic", params.CourseThematic)
|
||||
appendParams("learning_type", params.LearningType)
|
||||
appendTextParam("course_thematic", params.CourseThematic)
|
||||
appendTextParam("learning_type", params.LearningType)
|
||||
|
||||
var sb strings.Builder
|
||||
err = template.Must(template.New("").Parse(queryTemplateSelect)).Execute(&sb, qtParams)
|
||||
opts = append(
|
||||
opts,
|
||||
table.ValueParam("$limit", types.Int32Value(int32(params.Limit))),
|
||||
)
|
||||
|
||||
query, err := qtParams.render()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("executing template: %w", err)
|
||||
return result, fmt.Errorf("rendering query params: %w", err)
|
||||
}
|
||||
|
||||
query := sb.String()
|
||||
xcontext.LogInfo(ctx, r.log, "query prepared", slog.String("query", query), slog.String("args", tableParamOptsToString(opts...)))
|
||||
|
||||
courses := make([]domain.Course, 0, 1_000)
|
||||
readTx := table.TxControl(
|
||||
@ -171,12 +195,13 @@ func (r *ydbCourseRepository) List(
|
||||
),
|
||||
table.CommitTx(),
|
||||
)
|
||||
|
||||
err = r.db.Table().Do(
|
||||
ctx,
|
||||
func(ctx context.Context, s table.Session) error {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
since := time.Since(start)
|
||||
since := time.Since(start).Truncate(time.Millisecond)
|
||||
xcontext.LogInfo(
|
||||
ctx, r.log,
|
||||
"executed query",
|
||||
@ -185,53 +210,227 @@ func (r *ydbCourseRepository) List(
|
||||
)
|
||||
}()
|
||||
|
||||
var lastKnownID string
|
||||
for {
|
||||
queryParams := table.NewQueryParameters(
|
||||
table.ValueParam("$limit", types.Int32Value(limit)),
|
||||
table.ValueParam("$id", types.TextValue(lastKnownID)),
|
||||
)
|
||||
_, res, err := s.Execute(
|
||||
ctx, readTx, query, queryParams,
|
||||
options.WithCollectStatsModeBasic(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing: %w", err)
|
||||
}
|
||||
queryParams := table.NewQueryParameters(opts...)
|
||||
|
||||
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||
break
|
||||
}
|
||||
|
||||
for res.NextRow() {
|
||||
var cdb courseDB
|
||||
err = res.ScanNamed(cdb.getNamedValues()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scanning row: %w", err)
|
||||
}
|
||||
|
||||
courses = append(courses, mapCourseDB(cdb))
|
||||
}
|
||||
if err = res.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastKnownID = courses[len(courses)-1].ID
|
||||
_, res, err := s.Execute(
|
||||
ctx, readTx, query, queryParams,
|
||||
options.WithCollectStatsModeBasic(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing: %w", err)
|
||||
}
|
||||
|
||||
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for res.NextRow() {
|
||||
var cdb courseDB
|
||||
err = res.ScanNamed(cdb.getNamedValues()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scanning row: %w", err)
|
||||
}
|
||||
|
||||
courses = append(courses, mapCourseDB(cdb))
|
||||
}
|
||||
if err = res.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.NextPageToken = courses[len(courses)-1].ID
|
||||
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(courses)))
|
||||
|
||||
return nil
|
||||
},
|
||||
table.WithIdempotent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return domain.ListCoursesResult{}, err
|
||||
}
|
||||
|
||||
return courses, err
|
||||
result.Courses = courses
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain.Course, err error) {
|
||||
const queryName = "get"
|
||||
func (r *ydbCourseRepository) ListLearningTypes(
|
||||
ctx context.Context,
|
||||
) (result domain.ListLearningTypeResult, err error) {
|
||||
const queryName = "list_learning_type"
|
||||
const querySelect = `SELECT DISTINCT learning_type FROM courses;`
|
||||
|
||||
readTx := table.TxControl(
|
||||
table.BeginTx(
|
||||
table.WithOnlineReadOnly(),
|
||||
),
|
||||
table.CommitTx(),
|
||||
)
|
||||
|
||||
err = r.db.Table().Do(
|
||||
ctx,
|
||||
func(ctx context.Context, s table.Session) error {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
since := time.Since(start).Truncate(time.Millisecond)
|
||||
xcontext.LogInfo(
|
||||
ctx, r.log,
|
||||
"executed query",
|
||||
slog.String("name", queryName),
|
||||
slog.Duration("elapsed", since),
|
||||
)
|
||||
}()
|
||||
|
||||
_, res, err := s.Execute(
|
||||
ctx, readTx, querySelect, table.NewQueryParameters(),
|
||||
options.WithCollectStatsModeNone(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for res.NextRow() {
|
||||
var learningTypeID string
|
||||
if err = res.Scan(&learningTypeID); err != nil {
|
||||
return fmt.Errorf("scanning row: %w", err)
|
||||
}
|
||||
|
||||
result.LearningTypeIDs = append(result.LearningTypeIDs, learningTypeID)
|
||||
}
|
||||
if err = res.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(result.LearningTypeIDs)))
|
||||
|
||||
return nil
|
||||
},
|
||||
table.WithIdempotent(),
|
||||
)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) ListCourseThematics(
|
||||
ctx context.Context,
|
||||
params domain.ListCourseThematicsParams,
|
||||
) (result domain.ListCourseThematicsResult, err error) {
|
||||
const queryName = "list_course_thematics"
|
||||
|
||||
qtParams := queryTemplateParams{
|
||||
Fields: "DISTINCT course_thematic",
|
||||
Table: "courses",
|
||||
Declares: []queryTemplateDeclaration{},
|
||||
Conditions: []string{},
|
||||
}
|
||||
|
||||
learningTypeValue := types.TextValue(params.LearningTypeID)
|
||||
d := queryTemplateDeclaration{
|
||||
Name: "learning_type",
|
||||
Type: learningTypeValue.Type().String(),
|
||||
}
|
||||
qtParams.Declares = append(qtParams.Declares, d)
|
||||
qtParams.Conditions = append(qtParams.Conditions, d.Name+"="+d.Arg())
|
||||
|
||||
opts := []table.ParameterOption{
|
||||
table.ValueParam(d.Arg(), learningTypeValue),
|
||||
}
|
||||
|
||||
query, err := qtParams.render()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("rendering query params: %w", err)
|
||||
}
|
||||
|
||||
readTx := table.TxControl(
|
||||
table.BeginTx(
|
||||
table.WithOnlineReadOnly(),
|
||||
),
|
||||
table.CommitTx(),
|
||||
)
|
||||
|
||||
err = r.db.Table().Do(
|
||||
ctx,
|
||||
func(ctx context.Context, s table.Session) error {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
since := time.Since(start).Truncate(time.Millisecond)
|
||||
xcontext.LogInfo(
|
||||
ctx, r.log,
|
||||
"executed query",
|
||||
slog.String("name", queryName),
|
||||
slog.Duration("elapsed", since),
|
||||
)
|
||||
}()
|
||||
|
||||
_, res, err := s.Execute(
|
||||
ctx, readTx, query, table.NewQueryParameters(opts...),
|
||||
options.WithCollectStatsModeNone(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for res.NextRow() {
|
||||
var courseThematicID string
|
||||
if err = res.Scan(&courseThematicID); err != nil {
|
||||
return fmt.Errorf("scanning row: %w", err)
|
||||
}
|
||||
|
||||
result.CourseThematicIDs = append(result.CourseThematicIDs, courseThematicID)
|
||||
}
|
||||
if err = res.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xcontext.LogDebug(ctx, r.log, "scanned rows", slog.Int("count", len(result.CourseThematicIDs)))
|
||||
|
||||
return nil
|
||||
},
|
||||
table.WithIdempotent(),
|
||||
)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) Get(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (course domain.Course, err error) {
|
||||
const queryName = "get"
|
||||
const querySelect = `DECLARE $id AS Text;
|
||||
SELECT
|
||||
id,
|
||||
external_id,
|
||||
source_type,
|
||||
source_name,
|
||||
course_thematic,
|
||||
learning_type,
|
||||
organization_id,
|
||||
origin_link,
|
||||
image_link,
|
||||
name,
|
||||
description,
|
||||
full_price,
|
||||
discount,
|
||||
duration,
|
||||
starts_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM
|
||||
courses
|
||||
WHERE
|
||||
id = $id;`
|
||||
|
||||
courses := make([]domain.Course, 0, 1)
|
||||
readTx := table.TxControl(
|
||||
table.BeginTx(
|
||||
table.WithOnlineReadOnly(),
|
||||
@ -255,73 +454,79 @@ func (r *ydbCourseRepository) Get(ctx context.Context, id string) (course domain
|
||||
_, res, err := s.Execute(
|
||||
ctx,
|
||||
readTx,
|
||||
`
|
||||
DECLARE $id AS Text;
|
||||
SELECT
|
||||
id,
|
||||
external_id,
|
||||
source_type,
|
||||
source_name,
|
||||
course_thematic,
|
||||
learning_type,
|
||||
organization_id,
|
||||
origin_link,
|
||||
image_link,
|
||||
name,
|
||||
description,
|
||||
full_price,
|
||||
discount,
|
||||
duration,
|
||||
starts_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM
|
||||
courses
|
||||
WHERE
|
||||
id = $id;
|
||||
`,
|
||||
querySelect,
|
||||
table.NewQueryParameters(
|
||||
table.ValueParam("$id", types.TextValue(id)),
|
||||
),
|
||||
options.WithCollectStatsModeBasic(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing: %w", err)
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
for res.NextResultSet(ctx) {
|
||||
for res.NextRow() {
|
||||
var cdb courseDB
|
||||
_ = res.ScanNamed(cdb.getNamedValues()...)
|
||||
courses = append(courses, mapCourseDB(cdb))
|
||||
if !res.NextResultSet(ctx) || !res.HasNextRow() {
|
||||
return errors.ErrNotFound
|
||||
}
|
||||
|
||||
for res.NextRow() {
|
||||
var cdb courseDB
|
||||
err = res.ScanNamed(cdb.getNamedValues()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scanning row: %w", err)
|
||||
}
|
||||
|
||||
course = mapCourseDB(cdb)
|
||||
}
|
||||
if err = res.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stats := res.Stats()
|
||||
xcontext.LogInfo(
|
||||
ctx, r.log, "query stats",
|
||||
slog.String("ast", stats.QueryAST()),
|
||||
slog.String("plan", stats.QueryPlan()),
|
||||
slog.Duration("total_cpu_time", stats.TotalCPUTime()),
|
||||
slog.Duration("total_duration", stats.TotalDuration()),
|
||||
slog.Duration("process_cpu_time", stats.ProcessCPUTime()),
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
table.WithIdempotent())
|
||||
if err != nil {
|
||||
return domain.Course{}, err
|
||||
}
|
||||
|
||||
if len(courses) == 0 {
|
||||
return course, errors.ErrNotFound
|
||||
}
|
||||
|
||||
return courses[0], err
|
||||
table.WithIdempotent(),
|
||||
)
|
||||
return course, err
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
||||
return domain.Course{}, nil
|
||||
}
|
||||
|
||||
func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
|
||||
st := mapSourceTypeFromDomain(params.SourceType)
|
||||
type updateCourseParams struct {
|
||||
domain.CreateCourseParams
|
||||
|
||||
CreatedAt time.Time
|
||||
DeletedAt nullable.Value[time.Time]
|
||||
}
|
||||
|
||||
func updateCourseParamsAsStruct(params updateCourseParams) types.Value {
|
||||
opts := createCourseParamsAsStructValues(params.CreateCourseParams)
|
||||
now := time.Now()
|
||||
return types.StructValue(
|
||||
append(
|
||||
opts[:len(opts)-3],
|
||||
types.StructFieldValue("created_at", types.DatetimeValueFromTime(params.CreatedAt)),
|
||||
types.StructFieldValue("updated_at", types.DatetimeValueFromTime(now)),
|
||||
types.StructFieldValue("deleted_at", types.NullableDatetimeValue(nil)),
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
func createCourseParamsAsStructValues(params domain.CreateCourseParams) []types.StructValueOption {
|
||||
st := mapSourceTypeFromDomain(params.SourceType)
|
||||
now := time.Now()
|
||||
|
||||
return []types.StructValueOption{
|
||||
types.StructFieldValue("id", types.TextValue(params.ID)),
|
||||
types.StructFieldValue("name", types.TextValue(params.Name)),
|
||||
types.StructFieldValue("source_type", types.TextValue(st)),
|
||||
@ -340,11 +545,16 @@ func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
|
||||
types.StructFieldValue("created_at", types.DatetimeValueFromTime(now)),
|
||||
types.StructFieldValue("updated_at", types.DatetimeValueFromTime(now)),
|
||||
types.StructFieldValue("deleted_at", types.NullableDatetimeValue(nil)),
|
||||
}
|
||||
}
|
||||
|
||||
func createCourseParamsAsStruct(params domain.CreateCourseParams) types.Value {
|
||||
return types.StructValue(
|
||||
createCourseParamsAsStructValues(params)...,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) CreateBatch(ctx context.Context, params ...domain.CreateCourseParams) error {
|
||||
// -- PRAGMA TablePathPrefix("courses");
|
||||
const upsertQuery = `DECLARE $courseData AS List<Struct<
|
||||
id: Text,
|
||||
external_id: Optional<Text>,
|
||||
@ -423,6 +633,100 @@ func (r *ydbCourseRepository) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) UpdateCourseDescription(ctx context.Context, id, description string) error {
|
||||
course, err := r.Get(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting course: %w", err)
|
||||
}
|
||||
|
||||
params := updateCourseParams{
|
||||
CreateCourseParams: domain.CreateCourseParams{
|
||||
ID: course.ID,
|
||||
ExternalID: course.ExternalID,
|
||||
Name: course.Name,
|
||||
SourceType: course.SourceType,
|
||||
SourceName: course.SourceName,
|
||||
CourseThematic: course.Thematic,
|
||||
LearningType: course.LearningType,
|
||||
OrganizationID: course.OrganizationID,
|
||||
OriginLink: course.OriginLink,
|
||||
ImageLink: course.ImageLink,
|
||||
Description: description,
|
||||
FullPrice: course.FullPrice,
|
||||
Discount: course.Discount,
|
||||
Duration: course.Duration,
|
||||
StartsAt: course.StartsAt,
|
||||
},
|
||||
CreatedAt: course.CreatedAt,
|
||||
DeletedAt: course.DeletedAt,
|
||||
}
|
||||
|
||||
updateStruct := updateCourseParamsAsStruct(params)
|
||||
|
||||
const upsertQuery = `DECLARE $courseData AS List<Struct<
|
||||
id: Text,
|
||||
external_id: Optional<Text>,
|
||||
name: Text,
|
||||
source_type: Text,
|
||||
source_name: Optional<Text>,
|
||||
course_thematic: Text,
|
||||
learning_type: Text,
|
||||
organization_id: Text,
|
||||
origin_link: Text,
|
||||
image_link: Text,
|
||||
description: Text,
|
||||
full_price: Double,
|
||||
discount: Double,
|
||||
duration: Interval,
|
||||
starts_at: Datetime,
|
||||
created_at: Datetime,
|
||||
updated_at: Datetime,
|
||||
deleted_at: Optional<Datetime>>>;
|
||||
|
||||
REPLACE INTO
|
||||
courses
|
||||
SELECT
|
||||
id,
|
||||
external_id,
|
||||
name,
|
||||
source_type,
|
||||
source_name,
|
||||
course_thematic,
|
||||
learning_type,
|
||||
organization_id,
|
||||
origin_link,
|
||||
image_link,
|
||||
description,
|
||||
full_price,
|
||||
discount,
|
||||
duration,
|
||||
starts_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at
|
||||
FROM AS_TABLE($courseData);`
|
||||
|
||||
writeTx := table.TxControl(
|
||||
table.BeginTx(
|
||||
table.WithSerializableReadWrite(),
|
||||
),
|
||||
table.CommitTx(),
|
||||
)
|
||||
err = r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
|
||||
queryParams := table.NewQueryParameters(
|
||||
table.ValueParam("$courseData", types.ListValue(updateStruct)),
|
||||
)
|
||||
_, _, err := s.Execute(ctx, writeTx, upsertQuery, queryParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ydbCourseRepository) CreateCourseTable(ctx context.Context) error {
|
||||
return r.db.Table().Do(ctx, func(ctx context.Context, s table.Session) error {
|
||||
return s.CreateTable(
|
||||
@ -537,8 +841,8 @@ func mapCourseDB(cdb courseDB) domain.Course {
|
||||
Name: cdb.Name,
|
||||
SourceType: st,
|
||||
SourceName: nullable.NewValuePtr(cdb.SourceName),
|
||||
Thematic: cdb.CourseThematic,
|
||||
LearningType: cdb.LearningType,
|
||||
ThematicID: cdb.CourseThematic,
|
||||
LearningTypeID: cdb.LearningType,
|
||||
OrganizationID: cdb.OrganizationID,
|
||||
OriginLink: cdb.OriginLink,
|
||||
ImageLink: cdb.ImageLink,
|
||||
@ -579,9 +883,27 @@ type queryTemplateParams struct {
|
||||
Suffix string
|
||||
}
|
||||
|
||||
const queryTemplateSelect = `
|
||||
{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}}\n{{end}}
|
||||
func (p queryTemplateParams) render() (string, error) {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(queryTemplateSelect) * 3)
|
||||
|
||||
err := querySelect.Execute(&sb, p)
|
||||
return sb.String(), err
|
||||
}
|
||||
|
||||
const queryTemplateSelect = `{{ range .Declares }}DECLARE ${{.Name}} AS {{.Type}};{{end}}
|
||||
SELECT {{.Fields}}
|
||||
FROM {{.Table}}
|
||||
WHERE {{ range .Conditions }}{{.}}\n{{end}}
|
||||
WHERE 1=1 {{ range .Conditions }} AND {{.}} {{ end }}
|
||||
{{.Suffix}}`
|
||||
|
||||
var querySelect = template.Must(template.New("").Parse(queryTemplateSelect))
|
||||
|
||||
func tableParamOptsToString(in ...table.ParameterOption) string {
|
||||
var sb strings.Builder
|
||||
for _, opt := range in {
|
||||
sb.WriteString(opt.Name() + ":" + opt.Value().Yql() + ";")
|
||||
// sb.WriteString(opt.Name() + " (" + opt.Value().Type().String() + "); ")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@ -6,14 +6,24 @@ import (
|
||||
)
|
||||
|
||||
type Commands struct {
|
||||
InsertCourses command.CreateCoursesHandler
|
||||
InsertCourse command.CreateCourseHandler
|
||||
DeleteCourse command.DeleteCourseHandler
|
||||
InsertCourses command.CreateCoursesHandler
|
||||
InsertCourse command.CreateCourseHandler
|
||||
DeleteCourse command.DeleteCourseHandler
|
||||
UpdateCourseDescription command.UpdateCourseDescriptionHandler
|
||||
|
||||
InsertOrganization command.CreateOrganizationHandler
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
GetCourse query.GetCourseHandler
|
||||
ListCourses query.ListCourseHandler
|
||||
GetCourse query.GetCourseHandler
|
||||
ListCourses query.ListCourseHandler
|
||||
ListLearningTypes query.ListLearningTypesHandler
|
||||
ListCourseThematics query.ListCourseThematicsHandler
|
||||
ListCourseStatistics query.ListCoursesStatsHandler
|
||||
|
||||
ListOrganzations query.ListOrganizationsHandler
|
||||
ListOrganizationsStats query.ListOrganizationsStatsHandler
|
||||
GetOrganization query.GetOrganizationHandler
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
@ -78,7 +78,7 @@ func NewCreateCoursesHandler(
|
||||
}
|
||||
|
||||
func (h createCoursesHandler) Handle(ctx context.Context, cmd CreateCourses) error {
|
||||
params := xslice.Map(cmd.Courses, func(in CreateCourse) (out domain.CreateCourseParams) {
|
||||
params := xslices.Map(cmd.Courses, func(in CreateCourse) (out domain.CreateCourseParams) {
|
||||
return domain.CreateCourseParams(in)
|
||||
})
|
||||
err := h.repo.CreateBatch(ctx, params...)
|
||||
|
||||
53
internal/kurious/app/command/createorganization.go
Normal file
53
internal/kurious/app/command/createorganization.go
Normal file
@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type CreateOrganization struct {
|
||||
ID string
|
||||
ExternalID nullable.Value[string]
|
||||
Alias string
|
||||
Name string
|
||||
Site string
|
||||
Logo string
|
||||
}
|
||||
|
||||
type CreateOrganizationHandler decorator.CommandHandler[CreateOrganization]
|
||||
|
||||
type createOrganizationHandler struct {
|
||||
repo domain.OrganizationRepository
|
||||
}
|
||||
|
||||
func NewCreateOrganizationHandler(
|
||||
repo domain.OrganizationRepository,
|
||||
log *slog.Logger,
|
||||
) CreateOrganizationHandler {
|
||||
h := createOrganizationHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.ApplyCommandDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h createOrganizationHandler) Handle(ctx context.Context, cmd CreateOrganization) error {
|
||||
_, err := h.repo.Create(ctx, domain.CreateOrganizationParams{
|
||||
ID: cmd.ID,
|
||||
ExternalID: cmd.ExternalID,
|
||||
Alias: cmd.Alias,
|
||||
Name: cmd.Name,
|
||||
Site: cmd.Site,
|
||||
LogoLink: cmd.Logo,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating organization: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
internal/kurious/app/command/updatecoursedescription.go
Normal file
35
internal/kurious/app/command/updatecoursedescription.go
Normal file
@ -0,0 +1,35 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type UpdateCourseDescription struct {
|
||||
ID string
|
||||
Description string
|
||||
}
|
||||
|
||||
type UpdateCourseDescriptionHandler decorator.CommandHandler[UpdateCourseDescription]
|
||||
|
||||
type updateCourseDescriptionHandler struct {
|
||||
repo domain.CourseRepository
|
||||
}
|
||||
|
||||
func NewUpdateCourseDescriptionHandler(
|
||||
repo domain.CourseRepository,
|
||||
log *slog.Logger,
|
||||
) UpdateCourseDescriptionHandler {
|
||||
h := updateCourseDescriptionHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.ApplyCommandDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h updateCourseDescriptionHandler) Handle(ctx context.Context, cmd UpdateCourseDescription) error {
|
||||
return h.repo.UpdateCourseDescription(ctx, cmd.ID, cmd.Description)
|
||||
}
|
||||
@ -16,15 +16,18 @@ type GetCourse struct {
|
||||
type GetCourseHandler decorator.QueryHandler[GetCourse, domain.Course]
|
||||
|
||||
type getCourseHandler struct {
|
||||
repo domain.CourseRepository
|
||||
repo domain.CourseRepository
|
||||
mapper domain.CourseMapper
|
||||
}
|
||||
|
||||
func NewGetCourseHandler(
|
||||
repo domain.CourseRepository,
|
||||
mapper domain.CourseMapper,
|
||||
log *slog.Logger,
|
||||
) GetCourseHandler {
|
||||
h := getCourseHandler{
|
||||
repo: repo,
|
||||
repo: repo,
|
||||
mapper: mapper,
|
||||
}
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
@ -35,5 +38,8 @@ func (h getCourseHandler) Handle(ctx context.Context, query GetCourse) (domain.C
|
||||
return domain.Course{}, fmt.Errorf("getting course: %w", err)
|
||||
}
|
||||
|
||||
course.LearningType = h.mapper.LearningTypeNameByID(course.LearningTypeID)
|
||||
course.Thematic = h.mapper.CourseThematicNameByID(course.ThematicID)
|
||||
|
||||
return course, nil
|
||||
}
|
||||
|
||||
45
internal/kurious/app/query/getorganization.go
Normal file
45
internal/kurious/app/query/getorganization.go
Normal file
@ -0,0 +1,45 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type GetOrganization struct {
|
||||
ID nullable.Value[string]
|
||||
ExternalID nullable.Value[string]
|
||||
}
|
||||
|
||||
type GetOrganizationHandler decorator.QueryHandler[GetOrganization, domain.Organization]
|
||||
|
||||
type getOrganizationHandler struct {
|
||||
repo domain.OrganizationRepository
|
||||
}
|
||||
|
||||
func NewGetOrganizationHandler(
|
||||
repo domain.OrganizationRepository,
|
||||
log *slog.Logger,
|
||||
) GetOrganizationHandler {
|
||||
h := getOrganizationHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h getOrganizationHandler) Handle(ctx context.Context, query GetOrganization) (domain.Organization, error) {
|
||||
organization, err := h.repo.Get(ctx, domain.GetOrganizationParams{
|
||||
ID: query.ID,
|
||||
ExternalID: query.ExternalID,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Organization{}, fmt.Errorf("getting organization: %w", err)
|
||||
}
|
||||
|
||||
return organization, nil
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
@ -16,37 +17,73 @@ type ListCourse struct {
|
||||
OrganizationID string
|
||||
Keyword string
|
||||
|
||||
Limit int
|
||||
Offset int
|
||||
OrderBy string
|
||||
Ascending bool
|
||||
|
||||
Limit int
|
||||
Offset int
|
||||
NextPageToken string
|
||||
}
|
||||
|
||||
type ListCourseHandler decorator.QueryHandler[ListCourse, []domain.Course]
|
||||
type ListCourseHandler decorator.QueryHandler[ListCourse, domain.ListCoursesResult]
|
||||
|
||||
type listCourseHandler struct {
|
||||
repo domain.CourseRepository
|
||||
repo domain.CourseRepository
|
||||
mapper domain.CourseMapper
|
||||
}
|
||||
|
||||
func NewListCourseHandler(
|
||||
repo domain.CourseRepository,
|
||||
mapper domain.CourseMapper,
|
||||
log *slog.Logger,
|
||||
) ListCourseHandler {
|
||||
h := listCourseHandler{
|
||||
repo: repo,
|
||||
repo: repo,
|
||||
mapper: mapper,
|
||||
}
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) ([]domain.Course, error) {
|
||||
courses, err := h.repo.List(ctx, domain.ListCoursesParams{
|
||||
CourseThematic: query.CourseThematic,
|
||||
LearningType: query.LearningType,
|
||||
OrganizationID: query.OrganizationID,
|
||||
Limit: query.Limit,
|
||||
Offset: query.Offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing courses: %w", err)
|
||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
||||
const defaultBatchSize = 1_000
|
||||
|
||||
drainFull := query.Limit == 0
|
||||
if !drainFull {
|
||||
out.Courses = make([]domain.Course, 0, query.Limit)
|
||||
} else {
|
||||
query.Limit = defaultBatchSize
|
||||
}
|
||||
for {
|
||||
result, err := h.repo.List(ctx, domain.ListCoursesParams{
|
||||
CourseThematic: query.CourseThematic,
|
||||
LearningType: query.LearningType,
|
||||
OrganizationID: query.OrganizationID,
|
||||
Limit: query.Limit,
|
||||
Offset: query.Offset,
|
||||
OrderBy: query.OrderBy,
|
||||
Ascending: query.Ascending,
|
||||
})
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("listing courses: %w", err)
|
||||
}
|
||||
|
||||
result.Courses = xslices.Map(result.Courses, func(in domain.Course) domain.Course {
|
||||
in.LearningType = h.mapper.LearningTypeNameByID(in.LearningTypeID)
|
||||
in.Thematic = h.mapper.CourseThematicNameByID(in.ThematicID)
|
||||
|
||||
return in
|
||||
})
|
||||
|
||||
out.Courses = append(out.Courses, result.Courses...)
|
||||
out.Count = result.Count
|
||||
|
||||
if drainFull && len(result.Courses) == query.Limit {
|
||||
query.Offset += query.Limit
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
return out, nil
|
||||
}
|
||||
|
||||
69
internal/kurious/app/query/listcoursesstats.go
Normal file
69
internal/kurious/app/query/listcoursesstats.go
Normal file
@ -0,0 +1,69 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListCoursesStats struct {
|
||||
LearningTypeID string
|
||||
CourseThematicsID string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type ListCoursesStatsResult struct{}
|
||||
|
||||
type ListCoursesStatsHandler decorator.QueryHandler[ListCoursesStats, domain.ListCoursesStatsResult]
|
||||
|
||||
type listCoursesStatsHandler struct {
|
||||
repo domain.CourseRepository
|
||||
mapper domain.CourseMapper
|
||||
}
|
||||
|
||||
func NewListCoursesStatsHandler(
|
||||
mapper domain.CourseMapper,
|
||||
repo domain.CourseRepository,
|
||||
log *slog.Logger,
|
||||
) ListCoursesStatsHandler {
|
||||
h := listCoursesStatsHandler{
|
||||
repo: repo,
|
||||
mapper: mapper,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listCoursesStatsHandler) Handle(
|
||||
ctx context.Context,
|
||||
query ListCoursesStats,
|
||||
) (out domain.ListCoursesStatsResult, err error) {
|
||||
if query.OrganizationID != "" {
|
||||
statistics, err := h.repo.ListStatistics(ctx, domain.ListStatisticsParams{
|
||||
LearningTypeID: query.LearningTypeID,
|
||||
CourseThematicID: query.CourseThematicsID,
|
||||
OrganizaitonID: query.OrganizationID,
|
||||
})
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("listing statistics: %w", err)
|
||||
}
|
||||
|
||||
out.StatsByLearningType = make(map[string]domain.LearningTypeStat, len(statistics.LearningTypeStatistics))
|
||||
for _, unit := range statistics.LearningTypeStatistics {
|
||||
stats, ok := out.StatsByLearningType[unit.LearningTypeID]
|
||||
stats.Count++
|
||||
if !ok {
|
||||
stats.CourseThematic = make(map[string]int)
|
||||
}
|
||||
stats.CourseThematic[unit.CourseThematicID]++
|
||||
out.StatsByLearningType[unit.LearningTypeID] = stats
|
||||
}
|
||||
} else {
|
||||
stats := h.mapper.GetStats(false)
|
||||
out.StatsByLearningType = stats
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
62
internal/kurious/app/query/listcoursethematics.go
Normal file
62
internal/kurious/app/query/listcoursethematics.go
Normal file
@ -0,0 +1,62 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListCourseThematics struct {
|
||||
LearningTypeID string
|
||||
}
|
||||
|
||||
type CourseThematic struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
type ListCourseThematicsResult struct {
|
||||
CourseThematics []CourseThematic
|
||||
}
|
||||
|
||||
type ListCourseThematicsHandler decorator.QueryHandler[ListCourseThematics, ListCourseThematicsResult]
|
||||
|
||||
type listCourseThematicsHandler struct {
|
||||
repo domain.CourseRepository
|
||||
mapper domain.CourseMapper
|
||||
}
|
||||
|
||||
func NewListCourseThematicsHandler(
|
||||
repo domain.CourseRepository,
|
||||
mapper domain.CourseMapper,
|
||||
log *slog.Logger,
|
||||
) ListCourseThematicsHandler {
|
||||
h := listCourseThematicsHandler{
|
||||
repo: repo,
|
||||
mapper: mapper,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listCourseThematicsHandler) Handle(ctx context.Context, query ListCourseThematics) (out ListCourseThematicsResult, err error) {
|
||||
result, err := h.repo.ListCourseThematics(ctx, domain.ListCourseThematicsParams{
|
||||
LearningTypeID: query.LearningTypeID,
|
||||
})
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("listing course thematics from repo: %w", err)
|
||||
}
|
||||
|
||||
out.CourseThematics = make([]CourseThematic, 0, len(result.CourseThematicIDs))
|
||||
for _, ct := range result.CourseThematicIDs {
|
||||
var item CourseThematic
|
||||
item.ID = ct
|
||||
item.Name = h.mapper.CourseThematicNameByID(ct)
|
||||
out.CourseThematics = append(out.CourseThematics, item)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
58
internal/kurious/app/query/listlearningtypes.go
Normal file
58
internal/kurious/app/query/listlearningtypes.go
Normal file
@ -0,0 +1,58 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListLearningTypes struct{}
|
||||
|
||||
type LearningType struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
type ListLearningTypesResult struct {
|
||||
LearningTypes []LearningType
|
||||
}
|
||||
|
||||
type ListLearningTypesHandler decorator.QueryHandler[ListLearningTypes, ListLearningTypesResult]
|
||||
|
||||
type listLearningTypesHandler struct {
|
||||
repo domain.CourseRepository
|
||||
mapper domain.CourseMapper
|
||||
}
|
||||
|
||||
func NewListLearningTypesHandler(
|
||||
repo domain.CourseRepository,
|
||||
mapper domain.CourseMapper,
|
||||
log *slog.Logger,
|
||||
) ListLearningTypesHandler {
|
||||
h := listLearningTypesHandler{
|
||||
repo: repo,
|
||||
mapper: mapper,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listLearningTypesHandler) Handle(ctx context.Context, query ListLearningTypes) (out ListLearningTypesResult, err error) {
|
||||
result, err := h.repo.ListLearningTypes(ctx)
|
||||
if err != nil {
|
||||
return out, fmt.Errorf("listing learning types from repo: %w", err)
|
||||
}
|
||||
|
||||
out.LearningTypes = make([]LearningType, 0, len(result.LearningTypeIDs))
|
||||
for _, lt := range result.LearningTypeIDs {
|
||||
var item LearningType
|
||||
item.ID = lt
|
||||
item.Name = h.mapper.LearningTypeNameByID(lt)
|
||||
out.LearningTypes = append(out.LearningTypes, item)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
42
internal/kurious/app/query/listorganizations.go
Normal file
42
internal/kurious/app/query/listorganizations.go
Normal file
@ -0,0 +1,42 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListOrganizations struct {
|
||||
IDs []string
|
||||
}
|
||||
|
||||
type ListOrganizationsHandler decorator.QueryHandler[ListOrganizations, []domain.Organization]
|
||||
|
||||
type listOrganizationsHandler struct {
|
||||
repo domain.OrganizationRepository
|
||||
}
|
||||
|
||||
func NewListOrganizationsHandler(
|
||||
repo domain.OrganizationRepository,
|
||||
log *slog.Logger,
|
||||
) ListOrganizationsHandler {
|
||||
h := listOrganizationsHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listOrganizationsHandler) Handle(ctx context.Context, query ListOrganizations) ([]domain.Organization, error) {
|
||||
organizations, err := h.repo.List(ctx, domain.ListOrganizationsParams{
|
||||
IDs: query.IDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing organizations: %w", err)
|
||||
}
|
||||
|
||||
return organizations, nil
|
||||
}
|
||||
52
internal/kurious/app/query/listorganizationstats.go
Normal file
52
internal/kurious/app/query/listorganizationstats.go
Normal file
@ -0,0 +1,52 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type ListOrganizationsStats struct {
|
||||
LearningTypeID string
|
||||
CourseThematicID string
|
||||
IDs []string
|
||||
}
|
||||
|
||||
type ListOrganizationsStatsHandler decorator.QueryHandler[
|
||||
ListOrganizationsStats,
|
||||
[]domain.OrganizationStat,
|
||||
]
|
||||
|
||||
type listOrganizationsStatsHandler struct {
|
||||
repo domain.OrganizationRepository
|
||||
}
|
||||
|
||||
func NewListOrganizationsStatsHandler(
|
||||
repo domain.OrganizationRepository,
|
||||
log *slog.Logger,
|
||||
) ListOrganizationsStatsHandler {
|
||||
h := listOrganizationsStatsHandler{
|
||||
repo: repo,
|
||||
}
|
||||
|
||||
return decorator.AddQueryDecorators(h, log)
|
||||
}
|
||||
|
||||
func (h listOrganizationsStatsHandler) Handle(
|
||||
ctx context.Context,
|
||||
query ListOrganizationsStats,
|
||||
) ([]domain.OrganizationStat, error) {
|
||||
stats, err := h.repo.ListStats(ctx, domain.ListOrganizationsParams{
|
||||
LearningTypeID: query.LearningTypeID,
|
||||
CourseThematicID: query.CourseThematicID,
|
||||
IDs: query.IDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing stats: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
9
internal/kurious/domain/category.go
Normal file
9
internal/kurious/domain/category.go
Normal file
@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
import "git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
|
||||
type LearningCategory struct {
|
||||
ID string
|
||||
Logo nullable.Value[string]
|
||||
CoursesCount int
|
||||
}
|
||||
@ -27,9 +27,13 @@ type Course struct {
|
||||
// FullPrice is a course full price without discount.
|
||||
FullPrice float64
|
||||
// Discount for the course.
|
||||
Discount float64
|
||||
Thematic string
|
||||
LearningType string
|
||||
Discount float64
|
||||
|
||||
Thematic string
|
||||
ThematicID string
|
||||
|
||||
LearningType string
|
||||
LearningTypeID string
|
||||
|
||||
// Duration for the course. It will be splitted in values like:
|
||||
// full month / full day / full hour.
|
||||
|
||||
12
internal/kurious/domain/error.go
Normal file
12
internal/kurious/domain/error.go
Normal file
@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
ErrNotFound PlainError = "not found"
|
||||
ErrNotImplemented PlainError = "not implemented"
|
||||
)
|
||||
|
||||
type PlainError string
|
||||
|
||||
func (err PlainError) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
12
internal/kurious/domain/mapper.go
Normal file
12
internal/kurious/domain/mapper.go
Normal file
@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type CourseMapper interface {
|
||||
CourseThematicNameByID(string) string
|
||||
LearningTypeNameByID(string) string
|
||||
|
||||
CollectCounts(context.Context, CourseRepository) error
|
||||
GetCounts(byCourseThematic, byLearningType string) int
|
||||
GetStats(copyMap bool) map[string]LearningTypeStat
|
||||
}
|
||||
534
internal/kurious/domain/mocks/CourseRepository.go
Normal file
534
internal/kurious/domain/mocks/CourseRepository.go
Normal file
@ -0,0 +1,534 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
domain "git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// CourseRepository is an autogenerated mock type for the CourseRepository type
|
||||
type CourseRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type CourseRepository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *CourseRepository) EXPECT() *CourseRepository_Expecter {
|
||||
return &CourseRepository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: _a0, _a1
|
||||
func (_m *CourseRepository) Create(_a0 context.Context, _a1 domain.CreateCourseParams) (domain.Course, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 domain.Course
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateCourseParams) (domain.Course, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateCourseParams) domain.Course); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.Course)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, domain.CreateCourseParams) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CourseRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
|
||||
type CourseRepository_Create_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Create is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 domain.CreateCourseParams
|
||||
func (_e *CourseRepository_Expecter) Create(_a0 interface{}, _a1 interface{}) *CourseRepository_Create_Call {
|
||||
return &CourseRepository_Create_Call{Call: _e.mock.On("Create", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Create_Call) Run(run func(_a0 context.Context, _a1 domain.CreateCourseParams)) *CourseRepository_Create_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.CreateCourseParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Create_Call) Return(_a0 domain.Course, _a1 error) *CourseRepository_Create_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Create_Call) RunAndReturn(run func(context.Context, domain.CreateCourseParams) (domain.Course, error)) *CourseRepository_Create_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// CreateBatch provides a mock function with given fields: _a0, _a1
|
||||
func (_m *CourseRepository) CreateBatch(_a0 context.Context, _a1 ...domain.CreateCourseParams) error {
|
||||
_va := make([]interface{}, len(_a1))
|
||||
for _i := range _a1 {
|
||||
_va[_i] = _a1[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, _a0)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateBatch")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, ...domain.CreateCourseParams) error); ok {
|
||||
r0 = rf(_a0, _a1...)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CourseRepository_CreateBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBatch'
|
||||
type CourseRepository_CreateBatch_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateBatch is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 ...domain.CreateCourseParams
|
||||
func (_e *CourseRepository_Expecter) CreateBatch(_a0 interface{}, _a1 ...interface{}) *CourseRepository_CreateBatch_Call {
|
||||
return &CourseRepository_CreateBatch_Call{Call: _e.mock.On("CreateBatch",
|
||||
append([]interface{}{_a0}, _a1...)...)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_CreateBatch_Call) Run(run func(_a0 context.Context, _a1 ...domain.CreateCourseParams)) *CourseRepository_CreateBatch_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]domain.CreateCourseParams, len(args)-1)
|
||||
for i, a := range args[1:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(domain.CreateCourseParams)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_CreateBatch_Call) Return(_a0 error) *CourseRepository_CreateBatch_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_CreateBatch_Call) RunAndReturn(run func(context.Context, ...domain.CreateCourseParams) error) *CourseRepository_CreateBatch_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *CourseRepository) Delete(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CourseRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type CourseRepository_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
func (_e *CourseRepository_Expecter) Delete(ctx interface{}, id interface{}) *CourseRepository_Delete_Call {
|
||||
return &CourseRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Delete_Call) Run(run func(ctx context.Context, id string)) *CourseRepository_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Delete_Call) Return(_a0 error) *CourseRepository_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Delete_Call) RunAndReturn(run func(context.Context, string) error) *CourseRepository_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: ctx, id
|
||||
func (_m *CourseRepository) Get(ctx context.Context, id string) (domain.Course, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 domain.Course
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.Course, error)); ok {
|
||||
return rf(ctx, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) domain.Course); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.Course)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CourseRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
|
||||
type CourseRepository_Get_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Get is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
func (_e *CourseRepository_Expecter) Get(ctx interface{}, id interface{}) *CourseRepository_Get_Call {
|
||||
return &CourseRepository_Get_Call{Call: _e.mock.On("Get", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Get_Call) Run(run func(ctx context.Context, id string)) *CourseRepository_Get_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Get_Call) Return(_a0 domain.Course, _a1 error) *CourseRepository_Get_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_Get_Call) RunAndReturn(run func(context.Context, string) (domain.Course, error)) *CourseRepository_Get_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetByExternalID provides a mock function with given fields: ctx, id
|
||||
func (_m *CourseRepository) GetByExternalID(ctx context.Context, id string) (domain.Course, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetByExternalID")
|
||||
}
|
||||
|
||||
var r0 domain.Course
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.Course, error)); ok {
|
||||
return rf(ctx, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) domain.Course); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.Course)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CourseRepository_GetByExternalID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByExternalID'
|
||||
type CourseRepository_GetByExternalID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetByExternalID is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
func (_e *CourseRepository_Expecter) GetByExternalID(ctx interface{}, id interface{}) *CourseRepository_GetByExternalID_Call {
|
||||
return &CourseRepository_GetByExternalID_Call{Call: _e.mock.On("GetByExternalID", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_GetByExternalID_Call) Run(run func(ctx context.Context, id string)) *CourseRepository_GetByExternalID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_GetByExternalID_Call) Return(_a0 domain.Course, _a1 error) *CourseRepository_GetByExternalID_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_GetByExternalID_Call) RunAndReturn(run func(context.Context, string) (domain.Course, error)) *CourseRepository_GetByExternalID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: _a0, _a1
|
||||
func (_m *CourseRepository) List(_a0 context.Context, _a1 domain.ListCoursesParams) (domain.ListCoursesResult, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 domain.ListCoursesResult
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCoursesParams) (domain.ListCoursesResult, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCoursesParams) domain.ListCoursesResult); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.ListCoursesResult)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, domain.ListCoursesParams) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CourseRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
|
||||
type CourseRepository_List_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// List is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 domain.ListCoursesParams
|
||||
func (_e *CourseRepository_Expecter) List(_a0 interface{}, _a1 interface{}) *CourseRepository_List_Call {
|
||||
return &CourseRepository_List_Call{Call: _e.mock.On("List", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_List_Call) Run(run func(_a0 context.Context, _a1 domain.ListCoursesParams)) *CourseRepository_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.ListCoursesParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_List_Call) Return(_a0 domain.ListCoursesResult, _a1 error) *CourseRepository_List_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_List_Call) RunAndReturn(run func(context.Context, domain.ListCoursesParams) (domain.ListCoursesResult, error)) *CourseRepository_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListCourseThematics provides a mock function with given fields: _a0, _a1
|
||||
func (_m *CourseRepository) ListCourseThematics(_a0 context.Context, _a1 domain.ListCourseThematicsParams) (domain.ListCourseThematicsResult, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListCourseThematics")
|
||||
}
|
||||
|
||||
var r0 domain.ListCourseThematicsResult
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCourseThematicsParams) (domain.ListCourseThematicsResult, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.ListCourseThematicsParams) domain.ListCourseThematicsResult); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.ListCourseThematicsResult)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, domain.ListCourseThematicsParams) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CourseRepository_ListCourseThematics_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListCourseThematics'
|
||||
type CourseRepository_ListCourseThematics_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListCourseThematics is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 domain.ListCourseThematicsParams
|
||||
func (_e *CourseRepository_Expecter) ListCourseThematics(_a0 interface{}, _a1 interface{}) *CourseRepository_ListCourseThematics_Call {
|
||||
return &CourseRepository_ListCourseThematics_Call{Call: _e.mock.On("ListCourseThematics", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_ListCourseThematics_Call) Run(run func(_a0 context.Context, _a1 domain.ListCourseThematicsParams)) *CourseRepository_ListCourseThematics_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.ListCourseThematicsParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_ListCourseThematics_Call) Return(_a0 domain.ListCourseThematicsResult, _a1 error) *CourseRepository_ListCourseThematics_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_ListCourseThematics_Call) RunAndReturn(run func(context.Context, domain.ListCourseThematicsParams) (domain.ListCourseThematicsResult, error)) *CourseRepository_ListCourseThematics_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListLearningTypes provides a mock function with given fields: _a0
|
||||
func (_m *CourseRepository) ListLearningTypes(_a0 context.Context) (domain.ListLearningTypeResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListLearningTypes")
|
||||
}
|
||||
|
||||
var r0 domain.ListLearningTypeResult
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) (domain.ListLearningTypeResult, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) domain.ListLearningTypeResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.ListLearningTypeResult)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CourseRepository_ListLearningTypes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListLearningTypes'
|
||||
type CourseRepository_ListLearningTypes_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListLearningTypes is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
func (_e *CourseRepository_Expecter) ListLearningTypes(_a0 interface{}) *CourseRepository_ListLearningTypes_Call {
|
||||
return &CourseRepository_ListLearningTypes_Call{Call: _e.mock.On("ListLearningTypes", _a0)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_ListLearningTypes_Call) Run(run func(_a0 context.Context)) *CourseRepository_ListLearningTypes_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_ListLearningTypes_Call) Return(_a0 domain.ListLearningTypeResult, _a1 error) *CourseRepository_ListLearningTypes_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_ListLearningTypes_Call) RunAndReturn(run func(context.Context) (domain.ListLearningTypeResult, error)) *CourseRepository_ListLearningTypes_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateCourseDescription provides a mock function with given fields: ctx, id, description
|
||||
func (_m *CourseRepository) UpdateCourseDescription(ctx context.Context, id string, description string) error {
|
||||
ret := _m.Called(ctx, id, description)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateCourseDescription")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, id, description)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CourseRepository_UpdateCourseDescription_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCourseDescription'
|
||||
type CourseRepository_UpdateCourseDescription_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateCourseDescription is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
// - description string
|
||||
func (_e *CourseRepository_Expecter) UpdateCourseDescription(ctx interface{}, id interface{}, description interface{}) *CourseRepository_UpdateCourseDescription_Call {
|
||||
return &CourseRepository_UpdateCourseDescription_Call{Call: _e.mock.On("UpdateCourseDescription", ctx, id, description)}
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_UpdateCourseDescription_Call) Run(run func(ctx context.Context, id string, description string)) *CourseRepository_UpdateCourseDescription_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_UpdateCourseDescription_Call) Return(_a0 error) *CourseRepository_UpdateCourseDescription_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CourseRepository_UpdateCourseDescription_Call) RunAndReturn(run func(context.Context, string, string) error) *CourseRepository_UpdateCourseDescription_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewCourseRepository creates a new instance of CourseRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewCourseRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *CourseRepository {
|
||||
mock := &CourseRepository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
199
internal/kurious/domain/mocks/LearningCategoryRepository.go
Normal file
199
internal/kurious/domain/mocks/LearningCategoryRepository.go
Normal file
@ -0,0 +1,199 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
domain "git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// LearningCategoryRepository is an autogenerated mock type for the LearningCategoryRepository type
|
||||
type LearningCategoryRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type LearningCategoryRepository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *LearningCategoryRepository) EXPECT() *LearningCategoryRepository_Expecter {
|
||||
return &LearningCategoryRepository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: _a0, _a1
|
||||
func (_m *LearningCategoryRepository) Get(_a0 context.Context, _a1 string) (domain.LearningCategory, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 domain.LearningCategory
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (domain.LearningCategory, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) domain.LearningCategory); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.LearningCategory)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LearningCategoryRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
|
||||
type LearningCategoryRepository_Get_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Get is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 string
|
||||
func (_e *LearningCategoryRepository_Expecter) Get(_a0 interface{}, _a1 interface{}) *LearningCategoryRepository_Get_Call {
|
||||
return &LearningCategoryRepository_Get_Call{Call: _e.mock.On("Get", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_Get_Call) Run(run func(_a0 context.Context, _a1 string)) *LearningCategoryRepository_Get_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_Get_Call) Return(_a0 domain.LearningCategory, _a1 error) *LearningCategoryRepository_Get_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_Get_Call) RunAndReturn(run func(context.Context, string) (domain.LearningCategory, error)) *LearningCategoryRepository_Get_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: _a0
|
||||
func (_m *LearningCategoryRepository) List(_a0 context.Context) ([]domain.LearningCategory, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []domain.LearningCategory
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]domain.LearningCategory, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []domain.LearningCategory); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]domain.LearningCategory)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LearningCategoryRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
|
||||
type LearningCategoryRepository_List_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// List is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
func (_e *LearningCategoryRepository_Expecter) List(_a0 interface{}) *LearningCategoryRepository_List_Call {
|
||||
return &LearningCategoryRepository_List_Call{Call: _e.mock.On("List", _a0)}
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_List_Call) Run(run func(_a0 context.Context)) *LearningCategoryRepository_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_List_Call) Return(_a0 []domain.LearningCategory, _a1 error) *LearningCategoryRepository_List_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_List_Call) RunAndReturn(run func(context.Context) ([]domain.LearningCategory, error)) *LearningCategoryRepository_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Upsert provides a mock function with given fields: _a0, _a1
|
||||
func (_m *LearningCategoryRepository) Upsert(_a0 context.Context, _a1 domain.LearningCategory) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Upsert")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.LearningCategory) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LearningCategoryRepository_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert'
|
||||
type LearningCategoryRepository_Upsert_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Upsert is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 domain.LearningCategory
|
||||
func (_e *LearningCategoryRepository_Expecter) Upsert(_a0 interface{}, _a1 interface{}) *LearningCategoryRepository_Upsert_Call {
|
||||
return &LearningCategoryRepository_Upsert_Call{Call: _e.mock.On("Upsert", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_Upsert_Call) Run(run func(_a0 context.Context, _a1 domain.LearningCategory)) *LearningCategoryRepository_Upsert_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.LearningCategory))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_Upsert_Call) Return(_a0 error) *LearningCategoryRepository_Upsert_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LearningCategoryRepository_Upsert_Call) RunAndReturn(run func(context.Context, domain.LearningCategory) error) *LearningCategoryRepository_Upsert_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewLearningCategoryRepository creates a new instance of LearningCategoryRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewLearningCategoryRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *LearningCategoryRepository {
|
||||
mock := &LearningCategoryRepository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
256
internal/kurious/domain/mocks/OrganizationRepository.go
Normal file
256
internal/kurious/domain/mocks/OrganizationRepository.go
Normal file
@ -0,0 +1,256 @@
|
||||
// Code generated by mockery v2.42.1. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
domain "git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// OrganizationRepository is an autogenerated mock type for the OrganizationRepository type
|
||||
type OrganizationRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type OrganizationRepository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *OrganizationRepository) EXPECT() *OrganizationRepository_Expecter {
|
||||
return &OrganizationRepository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: _a0, _a1
|
||||
func (_m *OrganizationRepository) Create(_a0 context.Context, _a1 domain.CreateOrganizationParams) (domain.Organization, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Create")
|
||||
}
|
||||
|
||||
var r0 domain.Organization
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateOrganizationParams) (domain.Organization, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.CreateOrganizationParams) domain.Organization); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.Organization)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, domain.CreateOrganizationParams) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OrganizationRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
|
||||
type OrganizationRepository_Create_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Create is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 domain.CreateOrganizationParams
|
||||
func (_e *OrganizationRepository_Expecter) Create(_a0 interface{}, _a1 interface{}) *OrganizationRepository_Create_Call {
|
||||
return &OrganizationRepository_Create_Call{Call: _e.mock.On("Create", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Create_Call) Run(run func(_a0 context.Context, _a1 domain.CreateOrganizationParams)) *OrganizationRepository_Create_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.CreateOrganizationParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Create_Call) Return(_a0 domain.Organization, _a1 error) *OrganizationRepository_Create_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Create_Call) RunAndReturn(run func(context.Context, domain.CreateOrganizationParams) (domain.Organization, error)) *OrganizationRepository_Create_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: ctx, id
|
||||
func (_m *OrganizationRepository) Delete(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// OrganizationRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type OrganizationRepository_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
func (_e *OrganizationRepository_Expecter) Delete(ctx interface{}, id interface{}) *OrganizationRepository_Delete_Call {
|
||||
return &OrganizationRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Delete_Call) Run(run func(ctx context.Context, id string)) *OrganizationRepository_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Delete_Call) Return(_a0 error) *OrganizationRepository_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Delete_Call) RunAndReturn(run func(context.Context, string) error) *OrganizationRepository_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: _a0, _a1
|
||||
func (_m *OrganizationRepository) Get(_a0 context.Context, _a1 domain.GetOrganizationParams) (domain.Organization, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Get")
|
||||
}
|
||||
|
||||
var r0 domain.Organization
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.GetOrganizationParams) (domain.Organization, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.GetOrganizationParams) domain.Organization); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Get(0).(domain.Organization)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, domain.GetOrganizationParams) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OrganizationRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
|
||||
type OrganizationRepository_Get_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Get is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
// - _a1 domain.GetOrganizationParams
|
||||
func (_e *OrganizationRepository_Expecter) Get(_a0 interface{}, _a1 interface{}) *OrganizationRepository_Get_Call {
|
||||
return &OrganizationRepository_Get_Call{Call: _e.mock.On("Get", _a0, _a1)}
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Get_Call) Run(run func(_a0 context.Context, _a1 domain.GetOrganizationParams)) *OrganizationRepository_Get_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.GetOrganizationParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Get_Call) Return(_a0 domain.Organization, _a1 error) *OrganizationRepository_Get_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_Get_Call) RunAndReturn(run func(context.Context, domain.GetOrganizationParams) (domain.Organization, error)) *OrganizationRepository_Get_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: _a0
|
||||
func (_m *OrganizationRepository) List(_a0 context.Context) ([]domain.Organization, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 []domain.Organization
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) ([]domain.Organization, error)); ok {
|
||||
return rf(_a0)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context) []domain.Organization); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]domain.Organization)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OrganizationRepository_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List'
|
||||
type OrganizationRepository_List_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// List is a helper method to define mock.On call
|
||||
// - _a0 context.Context
|
||||
func (_e *OrganizationRepository_Expecter) List(_a0 interface{}) *OrganizationRepository_List_Call {
|
||||
return &OrganizationRepository_List_Call{Call: _e.mock.On("List", _a0)}
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_List_Call) Run(run func(_a0 context.Context)) *OrganizationRepository_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_List_Call) Return(_a0 []domain.Organization, _a1 error) *OrganizationRepository_List_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *OrganizationRepository_List_Call) RunAndReturn(run func(context.Context) ([]domain.Organization, error)) *OrganizationRepository_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewOrganizationRepository creates a new instance of OrganizationRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewOrganizationRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *OrganizationRepository {
|
||||
mock := &OrganizationRepository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@ -20,3 +20,11 @@ type Organization struct {
|
||||
UpdatedAt time.Time
|
||||
DeletedAt nullable.Value[time.Time]
|
||||
}
|
||||
|
||||
type OrganizationStat struct {
|
||||
ID string
|
||||
ExternalID nullable.Value[string]
|
||||
Name string
|
||||
|
||||
CoursesCount uint64
|
||||
}
|
||||
|
||||
@ -12,9 +12,16 @@ type ListCoursesParams struct {
|
||||
CourseThematic string
|
||||
OrganizationID string
|
||||
|
||||
NextPageToken string
|
||||
Limit int
|
||||
Offset int
|
||||
Limit int
|
||||
Offset int
|
||||
OrderBy string
|
||||
Ascending bool
|
||||
}
|
||||
|
||||
type ListStatisticsParams struct {
|
||||
LearningTypeID string
|
||||
CourseThematicID string
|
||||
OrganizaitonID string
|
||||
}
|
||||
|
||||
type CreateCourseParams struct {
|
||||
@ -35,15 +42,51 @@ type CreateCourseParams struct {
|
||||
StartsAt time.Time
|
||||
}
|
||||
|
||||
type LearningTypeStat struct {
|
||||
Count int
|
||||
CourseThematic map[string]int
|
||||
}
|
||||
|
||||
type ListCoursesStatsResult struct {
|
||||
StatsByLearningType map[string]LearningTypeStat
|
||||
}
|
||||
|
||||
type ListCoursesResult struct {
|
||||
Courses []Course
|
||||
NextPageToken string
|
||||
Count int
|
||||
}
|
||||
|
||||
type ListLearningTypeResult struct {
|
||||
LearningTypeIDs []string
|
||||
}
|
||||
|
||||
type ListCourseThematicsParams struct {
|
||||
LearningTypeID string
|
||||
}
|
||||
|
||||
type ListCourseThematicsResult struct {
|
||||
CourseThematicIDs []string
|
||||
}
|
||||
|
||||
type StatisticUnit struct {
|
||||
LearningTypeID string
|
||||
CourseThematicID string
|
||||
OrganizationID string
|
||||
Count int
|
||||
}
|
||||
|
||||
type ListStatisticsResult struct {
|
||||
LearningTypeStatistics []StatisticUnit
|
||||
}
|
||||
|
||||
//go:generate mockery --name CourseRepository
|
||||
type CourseRepository interface {
|
||||
// List courses by specifid parameters.
|
||||
List(ctx context.Context, params ListCoursesParams) (ListCoursesResult, error)
|
||||
List(context.Context, ListCoursesParams) (ListCoursesResult, error)
|
||||
ListLearningTypes(context.Context) (ListLearningTypeResult, error)
|
||||
ListCourseThematics(context.Context, ListCourseThematicsParams) (ListCourseThematicsResult, error)
|
||||
ListStatistics(context.Context, ListStatisticsParams) (ListStatisticsResult, error)
|
||||
// Get course by id.
|
||||
// Should return ErrNotFound in case course not found.
|
||||
Get(ctx context.Context, id string) (Course, error)
|
||||
@ -57,6 +100,14 @@ type CourseRepository interface {
|
||||
Create(context.Context, CreateCourseParams) (Course, error)
|
||||
// Delete course by id.
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// UpdateCourseDescription is a temporary method to udpate description field
|
||||
UpdateCourseDescription(ctx context.Context, id string, description string) error
|
||||
}
|
||||
|
||||
type GetOrganizationParams struct {
|
||||
ID nullable.Value[string]
|
||||
ExternalID nullable.Value[string]
|
||||
}
|
||||
|
||||
type CreateOrganizationParams struct {
|
||||
@ -69,9 +120,59 @@ type CreateOrganizationParams struct {
|
||||
LogoLink string
|
||||
}
|
||||
|
||||
type ListOrganizationsParams struct {
|
||||
LearningTypeID string
|
||||
CourseThematicID string
|
||||
IDs []string
|
||||
}
|
||||
|
||||
//go:generate mockery --name OrganizationRepository
|
||||
type OrganizationRepository interface {
|
||||
Get(ctx context.Context) (Organization, error)
|
||||
ListStats(context.Context, ListOrganizationsParams) ([]OrganizationStat, error)
|
||||
List(context.Context, ListOrganizationsParams) ([]Organization, error)
|
||||
Get(context.Context, GetOrganizationParams) (Organization, error)
|
||||
Create(context.Context, CreateOrganizationParams) (Organization, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
type NotImplementedOrganizationRepository struct{}
|
||||
|
||||
func (NotImplementedOrganizationRepository) ListStats(
|
||||
context.Context,
|
||||
ListOrganizationsParams,
|
||||
) ([]OrganizationStat, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
func (NotImplementedOrganizationRepository) List(context.Context, ListOrganizationsParams) ([]Organization, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
func (NotImplementedOrganizationRepository) Get(context.Context, GetOrganizationParams) (Organization, error) {
|
||||
return Organization{}, ErrNotImplemented
|
||||
}
|
||||
func (NotImplementedOrganizationRepository) Create(context.Context, CreateOrganizationParams) (Organization, error) {
|
||||
return Organization{}, ErrNotImplemented
|
||||
}
|
||||
func (NotImplementedOrganizationRepository) Delete(ctx context.Context, id string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
//go:generate mockery --name LearningCategoryRepository
|
||||
type LearningCategoryRepository interface {
|
||||
Upsert(context.Context, LearningCategory) error
|
||||
|
||||
List(context.Context) ([]LearningCategory, error)
|
||||
Get(context.Context, string) (LearningCategory, error)
|
||||
}
|
||||
|
||||
type NotImplementedLearningCategory struct{}
|
||||
|
||||
func (NotImplementedLearningCategory) Upsert(context.Context, LearningCategory) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
func (NotImplementedLearningCategory) List(context.Context) ([]LearningCategory, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
func (NotImplementedLearningCategory) Get(context.Context, string) (LearningCategory, error) {
|
||||
return LearningCategory{}, ErrNotImplemented
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
"git.loyso.art/frx/kurious/internal/common/generator"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
@ -34,8 +34,9 @@ type syncSravniHandler struct {
|
||||
client sravni.Client
|
||||
log *slog.Logger
|
||||
|
||||
knownExternalIDs map[string]struct{}
|
||||
isRunning uint32
|
||||
knownExternalIDs map[string]struct{}
|
||||
knownOrganizationsByExternalID map[string]struct{}
|
||||
isRunning uint32
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
|
||||
@ -83,6 +84,7 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
|
||||
learningTypes := state.Props.InitialReduxState.Dictionaries.Data.LearningType
|
||||
courses := make([]sravni.Course, 0, 1024)
|
||||
buffer := make([]sravni.Course, 0, 512)
|
||||
organizations := make([]sravni.Organization, 0, 256)
|
||||
for _, learningType := range learningTypes.Fields {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@ -115,61 +117,98 @@ func (h *syncSravniHandler) Handle(ctx context.Context) (err error) {
|
||||
|
||||
// since count is known it might be optimized to allocate slice once per request.
|
||||
for _, courseThematic := range thematics {
|
||||
var filteredCourses int
|
||||
var filteredOrgs int
|
||||
|
||||
var orgsByID map[string]sravni.Organization
|
||||
buffer = buffer[:0]
|
||||
buffer, err = h.loadEducationalProducts(lctx, learningType.Value, courseThematic, buffer)
|
||||
buffer, orgsByID, err = h.loadEducationalProducts(lctx, learningType.Value, courseThematic, buffer)
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
xcontext.LogWithWarnError(lctx, h.log, err, "unable to load educational products", slog.Int("count", len(thematics)))
|
||||
continue
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return fmt.Errorf("loading educational products: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("loading educational products: %w", err)
|
||||
xcontext.LogWithWarnError(lctx, h.log, err, "unable to load educational products", slog.Int("count", len(thematics)))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
xslice.ForEach(buffer, func(c sravni.Course) {
|
||||
xslices.ForEach(buffer, func(c sravni.Course) {
|
||||
// TODO: if the same course appears in different categories, it should be handled
|
||||
if !h.setCourseIfNotKnown(c) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Learningtype = []string{learningType.Value}
|
||||
c.CourseThematics = []string{courseThematic}
|
||||
courses = append(courses, c)
|
||||
filteredCourses++
|
||||
})
|
||||
|
||||
xcontext.LogInfo(lctx, h.log, "parsed subitems", slog.String("course_thematic", courseThematic), slog.Int("amount", len(buffer)))
|
||||
for _, org := range orgsByID {
|
||||
if !h.setOrganizationIfNotKnown(org) {
|
||||
continue
|
||||
}
|
||||
|
||||
organizations = append(organizations, org)
|
||||
filteredOrgs++
|
||||
}
|
||||
|
||||
xcontext.LogInfo(
|
||||
lctx, h.log, "parsed subitems",
|
||||
slog.String("course_thematic", courseThematic),
|
||||
slog.Int("amount", len(buffer)),
|
||||
slog.Int("new_courses", filteredCourses),
|
||||
slog.Int("new_organizations", filteredOrgs),
|
||||
)
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
xcontext.LogDebug(lctx, h.log, "parsed items", slog.Duration("elapsed", elapsed), slog.Int("amount", len(courses)))
|
||||
|
||||
// TODO: if the same course appears in different categories, it should be handled
|
||||
courses = h.filterByCache(courses)
|
||||
if len(courses) == 0 {
|
||||
xcontext.LogInfo(lctx, h.log, "all courses were filtered out")
|
||||
continue
|
||||
xcontext.LogDebug(
|
||||
lctx, h.log, "filtered items",
|
||||
slog.Int("courses", len(courses)),
|
||||
slog.Int("organizations", len(organizations)),
|
||||
)
|
||||
|
||||
var insertCourseSuccess bool
|
||||
if len(courses) > 0 {
|
||||
err = h.insertCourses(lctx, courses)
|
||||
if err != nil {
|
||||
xcontext.LogWithError(lctx, h.log, err, "unable to insert courses")
|
||||
}
|
||||
|
||||
insertCourseSuccess = err == nil
|
||||
}
|
||||
|
||||
xcontext.LogDebug(lctx, h.log, "filtered items", slog.Int("amount", len(courses)))
|
||||
var insertOrgsSuccess bool
|
||||
if len(organizations) > 0 {
|
||||
err = h.insertOrganizations(lctx, organizations)
|
||||
if err != nil {
|
||||
xcontext.LogWithError(lctx, h.log, err, "unable to insert courses")
|
||||
}
|
||||
|
||||
insertOrgsSuccess = err == nil
|
||||
}
|
||||
|
||||
err = h.insertValues(lctx, courses)
|
||||
elapsed = time.Since(start) - elapsed
|
||||
elapsedField := slog.Duration("elapsed", elapsed)
|
||||
if err != nil {
|
||||
xcontext.LogWithError(lctx, h.log, err, "unable to insert courses", elapsedField)
|
||||
continue
|
||||
}
|
||||
|
||||
xslice.ForEach(courses, func(c sravni.Course) {
|
||||
h.knownExternalIDs[c.ID] = struct{}{}
|
||||
})
|
||||
|
||||
xcontext.LogInfo(
|
||||
lctx, h.log, "processed items",
|
||||
lctx, h.log, "inserting finished",
|
||||
elapsedField,
|
||||
slog.Int("count", len(courses)),
|
||||
slog.Bool("courses_insert_success", insertCourseSuccess),
|
||||
slog.Bool("organization_insert_success", insertOrgsSuccess),
|
||||
slog.Int("courses_count", len(courses)),
|
||||
slog.Int("organizations_count", len(organizations)),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learningType, courseThematic string, buf []sravni.Course) ([]sravni.Course, error) {
|
||||
func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learningType, courseThematic string, buf []sravni.Course) ([]sravni.Course, map[string]sravni.Organization, error) {
|
||||
const maxDeepIteration = 10
|
||||
const defaultLimit = 50
|
||||
|
||||
@ -177,6 +216,7 @@ func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learnin
|
||||
rateLimit := rate.NewLimiter(rateStrategy, 1)
|
||||
|
||||
var courses []sravni.Course
|
||||
var organizationsByID = make(map[string]sravni.Organization)
|
||||
if buf == nil || cap(buf) == 0 {
|
||||
courses = make([]sravni.Course, 0, 256)
|
||||
} else {
|
||||
@ -193,36 +233,50 @@ func (h *syncSravniHandler) loadEducationalProducts(ctx context.Context, learnin
|
||||
params.Offset = offset
|
||||
response, err := h.client.ListEducationalProducts(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing educational products: %w", err)
|
||||
return nil, nil, fmt.Errorf("listing educational products: %w", err)
|
||||
}
|
||||
|
||||
offset += defaultLimit
|
||||
|
||||
courses = append(courses, response.Items...)
|
||||
|
||||
for oid, org := range response.Organizations {
|
||||
organizationsByID[oid] = org
|
||||
}
|
||||
|
||||
if len(response.Items) < defaultLimit {
|
||||
break
|
||||
}
|
||||
|
||||
err = rateLimit.Wait(ctx)
|
||||
if err != nil {
|
||||
return courses, fmt.Errorf("waiting for limit: %w", err)
|
||||
return courses, organizationsByID, fmt.Errorf("waiting for limit: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
return courses, organizationsByID, nil
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) filterByCache(courses []sravni.Course) (toInsert []sravni.Course) {
|
||||
toCut := xslice.FilterInplace(courses, xslice.Not(h.isCached))
|
||||
return courses[:toCut]
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) isCached(course sravni.Course) bool {
|
||||
func (h *syncSravniHandler) setCourseIfNotKnown(course sravni.Course) (set bool) {
|
||||
_, ok := h.knownExternalIDs[course.ID]
|
||||
return ok
|
||||
if !ok {
|
||||
h.knownExternalIDs[course.ID] = struct{}{}
|
||||
}
|
||||
|
||||
return !ok
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) insertValues(ctx context.Context, courses []sravni.Course) error {
|
||||
courseParams := xslice.Map(courses, courseAsCreateCourseParams)
|
||||
func (h *syncSravniHandler) setOrganizationIfNotKnown(organization sravni.Organization) bool {
|
||||
_, ok := h.knownOrganizationsByExternalID[organization.ID]
|
||||
if !ok {
|
||||
h.knownOrganizationsByExternalID[organization.ID] = struct{}{}
|
||||
}
|
||||
|
||||
return !ok
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) insertCourses(ctx context.Context, courses []sravni.Course) error {
|
||||
courseParams := xslices.Map(courses, courseAsCreateCourseParams)
|
||||
err := h.svc.Commands.InsertCourses.Handle(ctx, command.CreateCourses{
|
||||
Courses: courseParams,
|
||||
})
|
||||
@ -233,28 +287,92 @@ func (h *syncSravniHandler) insertValues(ctx context.Context, courses []sravni.C
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) insertOrganizations(ctx context.Context, organizations []sravni.Organization) error {
|
||||
organizationParams := xslices.Map(organizations, func(in sravni.Organization) command.CreateOrganization {
|
||||
return command.CreateOrganization{
|
||||
ID: generator.RandomInt64ID(),
|
||||
ExternalID: nullable.NewValue(in.ID),
|
||||
Alias: in.Alias,
|
||||
Name: in.Name.Short,
|
||||
Site: "",
|
||||
Logo: in.Logotypes.Web,
|
||||
}
|
||||
})
|
||||
|
||||
for _, params := range organizationParams {
|
||||
err := h.svc.Commands.InsertOrganization.Handle(ctx, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting organization: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) fillOrganizaionCaches(ctx context.Context) error {
|
||||
if h.knownOrganizationsByExternalID != nil {
|
||||
xcontext.LogDebug(ctx, h.log, "organization cache already filled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
organizations, err := h.svc.Queries.ListOrganzations.Handle(ctx, query.ListOrganizations{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing organizations: %w", err)
|
||||
}
|
||||
|
||||
withExternalID := func(in domain.Organization) bool {
|
||||
return in.ExternalID.Valid()
|
||||
}
|
||||
getExtID := func(in domain.Organization) string {
|
||||
return in.ExternalID.Value()
|
||||
}
|
||||
|
||||
h.knownOrganizationsByExternalID = xslices.AsMap(xslices.Filter(organizations, withExternalID), getExtID)
|
||||
|
||||
xcontext.LogInfo(ctx, h.log, "cache filled", slog.String("kind", "organizations_by_external_id"), slog.Int("count", len(organizations)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) fillCaches(ctx context.Context) error {
|
||||
err := h.fillOrganizaionCaches(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = h.fillKnownExternalIDsCache(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *syncSravniHandler) fillKnownExternalIDsCache(ctx context.Context) error {
|
||||
if h.knownExternalIDs != nil {
|
||||
xcontext.LogInfo(ctx, h.log, "cache already filled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
courses, err := h.svc.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
||||
result, err := h.svc.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing courses: %w", err)
|
||||
}
|
||||
|
||||
courses := result.Courses
|
||||
|
||||
h.knownExternalIDs = make(map[string]struct{}, len(courses))
|
||||
|
||||
xslice.ForEach(courses, func(c domain.Course) {
|
||||
xslices.ForEach(courses, func(c domain.Course) {
|
||||
if !c.ExternalID.Valid() {
|
||||
return
|
||||
}
|
||||
h.knownExternalIDs[c.ExternalID.Value()] = struct{}{}
|
||||
})
|
||||
|
||||
xcontext.LogInfo(ctx, h.log, "cache filled", slog.Int("count", len(courses)))
|
||||
xcontext.LogInfo(ctx, h.log, "cache filled", slog.String("kind", "courses_by_external_id"), slog.Int("count", len(courses)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
21
internal/kurious/ports/http/bootstrap/common.templ
Normal file
21
internal/kurious/ports/http/bootstrap/common.templ
Normal file
@ -0,0 +1,21 @@
|
||||
package bootstrap
|
||||
|
||||
templ button(title string, attributes templ.Attributes) {
|
||||
<button class="button" { attributes... }>{ title }</button>
|
||||
}
|
||||
|
||||
templ buttonRedirect(id, title string, linkTo string) {
|
||||
<button
|
||||
class="button"
|
||||
id={ "origin-link-" + id }
|
||||
>
|
||||
{ title }
|
||||
</button>
|
||||
@onclickRedirect("origin-link-" + id, linkTo)
|
||||
}
|
||||
|
||||
script onclickRedirect(id, to string) {
|
||||
document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
}
|
||||
}
|
||||
122
internal/kurious/ports/http/bootstrap/common_templ.go
Normal file
122
internal/kurious/ports/http/bootstrap/common_templ.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.707
|
||||
package bootstrap
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
func button(title string, attributes templ.Attributes) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, attributes)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/common.templ`, Line: 4, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func buttonRedirect(id, title string, linkTo string) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"button\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("origin-link-" + id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/common.templ`, Line: 10, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/common.templ`, Line: 12, Col: 9}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = onclickRedirect("origin-link-"+id, linkTo).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func onclickRedirect(id, to string) templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_onclickRedirect_5c43`,
|
||||
Function: `function __templ_onclickRedirect_5c43(id, to){document.getElementById(id).onclick = () => {
|
||||
location.href = to
|
||||
}
|
||||
}`,
|
||||
Call: templ.SafeScript(`__templ_onclickRedirect_5c43`, id, to),
|
||||
CallInline: templ.SafeScriptInline(`__templ_onclickRedirect_5c43`, id, to),
|
||||
}
|
||||
}
|
||||
236
internal/kurious/ports/http/bootstrap/core.templ
Normal file
236
internal/kurious/ports/http/bootstrap/core.templ
Normal file
@ -0,0 +1,236 @@
|
||||
package bootstrap
|
||||
|
||||
templ head(title string) {
|
||||
<head>
|
||||
<title>{ title }</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous">
|
||||
</script>
|
||||
</head>
|
||||
}
|
||||
|
||||
templ headerNavbar(page PageKind) {
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary w-auto">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/htmlexamples/index.html">Kurious</a>
|
||||
<button
|
||||
class="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class={ "nav-link", templ.KV("active", page == PageIndex) }
|
||||
aria-current="page"
|
||||
href="/"
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class={ "nav-link", templ.KV("active", page == PageCourses) }
|
||||
aria-current="page"
|
||||
href="/courses"
|
||||
>
|
||||
Courses
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class={ "nav-link", templ.KV("active", page == PageAbout) }
|
||||
href="/about"
|
||||
>
|
||||
About us
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
}
|
||||
|
||||
templ footer() {
|
||||
<footer class="text-center text-lg-start bg-body-tertiary text-muted">
|
||||
<section class="p-2">
|
||||
<div class="container text-center text-md-start mt-5">
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-3 col-lg-4 col-xl-3 mx-auto mb-4">
|
||||
<h6 class="text-uppercase fw-bold mb-4">
|
||||
<i class="fas fa-gem me-3"></i>Courses
|
||||
</h6>
|
||||
<p>
|
||||
Welcome to Courses, your gateway to learning! Explore a diverse
|
||||
range of courses and advance your skills with us. Join our
|
||||
community and transform your life through education.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3 col-lg-2 col-xl-2 mx-auto mb-4">
|
||||
<h6 class="text-uppercase fw-bold mb-4">Useful links</h6>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Pricing</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Settings</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Orders</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#!" class="text-reset">Help</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4">
|
||||
<h6 class="text-uppercase fw-bold mb-4">Contact</h6>
|
||||
<p><i class="fas fa-home me-3"></i> New York, NY 10012, US</p>
|
||||
<p>
|
||||
<i class="fas fa-envelope me-3"></i>
|
||||
info@example.com
|
||||
</p>
|
||||
<p><i class="fas fa-phone me-3"></i> + 01 234 567 88</p>
|
||||
<p><i class="fas fa-print me-3"></i> + 01 234 567 89</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="text-center p-4" style="background-color: rgba(0, 0, 0, 0.05)">
|
||||
© 2024 Copyright:
|
||||
<a class="text-reset fw-bold" href="https://mdbootstrap.com/">kursov.net</a>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
script elementScriptsLoad() {
|
||||
const loadInputValues = () => {
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
|
||||
const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
|
||||
const base_url = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
|
||||
|
||||
const school_selector = document.getElementById('schoolSelect');
|
||||
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
|
||||
|
||||
const order_by = document.getElementById('sortBySelect');
|
||||
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
|
||||
|
||||
const ascending = document.getElementById('sortByOrder');
|
||||
|
||||
params = [];
|
||||
if (school_id) {
|
||||
params.push(`school_id=${school_id}`);
|
||||
};
|
||||
if (order_by_value) {
|
||||
params.push(`order_by=${order_by_value}`);
|
||||
};
|
||||
if (ascending && ascending.checked) {
|
||||
params.push(`asc=true`);
|
||||
};
|
||||
|
||||
const final_url = base_url + "?" + params.join("&");
|
||||
return {
|
||||
base_url: base_url,
|
||||
final_url: final_url,
|
||||
school_id: school_id,
|
||||
order_by: order_by_value,
|
||||
ascending: ascending,
|
||||
}
|
||||
};
|
||||
|
||||
const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
// const lt = document.getElementById('learning-type-filter');
|
||||
// const ct = document.getElementById('course-thematic-filter');
|
||||
// const prefix = (lt !== null && lt.value !== '') ? `/courses/${lt.value}` : `/courses`;
|
||||
// const out = (ct !== null && ct.value !== '') ? `${prefix}/${ct.value}` : prefix;
|
||||
|
||||
values = loadInputValues();
|
||||
|
||||
const out = values.final_url;
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
const filterBySchool = () => {
|
||||
/*
|
||||
const school_selector = document.getElementById('schoolSelect');
|
||||
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
|
||||
const order_by = document.getElementById('sortBySelect');
|
||||
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
|
||||
const ascending = document.getElementById('sortByOrder');
|
||||
|
||||
const baseUrl = `${window.location.pathname}?`;
|
||||
|
||||
params = [];
|
||||
if (school_id) {
|
||||
params.push(`school_id=${school_id}`);
|
||||
};
|
||||
if (order_by_value) {
|
||||
params.push(`order_by=${order_by_value}`);
|
||||
};
|
||||
if (ascending && ascending.checked) {
|
||||
params.push(`asc=true`);
|
||||
};
|
||||
|
||||
const finalUrl = baseUrl + params.join("&");
|
||||
*/
|
||||
|
||||
values = loadInputValues();
|
||||
const finalUrl = values.final_url;
|
||||
if (history.pushState) {
|
||||
// history.pushState(null, null, finalUrl);
|
||||
// TODO: remove once htmx implemented
|
||||
window.location.assign(finalUrl);
|
||||
} else {
|
||||
window.location.assign(finalUrl);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff) ff.addEventListener('submit', formFilterOnSubmit);
|
||||
|
||||
const fs = document.getElementById('schoolSelect');
|
||||
if (fs) fs.onchange = filterBySchool;
|
||||
|
||||
const ob = document.getElementById('sortBySelect');
|
||||
if (ob) ob.onchange = filterBySchool;
|
||||
});
|
||||
}
|
||||
|
||||
templ root(page PageKind, _ stats) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@head(string(page))
|
||||
<body>
|
||||
@headerNavbar(page)
|
||||
<div class="container">
|
||||
{ children... }
|
||||
</div>
|
||||
@elementScriptsLoad()
|
||||
</body>
|
||||
@footer()
|
||||
</html>
|
||||
}
|
||||
331
internal/kurious/ports/http/bootstrap/core_templ.go
Normal file
331
internal/kurious/ports/http/bootstrap/core_templ.go
Normal file
@ -0,0 +1,331 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.707
|
||||
package bootstrap
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
func head(title string) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<head><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 5, Col: 16}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN\" crossorigin=\"anonymous\"><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css\"><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL\" crossorigin=\"anonymous\">\n\t\t</script></head>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func headerNavbar(page PageKind) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<header><nav class=\"navbar navbar-expand-lg bg-body-tertiary w-auto\"><div class=\"container-fluid\"><a class=\"navbar-brand\" href=\"/htmlexamples/index.html\">Kurious</a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\"><ul class=\"navbar-nav me-auto mb-2 mb-lg-0\"><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{"nav-link", templ.KV("active", page == PageIndex)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-current=\"page\" href=\"/\">Home</a></li><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 = []any{"nav-link", templ.KV("active", page == PageCourses)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-current=\"page\" href=\"/courses\">Courses</a></li><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 = []any{"nav-link", templ.KV("active", page == PageAbout)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/core.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" href=\"/about\">About us</a></li></ul></div></div></nav></header>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func footer() templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<footer class=\"text-center text-lg-start bg-body-tertiary text-muted\"><section class=\"p-2\"><div class=\"container text-center text-md-start mt-5\"><div class=\"row mt-3\"><div class=\"col-md-3 col-lg-4 col-xl-3 mx-auto mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\"><i class=\"fas fa-gem me-3\"></i>Courses</h6><p>Welcome to Courses, your gateway to learning! Explore a diverse range of courses and advance your skills with us. Join our community and transform your life through education.</p></div><div class=\"col-md-3 col-lg-2 col-xl-2 mx-auto mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\">Useful links</h6><p><a href=\"#!\" class=\"text-reset\">Pricing</a></p><p><a href=\"#!\" class=\"text-reset\">Settings</a></p><p><a href=\"#!\" class=\"text-reset\">Orders</a></p><p><a href=\"#!\" class=\"text-reset\">Help</a></p></div><div class=\"col-md-4 col-lg-3 col-xl-3 mx-auto mb-md-0 mb-4\"><h6 class=\"text-uppercase fw-bold mb-4\">Contact</h6><p><i class=\"fas fa-home me-3\"></i> New York, NY 10012, US</p><p><i class=\"fas fa-envelope me-3\"></i> info@example.com</p><p><i class=\"fas fa-phone me-3\"></i> + 01 234 567 88</p><p><i class=\"fas fa-print me-3\"></i> + 01 234 567 89</p></div></div></div></section><div class=\"text-center p-4\" style=\"background-color: rgba(0, 0, 0, 0.05)\">© 2024 Copyright: <a class=\"text-reset fw-bold\" href=\"https://mdbootstrap.com/\">kursov.net</a></div></footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func elementScriptsLoad() templ.ComponentScript {
|
||||
return templ.ComponentScript{
|
||||
Name: `__templ_elementScriptsLoad_bfb1`,
|
||||
Function: `function __templ_elementScriptsLoad_bfb1(){const loadInputValues = () => {
|
||||
const lt = document.getElementById('learning-type-filter');
|
||||
const ct = document.getElementById('course-thematic-filter');
|
||||
|
||||
const prefix = (lt !== null && lt.value !== '') ? ` + "`" + `/courses/${lt.value}` + "`" + ` : ` + "`" + `/courses` + "`" + `;
|
||||
const base_url = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
|
||||
|
||||
const school_selector = document.getElementById('schoolSelect');
|
||||
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
|
||||
|
||||
const order_by = document.getElementById('sortBySelect');
|
||||
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
|
||||
|
||||
const ascending = document.getElementById('sortByOrder');
|
||||
|
||||
params = [];
|
||||
if (school_id) {
|
||||
params.push(` + "`" + `school_id=${school_id}` + "`" + `);
|
||||
};
|
||||
if (order_by_value) {
|
||||
params.push(` + "`" + `order_by=${order_by_value}` + "`" + `);
|
||||
};
|
||||
if (ascending && ascending.checked) {
|
||||
params.push(` + "`" + `asc=true` + "`" + `);
|
||||
};
|
||||
|
||||
const final_url = base_url + "?" + params.join("&");
|
||||
return {
|
||||
base_url: base_url,
|
||||
final_url: final_url,
|
||||
school_id: school_id,
|
||||
order_by: order_by_value,
|
||||
ascending: ascending,
|
||||
}
|
||||
};
|
||||
|
||||
const formFilterOnSubmit = event => {
|
||||
event.preventDefault();
|
||||
|
||||
// const lt = document.getElementById('learning-type-filter');
|
||||
// const ct = document.getElementById('course-thematic-filter');
|
||||
// const prefix = (lt !== null && lt.value !== '') ? ` + "`" + `/courses/${lt.value}` + "`" + ` : ` + "`" + `/courses` + "`" + `;
|
||||
// const out = (ct !== null && ct.value !== '') ? ` + "`" + `${prefix}/${ct.value}` + "`" + ` : prefix;
|
||||
|
||||
values = loadInputValues();
|
||||
|
||||
const out = values.final_url;
|
||||
document.location.assign(out);
|
||||
return false;
|
||||
};
|
||||
|
||||
const filterBySchool = () => {
|
||||
/*
|
||||
const school_selector = document.getElementById('schoolSelect');
|
||||
const school_id = (school_selector !== null && school_selector.value !== '') ? school_selector.value : '';
|
||||
const order_by = document.getElementById('sortBySelect');
|
||||
const order_by_value = (order_by !== null && order_by.value !== '') ? order_by.value : '';
|
||||
const ascending = document.getElementById('sortByOrder');
|
||||
|
||||
const baseUrl = ` + "`" + `${window.location.pathname}?` + "`" + `;
|
||||
|
||||
params = [];
|
||||
if (school_id) {
|
||||
params.push(` + "`" + `school_id=${school_id}` + "`" + `);
|
||||
};
|
||||
if (order_by_value) {
|
||||
params.push(` + "`" + `order_by=${order_by_value}` + "`" + `);
|
||||
};
|
||||
if (ascending && ascending.checked) {
|
||||
params.push(` + "`" + `asc=true` + "`" + `);
|
||||
};
|
||||
|
||||
const finalUrl = baseUrl + params.join("&");
|
||||
*/
|
||||
|
||||
values = loadInputValues();
|
||||
const finalUrl = values.final_url;
|
||||
if (history.pushState) {
|
||||
// history.pushState(null, null, finalUrl);
|
||||
// TODO: remove once htmx implemented
|
||||
window.location.assign(finalUrl);
|
||||
} else {
|
||||
window.location.assign(finalUrl);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ff = document.getElementById('filter-form');
|
||||
if (ff) ff.addEventListener('submit', formFilterOnSubmit);
|
||||
|
||||
const fs = document.getElementById('schoolSelect');
|
||||
if (fs) fs.onchange = filterBySchool;
|
||||
|
||||
const ob = document.getElementById('sortBySelect');
|
||||
if (ob) ob.onchange = filterBySchool;
|
||||
});
|
||||
}`,
|
||||
Call: templ.SafeScript(`__templ_elementScriptsLoad_bfb1`),
|
||||
CallInline: templ.SafeScriptInline(`__templ_elementScriptsLoad_bfb1`),
|
||||
}
|
||||
}
|
||||
|
||||
func root(page PageKind, _ stats) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"ru\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = head(string(page)).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = headerNavbar(page).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ_7745c5c3_Var11.Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = elementScriptsLoad().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
239
internal/kurious/ports/http/bootstrap/list.templ
Normal file
239
internal/kurious/ports/http/bootstrap/list.templ
Normal file
@ -0,0 +1,239 @@
|
||||
package bootstrap
|
||||
|
||||
import "path"
|
||||
import "strconv"
|
||||
|
||||
templ breadcrumbsItem(text, link string, isActive bool) {
|
||||
<li class={ "breadcrumb-item", templ.KV("active", isActive) }>
|
||||
if link != "" {
|
||||
<a
|
||||
href={ templ.URL(link) }
|
||||
itemprop="url"
|
||||
aria-label="breadcrumb"
|
||||
>{ text }</a>
|
||||
} else {
|
||||
<span itemprop="url" itemprop="title">{ text }</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
templ breadcrumNode(params BreadcrumbsParams) {
|
||||
<nav
|
||||
class={ "mt-4", breadcrumbSymbol() }
|
||||
aria-label="breadcrumbs"
|
||||
itemprop="breadcrumb"
|
||||
itemtype="https://schema.org/BreadcrumbList"
|
||||
itemscope
|
||||
>
|
||||
<ol class="breadcrumb">
|
||||
@breadcrumbsItem("Курсы", "/courses", params.ActiveLearningType.Empty())
|
||||
if !params.ActiveLearningType.Empty() {
|
||||
@breadcrumbsItem(
|
||||
params.ActiveLearningType.Name,
|
||||
path.Join("/", "courses", params.ActiveLearningType.ID),
|
||||
params.ActiveCourseThematic.Empty(),
|
||||
)
|
||||
}
|
||||
if !params.ActiveCourseThematic.Empty() {
|
||||
@breadcrumbsItem(
|
||||
params.ActiveCourseThematic.Name,
|
||||
path.Join("/", "courses", params.ActiveLearningType.ID, params.ActiveCourseThematic.ID),
|
||||
true,
|
||||
)
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
|
||||
css breadcrumbSymbol() {
|
||||
--bs-breadcrumb-divider: "/";
|
||||
}
|
||||
|
||||
templ listCoursesSectionHeader(params BreadcrumbsParams) {
|
||||
<section class="row bc-header">
|
||||
@breadcrumNode(params)
|
||||
</section>
|
||||
}
|
||||
|
||||
templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
<section class="row filters">
|
||||
<div
|
||||
class={
|
||||
templ.KV("visually-hidden", !params.Render),
|
||||
"p-2",
|
||||
}
|
||||
>
|
||||
<form id="filter-form" class="input-group">
|
||||
<span class="input-group-text">Filter courses</span>
|
||||
<select
|
||||
id="learning-type-filter"
|
||||
class={ "form-select" }
|
||||
>
|
||||
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
|
||||
for _, learningType := range params.AvailableLearningTypes {
|
||||
<option
|
||||
selected?={ params.ActiveLearningType.ID==learningType.ID }
|
||||
value={ learningType.ID }
|
||||
>{ learningType.Name }</option>
|
||||
}
|
||||
</select>
|
||||
<select
|
||||
id="course-thematic-filter"
|
||||
class={ "form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0) }
|
||||
>
|
||||
<option value="" selected?={ params.ActiveLearningType.ID=="" }>All</option>
|
||||
for _, courseThematic := range params.AvailableCourseThematics {
|
||||
<option
|
||||
selected?={ params.ActiveCourseThematic.ID==courseThematic.ID }
|
||||
value={ courseThematic.ID }
|
||||
>{ courseThematic.Name }</option>
|
||||
}
|
||||
</select>
|
||||
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
@filterCoursesSelectView(params.Schools)
|
||||
</section>
|
||||
}
|
||||
|
||||
templ selectOptionFromPairs(emptyName, activeID string, items []NameIDPair) {
|
||||
<option value="" selected?={ activeID == "" }>
|
||||
{ emptyName }
|
||||
</option>
|
||||
for _, item := range items {
|
||||
<option
|
||||
selected?={ activeID==item.ID }
|
||||
value={ item.ID }
|
||||
>
|
||||
{ item.Name }
|
||||
</option>
|
||||
}
|
||||
}
|
||||
|
||||
templ filterCoursesSelectView(params CoursesFilterViewParams) {
|
||||
<div class="filter-content d-flex p-2">
|
||||
<!-- School list -->
|
||||
<div class="col-3">
|
||||
<select name="school" id="schoolSelect" class="form-select" aria-label="school filter">
|
||||
@selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools)
|
||||
</select>
|
||||
</div>
|
||||
<!-- Sort options -->
|
||||
<div class="col-3 mx-4">
|
||||
<div class="input-group flex-nowrap">
|
||||
<select
|
||||
class="form-select"
|
||||
id="sortBySelect"
|
||||
aria-label="Sorting items by requested order"
|
||||
>
|
||||
@selectOptionFromPairs("Sort by:", params.OrderBy, params.OrderFields)
|
||||
</select>
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name="sortByOrder"
|
||||
id="sortByOrder"
|
||||
autocomplete="off"
|
||||
checked?={ !params.Ascending }
|
||||
/>
|
||||
<label class="btn" for="sortByOrder">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-sort-down"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
d="M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"
|
||||
></path>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4"></div>
|
||||
<div class="col-auto ms-auto">
|
||||
<div class="btn btn-primary">Promocodes</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ listCoursesLearning(courses []CourseInfo) {
|
||||
<div class="block">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
for _, course := range courses {
|
||||
@listCoursesCard(course)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ listCoursesLearningLegacy(containers []CategoryContainer) {
|
||||
for _, container := range containers {
|
||||
<section class="row first-class-group g-4">
|
||||
<h1 class="title">{ container.Name }</h1>
|
||||
for _, subcategory := range container.Subcategories {
|
||||
@listCoursesThematicRowLegacy(container.ID, subcategory)
|
||||
}
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
templ listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) {
|
||||
<div class="block second-class-group">
|
||||
<h3 class="title">
|
||||
<a href={ templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID) } class="text-decoration-none">
|
||||
{ subcategory.Name }
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up-right-square" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.854 8.803a.5.5 0 1 1-.708-.707L9.243 6H6.475a.5.5 0 1 1 0-1h3.975a.5.5 0 0 1 .5.5v3.975a.5.5 0 1 1-1 0V6.707z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</h3>
|
||||
<p class="visually-hidden">В категогрии { subcategory.Name } собраны { strconv.Itoa(subcategory.Count) } курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
for _, info := range subcategory.Courses {
|
||||
@listCoursesCard(info)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
css myImg() {
|
||||
min-height: 19rem;
|
||||
min-width: 19rem;
|
||||
}
|
||||
|
||||
css cardTextSize() {
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
templ listCoursesCard(info CourseInfo) {
|
||||
// <div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="col" id={ info.ID }>
|
||||
<div class="card h-100">
|
||||
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128") } alt="Course picture" class={ "card-img-top" }/>
|
||||
<div class={ "card-body", cardTextSize(), "row" }>
|
||||
<h5 class="card-title">{ info.Name }</h5>
|
||||
<div class="input-group d-flex align-self-end">
|
||||
<a
|
||||
href={ templ.URL(info.OriginLink) }
|
||||
class="btn text btn-outline-primary flex-grow-1"
|
||||
>Go!</a>
|
||||
<span class="input-group-text justify-content-end flex-fill">
|
||||
{ strconv.Itoa(info.FullPrice) } rub.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {
|
||||
@root(pageType, s) {
|
||||
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
|
||||
@listCoursesSectionFilters(params.FilterForm)
|
||||
@listCoursesLearning(params.Courses)
|
||||
@pagination(params.Pagination)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package bootstrap
|
||||
|
||||
templ listCoursesByCourseThematic(params ListCoursesParams) {
|
||||
<div class="container">
|
||||
<h2>Здесь вы можете найти интересующие вас курсы
|
||||
по теме { params.FilterForm.ActiveLearningType.Name }:
|
||||
</h2>
|
||||
|
||||
<ul class="list-group">
|
||||
for _, courseThematic := range params.FilterForm.AvailableCourseThematics {
|
||||
<li class="list-group-item">
|
||||
<a href={templ.SafeURL("/courses/" + params.FilterForm.ActiveLearningType.ID + "/" + courseThematic.ID)}>
|
||||
{courseThematic.Name}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ListCourseThematics(pageType PageKind, s stats, params ListCoursesParams) {
|
||||
@root(pageType, s) {
|
||||
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
|
||||
@listCoursesSectionFilters(params.FilterForm)
|
||||
@listCoursesByCourseThematic(params)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.707
|
||||
package bootstrap
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
func listCoursesByCourseThematic(params ListCoursesParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><h2>Здесь вы можете найти интересующие вас курсы по теме ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(params.FilterForm.ActiveLearningType.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list_course_thematics.templ`, Line: 6, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(":</h2><ul class=\"list-group\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, courseThematic := range params.FilterForm.AvailableCourseThematics {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"list-group-item\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 templ.SafeURL = templ.SafeURL("/courses/" + params.FilterForm.ActiveLearningType.ID + "/" + courseThematic.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var3)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list_course_thematics.templ`, Line: 13, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func ListCourseThematics(pageType PageKind, s stats, params ListCoursesParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var6 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesSectionFilters(params.FilterForm).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesByCourseThematic(params).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
949
internal/kurious/ports/http/bootstrap/list_templ.go
Normal file
949
internal/kurious/ports/http/bootstrap/list_templ.go
Normal file
@ -0,0 +1,949 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.707
|
||||
package bootstrap
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
import "strings"
|
||||
|
||||
import "path"
|
||||
import "strconv"
|
||||
|
||||
func breadcrumbsItem(text, link string, isActive bool) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var2 = []any{"breadcrumb-item", templ.KV("active", isActive)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if link != "" {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL(link)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" itemprop=\"url\" aria-label=\"breadcrumb\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 13, Col: 10}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<span itemprop=\"url\" itemprop=\"title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(text)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 15, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func breadcrumNode(params BreadcrumbsParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var8 = []any{"mt-4", breadcrumbSymbol()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" aria-label=\"breadcrumbs\" itemprop=\"breadcrumb\" itemtype=\"https://schema.org/BreadcrumbList\" itemscope><ol class=\"breadcrumb\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumbsItem("Курсы", "/courses", params.ActiveLearningType.Empty()).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !params.ActiveLearningType.Empty() {
|
||||
templ_7745c5c3_Err = breadcrumbsItem(
|
||||
params.ActiveLearningType.Name,
|
||||
path.Join("/", "courses", params.ActiveLearningType.ID),
|
||||
params.ActiveCourseThematic.Empty(),
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if !params.ActiveCourseThematic.Empty() {
|
||||
templ_7745c5c3_Err = breadcrumbsItem(
|
||||
params.ActiveCourseThematic.Name,
|
||||
path.Join("/", "courses", params.ActiveLearningType.ID, params.ActiveCourseThematic.ID),
|
||||
true,
|
||||
).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ol></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func breadcrumbSymbol() templ.CSSClass {
|
||||
var templ_7745c5c3_CSSBuilder strings.Builder
|
||||
templ_7745c5c3_CSSBuilder.WriteString(`--bs-breadcrumb-divider:"/";`)
|
||||
templ_7745c5c3_CSSID := templ.CSSID(`breadcrumbSymbol`, templ_7745c5c3_CSSBuilder.String())
|
||||
return templ.ComponentCSSClass{
|
||||
ID: templ_7745c5c3_CSSID,
|
||||
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
|
||||
}
|
||||
}
|
||||
|
||||
func listCoursesSectionHeader(params BreadcrumbsParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var10 == nil {
|
||||
templ_7745c5c3_Var10 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row bc-header\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = breadcrumNode(params).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row filters\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 = []any{
|
||||
templ.KV("visually-hidden", !params.Render),
|
||||
"p-2",
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><form id=\"filter-form\" class=\"input-group\"><span class=\"input-group-text\">Filter courses</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 = []any{"form-select"}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<select id=\"learning-type-filter\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if params.ActiveLearningType.ID == "" {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">All</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, learningType := range params.AvailableLearningTypes {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if params.ActiveLearningType.ID == learningType.ID {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 76, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(learningType.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 77, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 = []any{"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<select id=\"course-thematic-filter\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if params.ActiveLearningType.ID == "" {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">All</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, courseThematic := range params.AvailableCourseThematics {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if params.ActiveCourseThematic.ID == courseThematic.ID {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 88, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(courseThematic.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 89, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <button id=\"filter-course-thematic\" class=\"btn btn-outline-secondary\" type=\"submit\">Go</button></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = filterCoursesSelectView(params.Schools).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func selectOptionFromPairs(emptyName, activeID string, items []NameIDPair) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if activeID == "" {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(emptyName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 101, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range items {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if activeID == item.ID {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(item.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 106, Col: 18}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 108, Col: 14}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func filterCoursesSelectView(params CoursesFilterViewParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var26 == nil {
|
||||
templ_7745c5c3_Var26 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"filter-content d-flex p-2\"><!-- School list --><div class=\"col-3\"><select name=\"school\" id=\"schoolSelect\" class=\"form-select\" aria-label=\"school filter\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = selectOptionFromPairs("Pick a school:", params.SelectedSchoolID, params.Schools).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div><!-- Sort options --><div class=\"col-3 mx-4\"><div class=\"input-group flex-nowrap\"><select class=\"form-select\" id=\"sortBySelect\" aria-label=\"Sorting items by requested order\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = selectOptionFromPairs("Sort by:", params.OrderBy, params.OrderFields).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <input type=\"radio\" class=\"btn-check\" name=\"sortByOrder\" id=\"sortByOrder\" autocomplete=\"off\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !params.Ascending {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("> <label class=\"btn\" for=\"sortByOrder\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-sort-down\" viewBox=\"0 0 16 16\"><path d=\"M3.5 2.5a.5.5 0 0 0-1 0v8.793l-1.146-1.147a.5.5 0 0 0-.708.708l2 1.999.007.007a.497.497 0 0 0 .7-.006l2-2a.5.5 0 0 0-.707-.708L3.5 11.293zm3.5 1a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5M7.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z\"></path></svg></label></div></div><div class=\"col-4\"></div><div class=\"col-auto ms-auto\"><div class=\"btn btn-primary\">Promocodes</div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func listCoursesLearning(courses []CourseInfo) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var27 == nil {
|
||||
templ_7745c5c3_Var27 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block\"><div class=\"row row-cols-1 row-cols-md-4 g-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, course := range courses {
|
||||
templ_7745c5c3_Err = listCoursesCard(course).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func listCoursesLearningLegacy(containers []CategoryContainer) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var28 == nil {
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
for _, container := range containers {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"row first-class-group g-4\"><h1 class=\"title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(container.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 175, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, subcategory := range container.Subcategories {
|
||||
templ_7745c5c3_Err = listCoursesThematicRowLegacy(container.ID, subcategory).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func listCoursesThematicRowLegacy(categoryID string, subcategory SubcategoryContainer) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var30 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var30 == nil {
|
||||
templ_7745c5c3_Var30 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h3 class=\"title\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 templ.SafeURL = templ.SafeURL("/courses/" + categoryID + "/" + subcategory.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"text-decoration-none\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 187, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-up-right-square\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm5.854 8.803a.5.5 0 1 1-.708-.707L9.243 6H6.475a.5.5 0 1 1 0-1h3.975a.5.5 0 0 1 .5.5v3.975a.5.5 0 1 1-1 0V6.707z\"></path></svg></a></h3><p class=\"visually-hidden\">В категогрии ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 193, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" собраны ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(subcategory.Count))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 193, Col: 122}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" курсов. Раз в неделю мы обновляем информацию о всех курсах.</p><div class=\"row row-cols-1 row-cols-md-4 g-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, info := range subcategory.Courses {
|
||||
templ_7745c5c3_Err = listCoursesCard(info).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func myImg() templ.CSSClass {
|
||||
var templ_7745c5c3_CSSBuilder strings.Builder
|
||||
templ_7745c5c3_CSSBuilder.WriteString(`min-height:19rem;`)
|
||||
templ_7745c5c3_CSSBuilder.WriteString(`min-width:19rem;`)
|
||||
templ_7745c5c3_CSSID := templ.CSSID(`myImg`, templ_7745c5c3_CSSBuilder.String())
|
||||
return templ.ComponentCSSClass{
|
||||
ID: templ_7745c5c3_CSSID,
|
||||
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
|
||||
}
|
||||
}
|
||||
|
||||
func cardTextSize() templ.CSSClass {
|
||||
var templ_7745c5c3_CSSBuilder strings.Builder
|
||||
templ_7745c5c3_CSSBuilder.WriteString(`min-height:12rem;`)
|
||||
templ_7745c5c3_CSSID := templ.CSSID(`cardTextSize`, templ_7745c5c3_CSSBuilder.String())
|
||||
return templ.ComponentCSSClass{
|
||||
ID: templ_7745c5c3_CSSID,
|
||||
Class: templ.SafeCSS(`.` + templ_7745c5c3_CSSID + `{` + templ_7745c5c3_CSSBuilder.String() + `}`),
|
||||
}
|
||||
}
|
||||
|
||||
func listCoursesCard(info CourseInfo) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var35 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var35 == nil {
|
||||
templ_7745c5c3_Var35 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(info.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 213, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><div class=\"card h-100\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var37 = []any{"card-img-top"}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var37...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<img src=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(GetOrFallback(info.ImageLink, "https://placehold.co/128x128"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 215, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" alt=\"Course picture\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var37).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var40 = []any{"card-body", cardTextSize(), "row"}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var40...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var40).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><h5 class=\"card-title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 217, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><div class=\"input-group d-flex align-self-end\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var43 templ.SafeURL = templ.URL(info.OriginLink)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var43)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn text btn-outline-primary flex-grow-1\">Go!</a> <span class=\"input-group-text justify-content-end flex-fill\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 224, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" rub.</span></div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var45 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var45 == nil {
|
||||
templ_7745c5c3_Var45 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var46 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesSectionFilters(params.FilterForm).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesLearning(params.Courses).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = pagination(params.Pagination).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var46), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
96
internal/kurious/ports/http/bootstrap/main.templ
Normal file
96
internal/kurious/ports/http/bootstrap/main.templ
Normal file
@ -0,0 +1,96 @@
|
||||
package bootstrap
|
||||
|
||||
import "strconv"
|
||||
|
||||
type IndexCourseCategoryItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
ExampleThemes []string
|
||||
Count int
|
||||
}
|
||||
|
||||
// courseItemCard is a card that renders a single course thematic item
|
||||
// that holds multiple learning types. It expected to have a basic description
|
||||
// and an amount of items.
|
||||
templ courseItemCard(item IndexCourseCategoryItem) {
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{ item.Name }</h5>
|
||||
<hr/>
|
||||
<p>{ item.Description }</p>
|
||||
if len(item.ExampleThemes) > 0 {
|
||||
<p>В данной категории вы можете найти курсы по темам:</p>
|
||||
<ul>
|
||||
for _, exampleItem := range item.ExampleThemes {
|
||||
<li>
|
||||
<span class="d-inline-block text-truncate col-8">{ exampleItem }</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href={ templ.URL("/courses/" + item.ID) } class="btn btn-sm btn-outline-primary col-6">
|
||||
Open
|
||||
</a>
|
||||
<small class="text-body-secondary">
|
||||
{ strconv.Itoa(item.Count) } items.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ courseCategory(items []IndexCourseCategoryItem) {
|
||||
<div class="container w-75 mb-4">
|
||||
<div class="row g-4">
|
||||
for _, item := range items {
|
||||
<div class="col-12 col-md-8 col-lg-4">
|
||||
@courseItemCard(item)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int
|
||||
TotalPages int
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
templ pagination(p Pagination) {
|
||||
if p.Page > 0 && p.TotalPages > 0 {
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class={ "page-item" , templ.KV("disabled", p.Page==1), }>
|
||||
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1)) } class="page-link">Previous</a>
|
||||
</li>
|
||||
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
|
||||
<li
|
||||
class={ "page-item" , templ.KV("active", p.Page==i), }
|
||||
>
|
||||
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i)) } class="page-link">{ strconv.Itoa(i) }</a>
|
||||
</li>
|
||||
}
|
||||
<li class={ "page-item" , templ.KV("disabled", p.Page==p.TotalPages), }>
|
||||
<a href={ templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page+1)) } class="page-link">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
type MainPageParams struct {
|
||||
Breadcrumbs BreadcrumbsParams
|
||||
Categories []IndexCourseCategoryItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
|
||||
@root(pageType, s) {
|
||||
@listCoursesSectionHeader(params.Breadcrumbs)
|
||||
@courseCategory(params.Categories)
|
||||
@pagination(params.Pagination)
|
||||
}
|
||||
}
|
||||
372
internal/kurious/ports/http/bootstrap/main_templ.go
Normal file
372
internal/kurious/ports/http/bootstrap/main_templ.go
Normal file
@ -0,0 +1,372 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.2.707
|
||||
package bootstrap
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import "context"
|
||||
import "io"
|
||||
import "bytes"
|
||||
|
||||
import "strconv"
|
||||
|
||||
type IndexCourseCategoryItem struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
ExampleThemes []string
|
||||
Count int
|
||||
}
|
||||
|
||||
// courseItemCard is a card that renders a single course thematic item
|
||||
// that holds multiple learning types. It expected to have a basic description
|
||||
// and an amount of items.
|
||||
func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"card\"><div class=\"card-body\"><h5 class=\"card-title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 19, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><hr><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 21, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(item.ExampleThemes) > 0 {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>В данной категории вы можете найти курсы по темам:</p><ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, exampleItem := range item.ExampleThemes {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li><span class=\"d-inline-block text-truncate col-8\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(exampleItem)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 27, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 templ.SafeURL = templ.URL("/courses/" + item.ID)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"btn btn-sm btn-outline-primary col-6\">Open</a> <small class=\"text-body-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 37, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" items.</small></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
func courseCategory(items []IndexCourseCategoryItem) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75 mb-4\"><div class=\"row g-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, item := range items {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-12 col-md-8 col-lg-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = courseItemCard(item).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int
|
||||
TotalPages int
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func pagination(p Pagination) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var8 == nil {
|
||||
templ_7745c5c3_Var8 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
if p.Page > 0 && p.TotalPages > 0 {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<nav aria-label=\"Page navigation\"><ul class=\"pagination justify-content-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 = []any{"page-item", templ.KV("disabled", p.Page == 1)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">Previous</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
|
||||
var templ_7745c5c3_Var12 = []any{"page-item", templ.KV("active", p.Page == i)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 73, Col: 103}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
var templ_7745c5c3_Var16 = []any{"page-item", templ.KV("disabled", p.Page == p.TotalPages)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<li class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page+1))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var18)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" class=\"page-link\">Next</a></li></ul></nav>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
|
||||
type MainPageParams struct {
|
||||
Breadcrumbs BreadcrumbsParams
|
||||
Categories []IndexCourseCategoryItem
|
||||
Pagination Pagination
|
||||
}
|
||||
|
||||
func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component {
|
||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var19 == nil {
|
||||
templ_7745c5c3_Var19 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var20 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
templ_7745c5c3_Err = listCoursesSectionHeader(params.Breadcrumbs).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = courseCategory(params.Categories).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = pagination(params.Pagination).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var20), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
}
|
||||
157
internal/kurious/ports/http/bootstrap/vars.go
Normal file
157
internal/kurious/ports/http/bootstrap/vars.go
Normal file
@ -0,0 +1,157 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PageKind string
|
||||
|
||||
const (
|
||||
PageIndex PageKind = "main"
|
||||
PageCourses PageKind = "courses"
|
||||
PageAbout PageKind = "about"
|
||||
)
|
||||
|
||||
func getCompactedValue(value int) string {
|
||||
var (
|
||||
myValue float64
|
||||
dim string
|
||||
)
|
||||
switch {
|
||||
case value/1e6 > 0:
|
||||
cutted := value / 1e3
|
||||
myValue, dim = float64(cutted)/1e3, "m"
|
||||
case value/1e3 > 0:
|
||||
myValue, dim = float64(value/1e3), "k"
|
||||
default:
|
||||
myValue, dim = float64(value), ""
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(strconv.FormatFloat(myValue, 'f', 3, 32), ".000") + dim
|
||||
}
|
||||
|
||||
func MakeNewStats(courses, clients, categories int) stats {
|
||||
return stats{
|
||||
CoursesCount: getCompactedValue(courses),
|
||||
ClientsCount: getCompactedValue(clients),
|
||||
CategoriesCount: getCompactedValue(categories),
|
||||
}
|
||||
}
|
||||
|
||||
type stats struct {
|
||||
CoursesCount string
|
||||
ClientsCount string
|
||||
CategoriesCount string
|
||||
}
|
||||
|
||||
type NameIDPair struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
type SortingItem struct {
|
||||
NameIDPair
|
||||
|
||||
Ascending bool
|
||||
}
|
||||
|
||||
type SortingView struct {
|
||||
Items []SortingItem
|
||||
}
|
||||
|
||||
type CoursesFilterViewParams struct {
|
||||
SelectedSchoolID string
|
||||
Schools []NameIDPair
|
||||
OrderBy string
|
||||
OrderFields []NameIDPair
|
||||
Ascending bool
|
||||
}
|
||||
|
||||
type Category struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (c Category) Empty() bool {
|
||||
return c == (Category{})
|
||||
}
|
||||
|
||||
type BreadcrumbsParams struct {
|
||||
ActiveLearningType Category
|
||||
ActiveCourseThematic Category
|
||||
}
|
||||
|
||||
type FilterFormParams struct {
|
||||
BreadcrumbsParams
|
||||
|
||||
Schools CoursesFilterViewParams
|
||||
AvailableLearningTypes []Category
|
||||
AvailableCourseThematics []Category
|
||||
Render bool
|
||||
}
|
||||
|
||||
type CourseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
FullPrice int
|
||||
ImageLink string
|
||||
OriginLink string
|
||||
}
|
||||
|
||||
type CategoryBaseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
}
|
||||
|
||||
type CategoryContainer struct {
|
||||
CategoryBaseInfo
|
||||
|
||||
Subcategories []SubcategoryContainer
|
||||
}
|
||||
|
||||
type SubcategoryContainer struct {
|
||||
CategoryBaseInfo
|
||||
|
||||
Courses []CourseInfo
|
||||
}
|
||||
|
||||
type ListCoursesParams struct {
|
||||
FilterForm FilterFormParams
|
||||
Categories []CategoryContainer
|
||||
Pagination Pagination
|
||||
Items int
|
||||
Courses []CourseInfo
|
||||
}
|
||||
|
||||
func GetOrFallback[T comparable](value T, fallback T) T {
|
||||
var zeroValue T
|
||||
if value == zeroValue {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type ordered interface {
|
||||
~int8 | ~int16 | ~int32 | ~int64 | ~int |
|
||||
~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uint |
|
||||
~float32 | float64 | ~string
|
||||
}
|
||||
|
||||
func min[T ordered](lhs, rhs T) T {
|
||||
if lhs < rhs {
|
||||
return lhs
|
||||
}
|
||||
|
||||
return rhs
|
||||
}
|
||||
|
||||
func max[T ordered](lhs, rhs T) T {
|
||||
if lhs > rhs {
|
||||
return lhs
|
||||
}
|
||||
|
||||
return rhs
|
||||
}
|
||||
@ -1,118 +1,406 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslice"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/ports/http/bootstrap"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type courseServer struct {
|
||||
app app.Application
|
||||
var (
|
||||
paramsAttr = attribute.Key("params")
|
||||
|
||||
webtracer = otel.Tracer("http")
|
||||
)
|
||||
|
||||
type courseTemplServer struct {
|
||||
app service.Application
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
type pagination struct {
|
||||
page int
|
||||
perPage int
|
||||
}
|
||||
func makeTemplListCoursesParams(counts map[string]domain.LearningTypeStat, in ...domain.Course) bootstrap.ListCoursesParams {
|
||||
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
|
||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
|
||||
seenCourses := make(map[string]struct{}, len(in))
|
||||
|
||||
func (p pagination) LimitOffset() (limit, offset int) {
|
||||
if p.page < 0 {
|
||||
p.page = 0
|
||||
}
|
||||
|
||||
if p.perPage <= 0 {
|
||||
p.perPage = defaultPerPage
|
||||
}
|
||||
|
||||
return p.perPage, p.page * p.perPage
|
||||
}
|
||||
|
||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
query := r.URL.Query()
|
||||
|
||||
if query.Has("page") {
|
||||
out.page, err = strconv.Atoi(query.Get("page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("page", "bad page value")
|
||||
var out bootstrap.ListCoursesParams
|
||||
xslices.ForEach(in, func(c domain.Course) {
|
||||
courseInfo := bootstrap.CourseInfo{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
FullPrice: int(c.FullPrice),
|
||||
ImageLink: c.ImageLink,
|
||||
OriginLink: c.OriginLink,
|
||||
}
|
||||
}
|
||||
|
||||
if query.Has("per_page") {
|
||||
out.perPage, err = strconv.Atoi(query.Get("per_page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("per_page", "bad per_page value")
|
||||
coursesBySubcategory[c.ThematicID] = append(coursesBySubcategory[c.ThematicID], courseInfo)
|
||||
|
||||
if _, ok := subcategoriesByCategories[c.LearningTypeID]; !ok {
|
||||
subcategoriesByCategories[c.LearningTypeID] = map[string]struct{}{}
|
||||
}
|
||||
} else {
|
||||
out.perPage = 50
|
||||
}
|
||||
subcategoriesByCategories[c.LearningTypeID][c.ThematicID] = struct{}{}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
if _, ok := categoryByID[c.LearningTypeID]; !ok {
|
||||
categoryByID[c.LearningTypeID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.LearningTypeID,
|
||||
Name: c.LearningType,
|
||||
}
|
||||
}
|
||||
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
Count: counts[c.LearningTypeID].CourseThematic[c.ThematicID],
|
||||
}
|
||||
}
|
||||
|
||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||
out.pagination, err = parsePaginationFromQuery(r)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if _, ok := seenCourses[c.ExternalID.Value()]; ok && c.ExternalID.Valid() {
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query()
|
||||
out.learningType = query.Get("category")
|
||||
out.courseThematic = query.Get("type")
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type listCoursesParams struct {
|
||||
pagination
|
||||
|
||||
courseThematic string
|
||||
learningType string
|
||||
}
|
||||
|
||||
type templateCourse domain.Course
|
||||
|
||||
func mapDomainCourseToTemplate(in ...domain.Course) []templateCourse {
|
||||
return xslice.Map(in, func(v domain.Course) templateCourse {
|
||||
return templateCourse(v)
|
||||
out.Courses = append(out.Courses, courseInfo)
|
||||
seenCourses[c.ExternalID.Value()] = struct{}{}
|
||||
})
|
||||
|
||||
for categoryID, subcategoriesID := range subcategoriesByCategories {
|
||||
outCategory := bootstrap.CategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[categoryID],
|
||||
}
|
||||
|
||||
for subcategoryID := range subcategoriesID {
|
||||
outSubcategory := bootstrap.SubcategoryContainer{
|
||||
CategoryBaseInfo: categoryByID[subcategoryID],
|
||||
Courses: coursesBySubcategory[subcategoryID],
|
||||
}
|
||||
|
||||
outCategory.Subcategories = append(outCategory.Subcategories, outSubcategory)
|
||||
}
|
||||
|
||||
out.Categories = append(out.Categories, outCategory)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (c courseServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
params, err := parseListCoursesParams(r)
|
||||
if err != nil {
|
||||
handleError(ctx, err, w, c.log, "unable to parse list courses params")
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "http_server.list")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stats := bootstrap.MakeNewStats(10_240, 2_560_000, 1800)
|
||||
|
||||
pathParams, err := parseListCoursesParams(r)
|
||||
if handleError(ctx, err, w, c.log, "unable to parse list courses params") {
|
||||
return
|
||||
}
|
||||
|
||||
limit, offset := params.LimitOffset()
|
||||
jsonParams, _ := json.Marshal(pathParams)
|
||||
span.SetAttributes(paramsAttr.String(string(jsonParams)))
|
||||
|
||||
courses, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: params.courseThematic,
|
||||
LearningType: params.learningType,
|
||||
Limit: limit,
|
||||
var offset int
|
||||
if pathParams.Page > 0 {
|
||||
offset = (pathParams.Page - 1) * pathParams.PerPage
|
||||
}
|
||||
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||
CourseThematic: pathParams.CourseThematic,
|
||||
LearningType: pathParams.LearningType,
|
||||
OrganizationID: pathParams.School,
|
||||
OrderBy: orderByListing.getField(pathParams.OrderBy),
|
||||
Ascending: pathParams.Ascending,
|
||||
Limit: pathParams.PerPage,
|
||||
NextPageToken: pathParams.NextPageToken,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
handleError(ctx, err, w, c.log, "unable to list courses")
|
||||
}
|
||||
|
||||
templateCourses := mapDomainCourseToTemplate(courses...)
|
||||
|
||||
err = template.Must(template.ParseFiles("templates/list.tmpl")).Execute(w, templateCourses)
|
||||
if err != nil {
|
||||
handleError(ctx, err, w, c.log, "unable to execute template")
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
statsresult, err := c.app.Queries.ListCourseStatistics.Handle(ctx, query.ListCoursesStats{
|
||||
LearningTypeID: pathParams.LearningType,
|
||||
CourseThematicsID: pathParams.CourseThematic,
|
||||
OrganizationID: pathParams.School,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to load stats") {
|
||||
return
|
||||
}
|
||||
|
||||
params := makeTemplListCoursesParams(statsresult.StatsByLearningType, listCoursesResult.Courses...)
|
||||
|
||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||
return
|
||||
}
|
||||
|
||||
params.FilterForm.AvailableLearningTypes = xslices.Map(learningTypeResult.LearningTypes, func(in query.LearningType) bootstrap.Category {
|
||||
outcategory := bootstrap.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
}
|
||||
if in.ID == pathParams.LearningType {
|
||||
params.FilterForm.ActiveLearningType = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
|
||||
if pathParams.LearningType != "" {
|
||||
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||
LearningTypeID: pathParams.LearningType,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||
return
|
||||
}
|
||||
|
||||
params.FilterForm.AvailableCourseThematics = xslices.Map(courseThematicsResult.CourseThematics, func(in query.CourseThematic) bootstrap.Category {
|
||||
outcategory := bootstrap.Category{
|
||||
ID: in.ID,
|
||||
Name: in.Name,
|
||||
}
|
||||
if pathParams.CourseThematic == in.ID {
|
||||
params.FilterForm.ActiveCourseThematic = outcategory
|
||||
}
|
||||
|
||||
return outcategory
|
||||
})
|
||||
}
|
||||
|
||||
organizaions, err := c.app.Queries.ListOrganizationsStats.Handle(ctx, query.ListOrganizationsStats{
|
||||
LearningTypeID: pathParams.LearningType,
|
||||
CourseThematicID: pathParams.CourseThematic,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unable to list organizations") {
|
||||
return
|
||||
}
|
||||
|
||||
organizationStatSortFunc := func(lhs, rhs domain.OrganizationStat) int {
|
||||
if lhs.CoursesCount > rhs.CoursesCount {
|
||||
return -1
|
||||
} else if lhs.CoursesCount < rhs.CoursesCount {
|
||||
return 1
|
||||
}
|
||||
|
||||
if lhs.ID > rhs.ID {
|
||||
return 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
slices.SortFunc(organizaions, organizationStatSortFunc)
|
||||
|
||||
schools := xslices.Map(organizaions, func(in domain.OrganizationStat) bootstrap.NameIDPair {
|
||||
return bootstrap.NameIDPair{
|
||||
ID: in.ID,
|
||||
Name: fmt.Sprintf("%s (count: %d)", in.Name, in.CoursesCount),
|
||||
}
|
||||
})
|
||||
|
||||
params = bootstrap.ListCoursesParams{
|
||||
FilterForm: bootstrap.FilterFormParams{
|
||||
Render: true,
|
||||
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
||||
ActiveLearningType: params.FilterForm.ActiveLearningType,
|
||||
ActiveCourseThematic: params.FilterForm.ActiveCourseThematic,
|
||||
},
|
||||
AvailableLearningTypes: params.FilterForm.AvailableLearningTypes,
|
||||
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
|
||||
Schools: bootstrap.CoursesFilterViewParams{
|
||||
SelectedSchoolID: pathParams.School,
|
||||
Schools: schools,
|
||||
Ascending: pathParams.Ascending,
|
||||
OrderBy: pathParams.OrderBy,
|
||||
OrderFields: orderByListing.asNameIDPair(),
|
||||
},
|
||||
},
|
||||
Courses: params.Courses,
|
||||
Categories: params.Categories,
|
||||
Pagination: bootstrap.Pagination{
|
||||
Page: pathParams.Page,
|
||||
TotalPages: listCoursesResult.Count / pathParams.PerPage,
|
||||
BaseURL: r.URL.Path,
|
||||
},
|
||||
}
|
||||
|
||||
c.log.DebugContext(
|
||||
ctx, "params rendered",
|
||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
||||
slog.Int("items", len(listCoursesResult.Courses)),
|
||||
slog.Int("page", params.Pagination.Page),
|
||||
slog.Int("total_pages", params.Pagination.TotalPages),
|
||||
)
|
||||
|
||||
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
|
||||
if lhs.Count > rhs.Count {
|
||||
return 1
|
||||
} else if lhs.Count < rhs.Count {
|
||||
return -1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
span.AddEvent("starting to render")
|
||||
if pathParams.CourseThematic == "" {
|
||||
params.FilterForm.Render = true
|
||||
err = bootstrap.ListCourseThematics(bootstrap.PageCourses, stats, params).Render(ctx, w)
|
||||
} else {
|
||||
err = bootstrap.ListCourses(bootstrap.PageCourses, stats, params).Render(ctx, w)
|
||||
}
|
||||
span.AddEvent("render finished")
|
||||
|
||||
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
span.SetStatus(codes.Ok, "request completed")
|
||||
}
|
||||
|
||||
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var span trace.Span
|
||||
ctx, span = webtracer.Start(ctx, "http_server.index")
|
||||
defer func() {
|
||||
span.End()
|
||||
}()
|
||||
|
||||
stats := bootstrap.MakeNewStats(1, 2, 3)
|
||||
|
||||
coursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||
return
|
||||
}
|
||||
|
||||
params := bootstrap.MainPageParams{
|
||||
Categories: []bootstrap.IndexCourseCategoryItem{},
|
||||
}
|
||||
|
||||
coursesByLearningType := make(map[IDNamePair][]domain.Course)
|
||||
xslices.ForEach(coursesResult.Courses, func(in domain.Course) {
|
||||
pair := IDNamePair{
|
||||
ID: in.LearningTypeID,
|
||||
Name: in.LearningType,
|
||||
}
|
||||
coursesByLearningType[pair] = append(coursesByLearningType[pair], in)
|
||||
})
|
||||
|
||||
for learningTypeInfo, courses := range coursesByLearningType {
|
||||
category := bootstrap.IndexCourseCategoryItem{
|
||||
ID: learningTypeInfo.ID,
|
||||
Name: learningTypeInfo.Name,
|
||||
Count: len(courses),
|
||||
}
|
||||
|
||||
xslices.Shuffle(courses)
|
||||
if len(courses) > 3 {
|
||||
courses = courses[:3]
|
||||
}
|
||||
|
||||
names := xslices.Map(courses, func(in domain.Course) string {
|
||||
return in.Name
|
||||
})
|
||||
|
||||
category.ExampleThemes = names
|
||||
|
||||
params.Categories = append(params.Categories, category)
|
||||
}
|
||||
|
||||
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.IndexCourseCategoryItem) int {
|
||||
if lhs.Count < rhs.Count {
|
||||
return 1
|
||||
} else if lhs.Count > rhs.Count {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
span.AddEvent("starting to render")
|
||||
err = bootstrap.MainPage(bootstrap.PageIndex, stats, params).Render(ctx, w)
|
||||
span.AddEvent("render finished")
|
||||
if handleError(ctx, err, w, c.log, "rendeting template") {
|
||||
return
|
||||
}
|
||||
span.SetStatus(codes.Ok, "request completed")
|
||||
}
|
||||
|
||||
var orderByListing = newOrderableContainer(
|
||||
newOrderableUnit("pr", "Price", "full_price"),
|
||||
newOrderableUnit("na", "Name", "name"),
|
||||
newOrderableUnit("di", "Discount", "discount"),
|
||||
newOrderableUnit("du", "Duration", "duration"),
|
||||
newOrderableUnit("st", "Starts At", "starts_at"),
|
||||
)
|
||||
|
||||
type orderableUnit struct {
|
||||
ID string
|
||||
Name string
|
||||
Field string
|
||||
}
|
||||
|
||||
func newOrderableUnit(id, name, field string) orderableUnit {
|
||||
return orderableUnit{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Field: field,
|
||||
}
|
||||
}
|
||||
|
||||
type orderableContainer struct {
|
||||
nameByID map[string]string
|
||||
fieldByID map[string]string
|
||||
cachedNameIDPair []bootstrap.NameIDPair
|
||||
makeCache sync.Once
|
||||
}
|
||||
|
||||
func (c *orderableContainer) asNameIDPair() []bootstrap.NameIDPair {
|
||||
c.makeCache.Do(func() {
|
||||
c.cachedNameIDPair = make([]bootstrap.NameIDPair, 0, len(c.nameByID))
|
||||
for id, name := range c.nameByID {
|
||||
c.cachedNameIDPair = append(c.cachedNameIDPair, bootstrap.NameIDPair{
|
||||
ID: id,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return c.cachedNameIDPair
|
||||
}
|
||||
|
||||
func (c *orderableContainer) getField(id string) string {
|
||||
return c.fieldByID[id]
|
||||
}
|
||||
|
||||
func newOrderableContainer(units ...orderableUnit) *orderableContainer {
|
||||
nameByID := make(map[string]string, len(units))
|
||||
fieldByID := make(map[string]string, len(units))
|
||||
|
||||
xslices.ForEach(units, func(u orderableUnit) {
|
||||
nameByID[u.ID] = u.Name
|
||||
fieldByID[u.ID] = u.Field
|
||||
})
|
||||
|
||||
return &orderableContainer{
|
||||
nameByID: nameByID,
|
||||
fieldByID: fieldByID,
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,35 +5,43 @@ import (
|
||||
stderrors "errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||
)
|
||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
const (
|
||||
defaultPerPage = 50
|
||||
"github.com/gorilla/mux"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
app app.Application
|
||||
app service.Application
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewServer(app app.Application) Server {
|
||||
return Server{}
|
||||
}
|
||||
|
||||
func (s Server) Courses() courseServer {
|
||||
return courseServer{
|
||||
app: s.app,
|
||||
func NewServer(app service.Application, log *slog.Logger) Server {
|
||||
return Server{
|
||||
app: app,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slog.Logger, msg string) {
|
||||
func (s Server) Courses() courseTemplServer {
|
||||
return courseTemplServer(s)
|
||||
}
|
||||
|
||||
func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slog.Logger, msg string) bool {
|
||||
if err == nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
span := trace.SpanFromContext(ctx)
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, "error during handling request")
|
||||
|
||||
var errorString string
|
||||
var code int
|
||||
valErr := new(errors.ValidationError)
|
||||
@ -52,4 +60,82 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
||||
xcontext.LogWithWarnError(ctx, log, err, msg, slog.Int("status_code", code), slog.String("response", errorString))
|
||||
|
||||
http.Error(w, errorString, code)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type pagination struct {
|
||||
NextPageToken string
|
||||
PerPage int
|
||||
Page int
|
||||
}
|
||||
|
||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||
query := r.URL.Query()
|
||||
|
||||
if query.Has("next") && query.Has("page") {
|
||||
return out, errors.NewValidationError("next", `could not be set together with "page"`)
|
||||
}
|
||||
|
||||
out.NextPageToken = query.Get("next")
|
||||
|
||||
if query.Has("per_page") {
|
||||
out.PerPage, err = strconv.Atoi(query.Get("per_page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("per_page", "bad per_page value")
|
||||
}
|
||||
} else {
|
||||
out.PerPage = 20
|
||||
}
|
||||
if query.Has("page") {
|
||||
out.Page, err = strconv.Atoi(query.Get("page"))
|
||||
if err != nil {
|
||||
return out, errors.NewValidationError("page", "bad per_page value")
|
||||
}
|
||||
} else if !query.Has("next") {
|
||||
out.Page = 1
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
const (
|
||||
LearningTypePathParam = "learning_type"
|
||||
ThematicTypePathParam = "thematic_type"
|
||||
)
|
||||
|
||||
func parseListCoursesParams(r *http.Request) (out listCoursesParams, err error) {
|
||||
out.pagination, err = parsePaginationFromQuery(r)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
out.LearningType = vars[LearningTypePathParam]
|
||||
out.CourseThematic = vars[ThematicTypePathParam]
|
||||
|
||||
out.School = r.URL.Query().Get("school_id")
|
||||
out.OrderBy = xdefault.WithFallback(r.URL.Query().Get("order_by"), "price")
|
||||
|
||||
if r.URL.Query().Has("asc") {
|
||||
out.Ascending, _ = strconv.ParseBool(r.URL.Query().Get("asc"))
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type listCoursesParams struct {
|
||||
pagination
|
||||
|
||||
CourseThematic string
|
||||
LearningType string
|
||||
School string
|
||||
OrderBy string
|
||||
Ascending bool
|
||||
}
|
||||
|
||||
type IDNamePair struct {
|
||||
ID string
|
||||
Name string
|
||||
IsActive bool
|
||||
}
|
||||
|
||||
@ -1,86 +0,0 @@
|
||||
{{define "courses"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Courses</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
nav li {
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
}
|
||||
nav a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.course-plate {
|
||||
background-color: #f2f2f2;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.course-plate a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.course-plate a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>My Product</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Main page</a></li>
|
||||
<li><a href="/about">About us</a></li>
|
||||
<li><a href="/help">Help</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<h1>Courses</h1>
|
||||
{{range $category, $courses := .Courses}}
|
||||
<h2>{{$category}}</h2>
|
||||
<p>{{$category.Description}}</p>
|
||||
{{range $course := $courses}}
|
||||
<div class="course-plate">
|
||||
<h3><a href="/courses/{{$course.ID}}">{{$course.Name}}</a></h3>
|
||||
<p>{{$course.Description}}</p>
|
||||
<p>Full price: {{$course.FullPrice}}</p>
|
||||
<p>Discount: {{$course.Discount}}</p>
|
||||
<p>Thematic: {{$course.Thematic}}</p>
|
||||
<p>Learning type: {{$course.LearningType}}</p>
|
||||
<p>Duration: {{$course.Duration}}</p>
|
||||
<p>Starts at: {{$course.StartsAt}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@ -2,20 +2,33 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/adapters"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/command"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type RepositoryEngine uint8
|
||||
|
||||
const (
|
||||
RepositoryEngineUnknown RepositoryEngine = iota
|
||||
RepositoryEngineYDB
|
||||
RepositoryEngineSqlite
|
||||
)
|
||||
|
||||
type ApplicationConfig struct {
|
||||
LogConfig config.Log
|
||||
YDB config.YDB
|
||||
Sqlite config.Sqlite
|
||||
Engine RepositoryEngine
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
@ -25,29 +38,57 @@ type Application struct {
|
||||
closers []io.Closer
|
||||
}
|
||||
|
||||
func NewApplication(ctx context.Context, cfg ApplicationConfig) (Application, error) {
|
||||
func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (out Application, err error) {
|
||||
log := config.NewSLogger(cfg.LogConfig)
|
||||
ydbConnection, err := adapters.NewYDBConnection(ctx, cfg.YDB, log.With(slog.String("db", "ydb")))
|
||||
if err != nil {
|
||||
return Application{}, fmt.Errorf("making ydb connection: %w", err)
|
||||
|
||||
var repoCloser io.Closer
|
||||
var courseadapter domain.CourseRepository
|
||||
var organizationrepo domain.OrganizationRepository
|
||||
switch cfg.Engine {
|
||||
case RepositoryEngineSqlite:
|
||||
sqliteConnection, err := adapters.NewSqliteConnection(ctx, cfg.Sqlite, log.With(slog.String("db", "sqlite")))
|
||||
if err != nil {
|
||||
return Application{}, fmt.Errorf("making sqlite connection: %w", err)
|
||||
}
|
||||
|
||||
courseadapter = sqliteConnection.CourseRepository()
|
||||
organizationrepo = sqliteConnection.Organization()
|
||||
repoCloser = sqliteConnection
|
||||
case RepositoryEngineYDB:
|
||||
return Application{}, errors.New("ydb is no longer supported")
|
||||
default:
|
||||
return Application{}, errors.New("unable to decide which db engine to use")
|
||||
}
|
||||
|
||||
courseadapter := ydbConnection.CourseRepository()
|
||||
err = mapper.CollectCounts(ctx, courseadapter)
|
||||
if err != nil {
|
||||
xcontext.LogWithWarnError(ctx, log, err, "unable to properly collect counts")
|
||||
}
|
||||
|
||||
application := app.Application{
|
||||
Commands: app.Commands{
|
||||
InsertCourses: command.NewCreateCoursesHandler(courseadapter, log),
|
||||
InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
|
||||
DeleteCourse: command.NewDeleteCourseHandler(courseadapter, log),
|
||||
InsertCourses: command.NewCreateCoursesHandler(courseadapter, log),
|
||||
InsertCourse: command.NewCreateCourseHandler(courseadapter, log),
|
||||
DeleteCourse: command.NewDeleteCourseHandler(courseadapter, log),
|
||||
UpdateCourseDescription: command.NewUpdateCourseDescriptionHandler(courseadapter, log),
|
||||
|
||||
InsertOrganization: command.NewCreateOrganizationHandler(organizationrepo, log),
|
||||
},
|
||||
Queries: app.Queries{
|
||||
GetCourse: query.NewGetCourseHandler(courseadapter, log),
|
||||
ListCourses: query.NewListCourseHandler(courseadapter, log),
|
||||
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
|
||||
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
|
||||
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
|
||||
ListCourseStatistics: query.NewListCoursesStatsHandler(mapper, courseadapter, log),
|
||||
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
|
||||
|
||||
ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log),
|
||||
ListOrganizationsStats: query.NewListOrganizationsStatsHandler(organizationrepo, log),
|
||||
GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log),
|
||||
},
|
||||
}
|
||||
|
||||
out := Application{Application: application}
|
||||
out.closers = append(out.closers, ydbConnection)
|
||||
out = Application{Application: application}
|
||||
out.closers = append(out.closers, repoCloser)
|
||||
out.log = log
|
||||
|
||||
return out, nil
|
||||
|
||||
24
migrations/sqlite/001_initial.sql
Normal file
24
migrations/sqlite/001_initial.sql
Normal file
@ -0,0 +1,24 @@
|
||||
CREATE TABLE courses (
|
||||
id TEXT PRIMARY KEY,
|
||||
external_id TEXT,
|
||||
source_type TEXT NOT NULL,
|
||||
source_name TEXT,
|
||||
course_thematic TEXT NOT NULL,
|
||||
learning_type TEXT NOT NULL,
|
||||
organization_id TEXT NOT NULL,
|
||||
origin_link TEXT NOT NULL,
|
||||
image_link TEXT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
full_price REAL NOT NULL,
|
||||
discount REAL NOT NULL,
|
||||
duration INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
starts_at DATETIME,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX idx_course_thematic ON courses (course_thematic);
|
||||
CREATE INDEX idx_learning_type ON courses (learning_type);
|
||||
CREATE INDEX idx_organization_id ON courses (organization_id);
|
||||
19
migrations/sqlite/002_learning_and_organization.sql
Normal file
19
migrations/sqlite/002_learning_and_organization.sql
Normal file
@ -0,0 +1,19 @@
|
||||
CREATE TABLE learning_categories (
|
||||
id TEXT PRIMARY KEY,
|
||||
logo TEXT NULL,
|
||||
courses_count INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
create table organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
external_id TEXT NULL,
|
||||
alias TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
site TEXT NOT NULL,
|
||||
logo TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_organization_external_id ON organizations (external_id);
|
||||
18
migrations/sqlite/assets.go
Normal file
18
migrations/sqlite/assets.go
Normal file
@ -0,0 +1,18 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed *.sql
|
||||
var migrations embed.FS
|
||||
|
||||
func getMigrationEntries() ([]fs.DirEntry, error) {
|
||||
entries, err := fs.ReadDir(migrations, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
240
migrations/sqlite/migrator.go
Normal file
240
migrations/sqlite/migrator.go
Normal file
@ -0,0 +1,240 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||
)
|
||||
|
||||
type migrationUnit struct {
|
||||
num int
|
||||
name string
|
||||
path string
|
||||
}
|
||||
|
||||
func (u migrationUnit) apply(ctx context.Context, tx *sql.Tx) error {
|
||||
content, err := fs.ReadFile(migrations, u.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading file: %w", err)
|
||||
}
|
||||
|
||||
query := string(content)
|
||||
_, err = tx.ExecContext(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortMigrationUnit(lhs, rhs migrationUnit) int {
|
||||
if lhs.num < rhs.num {
|
||||
return -1
|
||||
} else if lhs.num > rhs.num {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func RunMigrations(ctx context.Context, db *sql.DB, log *slog.Logger) error {
|
||||
items, err := getMigrationEntries()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading directory: %w", err)
|
||||
}
|
||||
|
||||
units := make([]migrationUnit, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
itemName := item.Name()
|
||||
splitted := strings.SplitN(itemName, "_", 2)
|
||||
if len(splitted) != 2 {
|
||||
return fmt.Errorf("bad number of parts, expected 2, got %d", len(splitted))
|
||||
}
|
||||
|
||||
splittedNum, err := strconv.Atoi(splitted[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing migration number: %w", err)
|
||||
}
|
||||
|
||||
if splittedNum < 1 {
|
||||
return fmt.Errorf("migration number expected to be greater than 0, but got %d", splittedNum)
|
||||
}
|
||||
|
||||
unit := migrationUnit{
|
||||
num: splittedNum,
|
||||
name: strings.TrimSuffix(splitted[1], ".sql"),
|
||||
path: path.Join(itemName),
|
||||
}
|
||||
|
||||
xcontext.LogDebug(ctx, log, "found migration unit", slog.Any("unit", unit))
|
||||
|
||||
units = append(units, unit)
|
||||
}
|
||||
|
||||
slices.SortFunc(units, sortMigrationUnit)
|
||||
|
||||
mr := &metaRepository{
|
||||
db: db,
|
||||
log: log,
|
||||
}
|
||||
err = mr.prepare(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preparing meta repository: %w", err)
|
||||
}
|
||||
|
||||
count, err := mr.run(ctx, units...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("running transaction: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
xcontext.LogInfo(ctx, log, "some new migrations has been applied", slog.Int("count", count))
|
||||
} else {
|
||||
xcontext.LogDebug(ctx, log, "no new migrations has been applied")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type metaRepository struct {
|
||||
db *sql.DB
|
||||
log *slog.Logger
|
||||
lastAppliedNumber int
|
||||
}
|
||||
|
||||
func (r *metaRepository) run(ctx context.Context, units ...migrationUnit) (count int, err error) {
|
||||
idx, found := slices.BinarySearchFunc(units, r.lastAppliedNumber, func(mu migrationUnit, i int) int {
|
||||
return cmp.Compare(mu.num, i)
|
||||
})
|
||||
if !found && r.lastAppliedNumber > 0 {
|
||||
return 0, fmt.Errorf("migration %d stored in meta was not found in provided migrations", r.lastAppliedNumber)
|
||||
} else if r.lastAppliedNumber > 0 {
|
||||
idx++
|
||||
}
|
||||
|
||||
xcontext.LogDebug(
|
||||
ctx, r.log,
|
||||
"starting to apply migrations",
|
||||
slog.Int("last_applied_migration", r.lastAppliedNumber),
|
||||
slog.Int("next_migration_idx", idx),
|
||||
)
|
||||
|
||||
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{
|
||||
Isolation: sql.LevelDefault,
|
||||
ReadOnly: false,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("starting transaction: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
var errtx error
|
||||
if err != nil {
|
||||
xcontext.LogError(ctx, r.log, "rolling back migration changes due to error")
|
||||
errtx = tx.Rollback()
|
||||
} else {
|
||||
xcontext.LogDebug(ctx, r.log, "commiting migration changes")
|
||||
errtx = tx.Commit()
|
||||
}
|
||||
|
||||
err = errors.Join(err, errtx)
|
||||
}()
|
||||
|
||||
for i := idx; i < len(units); i++ {
|
||||
unit := units[i]
|
||||
err = unit.apply(ctx, tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to apply migration %q: %w", unit.name, err)
|
||||
}
|
||||
|
||||
err = r.adjustMigrationApplied(ctx, tx, unit)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("storing migration process info: %w", err)
|
||||
}
|
||||
|
||||
xcontext.LogInfo(
|
||||
ctx, r.log, "migration unit applied",
|
||||
slog.Int("number", unit.num),
|
||||
slog.String("name", unit.name),
|
||||
)
|
||||
count++
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *metaRepository) prepare(ctx context.Context) error {
|
||||
err := r.makeTable(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making table: %w", err)
|
||||
}
|
||||
|
||||
err = r.loadLastAppliedMigration(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading last applied migration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *metaRepository) makeTable(ctx context.Context) error {
|
||||
const query = `CREATE TABLE IF NOT EXISTS migration_meta (` +
|
||||
` id INT PRIMARY KEY NOT NULL` +
|
||||
`, name TEXT NOT NULL` +
|
||||
`, applied_at INT NOT NULL` +
|
||||
`);`
|
||||
|
||||
_, err := r.db.ExecContext(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *metaRepository) loadLastAppliedMigration(ctx context.Context) error {
|
||||
const query = `SELECT COALESCE(MAX(id), 0) FROM migration_meta;`
|
||||
|
||||
err := r.db.QueryRowContext(ctx, query).Scan(&r.lastAppliedNumber)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *metaRepository) adjustMigrationApplied(ctx context.Context, tx *sql.Tx, unit migrationUnit) error {
|
||||
const query = `INSERT INTO migration_meta (id, name, applied_at) VALUES (?, ?, ?)`
|
||||
|
||||
args := []any{
|
||||
unit.num,
|
||||
unit.name,
|
||||
time.Now().Truncate(time.Second).Unix(),
|
||||
}
|
||||
|
||||
_, err := tx.ExecContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("executing query: %w", err)
|
||||
}
|
||||
|
||||
r.lastAppliedNumber = unit.num
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user