add pagination
This commit is contained in:
@ -1 +1 @@
|
|||||||
3cf236a901d03e42352790df844d58c5
|
de22f926e2940e47830ef3a33736db71
|
||||||
|
|||||||
@ -43,9 +43,9 @@ func setupCoursesHTTP(srv xhttp.Server, router *mux.Router, _ *slog.Logger) {
|
|||||||
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
coursesListLearningOnlyPath := makePathTemplate(xhttp.LearningTypePathParam)
|
||||||
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
coursesListFullPath := makePathTemplate(xhttp.LearningTypePathParam, xhttp.ThematicTypePathParam)
|
||||||
|
|
||||||
muxHandleFunc(coursesRouter, "/", coursesAPI.Index).Methods(http.MethodGet)
|
muxHandleFunc(coursesRouter, "index", "/", coursesAPI.Index).Methods(http.MethodGet)
|
||||||
muxHandleFunc(coursesRouter, coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
muxHandleFunc(coursesRouter, "list_learning", coursesListLearningOnlyPath, coursesAPI.List).Methods(http.MethodGet)
|
||||||
muxHandleFunc(coursesRouter, coursesListFullPath, 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 {
|
func setupHTTP(cfg config.HTTP, srv xhttp.Server, log *slog.Logger) *http.Server {
|
||||||
@ -111,6 +111,7 @@ func middlewareTrace() mux.MiddlewareFunc {
|
|||||||
reqidAttr := attribute.Key("http.request_id")
|
reqidAttr := attribute.Key("http.request_id")
|
||||||
statusAttr := attribute.Key("http.status_code")
|
statusAttr := attribute.Key("http.status_code")
|
||||||
payloadAttr := attribute.Key("http.payload_size")
|
payloadAttr := attribute.Key("http.payload_size")
|
||||||
|
pathAttr := attribute.Key("http.template_path")
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -118,7 +119,16 @@ func middlewareTrace() mux.MiddlewareFunc {
|
|||||||
reqid := xcontext.GetRequestID(ctx)
|
reqid := xcontext.GetRequestID(ctx)
|
||||||
|
|
||||||
var span trace.Span
|
var span trace.Span
|
||||||
ctx, span = webtracer.Start(ctx, r.URL.String(), trace.WithAttributes(reqidAttr.String(reqid)))
|
route := mux.CurrentRoute(r)
|
||||||
|
hname := route.GetName()
|
||||||
|
hpath, _ := route.GetPathTemplate()
|
||||||
|
ctx, span = webtracer.Start(
|
||||||
|
ctx, "http."+hname,
|
||||||
|
trace.WithAttributes(
|
||||||
|
reqidAttr.String(reqid),
|
||||||
|
pathAttr.String(hpath),
|
||||||
|
),
|
||||||
|
)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
@ -131,6 +141,8 @@ func middlewareTrace() mux.MiddlewareFunc {
|
|||||||
)
|
)
|
||||||
if statusCode > 399 {
|
if statusCode > 399 {
|
||||||
span.SetStatus(codes.Error, "error during request")
|
span.SetStatus(codes.Error, "error during request")
|
||||||
|
} else {
|
||||||
|
span.SetStatus(codes.Ok, "request completed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/sdk/trace"
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
var webtracer = otel.Tracer("kuriweb")
|
var webtracer = otel.Tracer("kuriweb.http")
|
||||||
|
|
||||||
type shutdownFunc func(context.Context) error
|
type shutdownFunc func(context.Context) error
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ func newMeterProvider() (*metric.MeterProvider, error) {
|
|||||||
return meterProvider, nil
|
return meterProvider, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func muxHandleFunc(router *mux.Router, path string, hf http.HandlerFunc) *mux.Route {
|
func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route {
|
||||||
h := otelhttp.WithRouteTag(path, hf)
|
h := otelhttp.WithRouteTag(path, hf)
|
||||||
return router.Handle(path, h)
|
return router.Handle(path, h).Name(name)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
<p class="justify-content-center">Here you can find course for any taste</p>
|
<p class="justify-content-center">Here you can find course for any taste</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container w-75">
|
<div class="container w-75 mb-4">
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
|
|
||||||
@ -55,6 +55,11 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<p>In this category you can find courses of types such as</p>
|
<p>In this category you can find courses of types such as</p>
|
||||||
<p>web-development, backend development, frontend developent</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>
|
<p>This category contains <span>128</span> courses.</p>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
<a href="#" class="btn btn-sm btn-outline-primary col-6">
|
||||||
@ -127,56 +132,19 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row categories">
|
<nav aria-label="Page navigation example">
|
||||||
<div class="col">
|
<ul class="pagination justify-content-center">
|
||||||
<div class="block">
|
<li class="page-item disabled">
|
||||||
<ul>
|
<a class="page-link">Previous</a>
|
||||||
<p>Category 1</p>
|
</li>
|
||||||
<li>
|
<li class="page-item active"><a class="page-link" href="#">1</a></li>
|
||||||
<div class="block">item</div>
|
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||||
</li>
|
<li class="page-item"><a class="page-link" href="#">3</a></li>
|
||||||
<li>
|
<li class="page-item">
|
||||||
<div class="block">item</div>
|
<a class="page-link" href="#">Next</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
</ul>
|
||||||
<div class="block">item</div>
|
</nav>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="block">
|
|
||||||
<ul>
|
|
||||||
<p>Category 2</p>
|
|
||||||
<li>
|
|
||||||
<div class="block">item</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div class="block">item</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div class="block">item</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<div class="block">
|
|
||||||
<ul>
|
|
||||||
<p>Category 3</p>
|
|
||||||
<li>
|
|
||||||
<div class="block">item</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div class="block">item</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div class="block">item</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|||||||
@ -11,9 +11,9 @@ type inMemoryMapper struct {
|
|||||||
courseThematicsByID map[string]string
|
courseThematicsByID map[string]string
|
||||||
learningTypeByID map[string]string
|
learningTypeByID map[string]string
|
||||||
|
|
||||||
courseThematicsCountByID map[string]int
|
stats map[string]domain.LearningTypeStat
|
||||||
learningTypeCountByID map[string]int
|
courseThematicByLearningType map[string]string
|
||||||
totalCount int
|
totalCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
|
func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
|
||||||
@ -26,8 +26,8 @@ func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryM
|
|||||||
func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error {
|
func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error {
|
||||||
const batchSize = 1000
|
const batchSize = 1000
|
||||||
|
|
||||||
m.courseThematicsCountByID = map[string]int{}
|
m.stats = map[string]domain.LearningTypeStat{}
|
||||||
m.learningTypeCountByID = map[string]int{}
|
m.courseThematicByLearningType = map[string]string{}
|
||||||
|
|
||||||
var nextPageToken string
|
var nextPageToken string
|
||||||
for {
|
for {
|
||||||
@ -42,8 +42,14 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo
|
|||||||
}
|
}
|
||||||
m.totalCount += len(result.Courses)
|
m.totalCount += len(result.Courses)
|
||||||
for _, course := range result.Courses {
|
for _, course := range result.Courses {
|
||||||
m.courseThematicsCountByID[course.ThematicID]++
|
stat, ok := m.stats[course.LearningTypeID]
|
||||||
m.learningTypeCountByID[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 {
|
if len(result.Courses) < batchSize {
|
||||||
break
|
break
|
||||||
@ -55,13 +61,36 @@ func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int {
|
func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int {
|
||||||
if byCourseThematic != "" {
|
if byCourseThematic != "" && byLearningType == "" {
|
||||||
return m.courseThematicsCountByID[byCourseThematic]
|
byLearningType = m.courseThematicByLearningType[byCourseThematic]
|
||||||
} else if byLearningType != "" {
|
|
||||||
return m.learningTypeCountByID[byLearningType]
|
|
||||||
} else {
|
|
||||||
return m.totalCount
|
|
||||||
}
|
}
|
||||||
|
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 {
|
func (m *inMemoryMapper) CourseThematicNameByID(id string) string {
|
||||||
|
|||||||
@ -11,14 +11,19 @@ import (
|
|||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/config"
|
"git.loyso.art/frx/kurious/internal/common/config"
|
||||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
"git.loyso.art/frx/kurious/migrations/sqlite"
|
"git.loyso.art/frx/kurious/migrations/sqlite"
|
||||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var sqliteTracer = otel.Tracer("sqlite")
|
||||||
|
|
||||||
type sqliteConnection struct {
|
type sqliteConnection struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
shutdownTimeout time.Duration
|
shutdownTimeout time.Duration
|
||||||
@ -68,8 +73,20 @@ func (r *sqliteCourseRepository) List(
|
|||||||
) (result domain.ListCoursesResult, err error) {
|
) (result domain.ListCoursesResult, err error) {
|
||||||
const queryTemplate = `SELECT %s from courses WHERE 1=1`
|
const queryTemplate = `SELECT %s from courses WHERE 1=1`
|
||||||
|
|
||||||
|
ctx, span := sqliteTracer.Start(ctx, "sqlite.list")
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
span.End()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if params.NextPageToken != "" && params.Offset > 0 {
|
||||||
|
panic("could not use next_page_token and offset at the same time")
|
||||||
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
||||||
args := make([]any, 0, 1)
|
args := make([]any, 0, 6)
|
||||||
if params.LearningType != "" {
|
if params.LearningType != "" {
|
||||||
args = append(args, params.LearningType)
|
args = append(args, params.LearningType)
|
||||||
query += " AND learning_type = ?"
|
query += " AND learning_type = ?"
|
||||||
@ -87,13 +104,20 @@ func (r *sqliteCourseRepository) List(
|
|||||||
query += " AND id > ?"
|
query += " AND id > ?"
|
||||||
}
|
}
|
||||||
|
|
||||||
query += " ORDER BY id ASC"
|
query += " ORDER BY course_thematic, learning_type, id ASC"
|
||||||
|
|
||||||
if params.Limit > 0 {
|
if params.Limit > 0 {
|
||||||
query += " LIMIT ?"
|
query += " LIMIT ?"
|
||||||
args = append(args, params.Limit)
|
args = append(args, params.Limit)
|
||||||
}
|
}
|
||||||
|
if params.Offset > 0 {
|
||||||
|
query += " OFFSET ?"
|
||||||
|
args = append(args, params.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("query", query),
|
||||||
|
)
|
||||||
scanF := func(s rowsScanner) (err error) {
|
scanF := func(s rowsScanner) (err error) {
|
||||||
var cdb sqliteCourseDB
|
var cdb sqliteCourseDB
|
||||||
err = s.StructScan(&cdb)
|
err = s.StructScan(&cdb)
|
||||||
@ -114,6 +138,15 @@ func (r *sqliteCourseRepository) List(
|
|||||||
result.NextPageToken = result.Courses[lastIDx].ID
|
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("items_count", len(result.Courses)),
|
||||||
|
attribute.Int("total_items", result.Count),
|
||||||
|
)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,6 +256,40 @@ func (r *sqliteCourseRepository) Delete(ctx context.Context, id string) error {
|
|||||||
return errors.New("unimplemented")
|
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`
|
||||||
|
|
||||||
|
ctx, span := sqliteTracer.Start(ctx, "sqlite.listCount")
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
}
|
||||||
|
span.End()
|
||||||
|
}()
|
||||||
|
|
||||||
|
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 = ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
type rowsScanner interface {
|
||||||
sqlx.ColScanner
|
sqlx.ColScanner
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package adapters
|
package adapters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -74,3 +75,42 @@ func (s *sqliteCourseRepositorySuite) TestCreateCourse() {
|
|||||||
|
|
||||||
s.Require().Equal(expcourse, gotCourse)
|
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)
|
||||||
|
|
||||||
|
params.Offset = 0
|
||||||
|
for i := 0; i < listparts; i++ {
|
||||||
|
result, err := cr.List(s.ctx, params)
|
||||||
|
s.NoError(err)
|
||||||
|
s.Len(result.Courses, listparts)
|
||||||
|
|
||||||
|
params.NextPageToken = result.NextPageToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -15,10 +15,11 @@ type Commands struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Queries struct {
|
type Queries struct {
|
||||||
GetCourse query.GetCourseHandler
|
GetCourse query.GetCourseHandler
|
||||||
ListCourses query.ListCourseHandler
|
ListCourses query.ListCourseHandler
|
||||||
ListLearningTypes query.ListLearningTypesHandler
|
ListLearningTypes query.ListLearningTypesHandler
|
||||||
ListCourseThematics query.ListCourseThematicsHandler
|
ListCourseThematics query.ListCourseThematicsHandler
|
||||||
|
ListCourseStatistics query.ListCoursesStatsHandler
|
||||||
|
|
||||||
ListOrganzations query.ListOrganizationsHandler
|
ListOrganzations query.ListOrganizationsHandler
|
||||||
GetOrganization query.GetOrganizationHandler
|
GetOrganization query.GetOrganizationHandler
|
||||||
|
|||||||
@ -18,6 +18,7 @@ type ListCourse struct {
|
|||||||
Keyword string
|
Keyword string
|
||||||
|
|
||||||
Limit int
|
Limit int
|
||||||
|
Offset int
|
||||||
NextPageToken string
|
NextPageToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,11 +42,14 @@ func NewListCourseHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
||||||
out.AvailableCoursesOfSub = map[string]int{}
|
const defaultBatchSize = 1_000
|
||||||
|
|
||||||
out.NextPageToken = query.NextPageToken
|
out.NextPageToken = query.NextPageToken
|
||||||
drainFull := query.Limit == 0
|
drainFull := query.Limit == 0
|
||||||
if !drainFull {
|
if !drainFull {
|
||||||
out.Courses = make([]domain.Course, 0, query.Limit)
|
out.Courses = make([]domain.Course, 0, query.Limit)
|
||||||
|
} else {
|
||||||
|
query.Limit = defaultBatchSize
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
result, err := h.repo.List(ctx, domain.ListCoursesParams{
|
result, err := h.repo.List(ctx, domain.ListCoursesParams{
|
||||||
@ -53,8 +57,8 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
|
|||||||
LearningType: query.LearningType,
|
LearningType: query.LearningType,
|
||||||
OrganizationID: query.OrganizationID,
|
OrganizationID: query.OrganizationID,
|
||||||
Limit: query.Limit,
|
Limit: query.Limit,
|
||||||
|
Offset: query.Offset,
|
||||||
NextPageToken: out.NextPageToken,
|
NextPageToken: out.NextPageToken,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, fmt.Errorf("listing courses: %w", err)
|
return out, fmt.Errorf("listing courses: %w", err)
|
||||||
@ -69,19 +73,15 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
|
|||||||
|
|
||||||
out.Courses = append(out.Courses, result.Courses...)
|
out.Courses = append(out.Courses, result.Courses...)
|
||||||
out.NextPageToken = result.NextPageToken
|
out.NextPageToken = result.NextPageToken
|
||||||
|
out.Count = result.Count
|
||||||
|
|
||||||
if drainFull && len(result.Courses) > 0 && result.NextPageToken != "" {
|
if drainFull && len(result.Courses) == query.Limit {
|
||||||
|
query.Offset += query.Limit
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, course := range out.Courses {
|
|
||||||
if _, ok := out.AvailableCoursesOfSub[course.ThematicID]; !ok {
|
|
||||||
out.AvailableCoursesOfSub[course.ThematicID] = h.mapper.GetCounts(course.ThematicID, course.LearningTypeID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
38
internal/kurious/app/query/listcoursesstats.go
Normal file
38
internal/kurious/app/query/listcoursesstats.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.loyso.art/frx/kurious/internal/common/decorator"
|
||||||
|
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListCoursesStats struct{}
|
||||||
|
type ListCoursesStatsResult struct{}
|
||||||
|
|
||||||
|
type ListCoursesStatsHandler decorator.QueryHandler[ListCoursesStats, domain.ListCoursesStatsResult]
|
||||||
|
|
||||||
|
type listCoursesStatsHandler struct {
|
||||||
|
mapper domain.CourseMapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListCoursesStatsHandler(
|
||||||
|
mapper domain.CourseMapper,
|
||||||
|
log *slog.Logger,
|
||||||
|
) ListCoursesStatsHandler {
|
||||||
|
h := listCoursesStatsHandler{
|
||||||
|
mapper: mapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorator.AddQueryDecorators(h, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h listCoursesStatsHandler) Handle(
|
||||||
|
ctx context.Context,
|
||||||
|
query ListCoursesStats,
|
||||||
|
) (out domain.ListCoursesStatsResult, err error) {
|
||||||
|
stats := h.mapper.GetStats(false)
|
||||||
|
out.StatsByLearningType = stats
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@ -8,4 +8,5 @@ type CourseMapper interface {
|
|||||||
|
|
||||||
CollectCounts(context.Context, CourseRepository) error
|
CollectCounts(context.Context, CourseRepository) error
|
||||||
GetCounts(byCourseThematic, byLearningType string) int
|
GetCounts(byCourseThematic, byLearningType string) int
|
||||||
|
GetStats(copyMap bool) map[string]LearningTypeStat
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ type ListCoursesParams struct {
|
|||||||
|
|
||||||
NextPageToken string
|
NextPageToken string
|
||||||
Limit int
|
Limit int
|
||||||
|
Offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateCourseParams struct {
|
type CreateCourseParams struct {
|
||||||
@ -34,10 +35,19 @@ type CreateCourseParams struct {
|
|||||||
StartsAt time.Time
|
StartsAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LearningTypeStat struct {
|
||||||
|
Count int
|
||||||
|
CourseThematic map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCoursesStatsResult struct {
|
||||||
|
StatsByLearningType map[string]LearningTypeStat
|
||||||
|
}
|
||||||
|
|
||||||
type ListCoursesResult struct {
|
type ListCoursesResult struct {
|
||||||
Courses []Course
|
Courses []Course
|
||||||
AvailableCoursesOfSub map[string]int
|
NextPageToken string
|
||||||
NextPageToken string
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListLearningTypeResult struct {
|
type ListLearningTypeResult struct {
|
||||||
|
|||||||
@ -81,7 +81,7 @@ templ footer() {
|
|||||||
</footer>
|
</footer>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ root(page PageKind, s stats) {
|
templ root(page PageKind, _ stats) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
@head(string(page))
|
@head(string(page))
|
||||||
|
|||||||
@ -201,7 +201,7 @@ func footer() templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func root(page PageKind, s stats) templ.Component {
|
func root(page PageKind, _ stats) templ.Component {
|
||||||
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
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)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
|||||||
@ -169,5 +169,6 @@ templ ListCourses(pageType PageKind, s stats, params ListCoursesParams) {
|
|||||||
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
|
@listCoursesSectionHeader(params.FilterForm.BreadcrumbsParams)
|
||||||
@listCoursesSectionFilters(params.FilterForm)
|
@listCoursesSectionFilters(params.FilterForm)
|
||||||
@listCoursesLearning(params.Categories)
|
@listCoursesLearning(params.Categories)
|
||||||
|
@pagination(params.Pagination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -750,6 +750,14 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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 {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,11 @@ package bootstrap
|
|||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
type IndexCourseCategoryItem struct {
|
type IndexCourseCategoryItem struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Count int
|
ExampleThemes []string
|
||||||
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
// courseItemCard is a card that renders a single course thematic item
|
// courseItemCard is a card that renders a single course thematic item
|
||||||
@ -18,6 +19,16 @@ templ courseItemCard(item IndexCourseCategoryItem) {
|
|||||||
<h5 class="card-title">{ item.Name }</h5>
|
<h5 class="card-title">{ item.Name }</h5>
|
||||||
<hr/>
|
<hr/>
|
||||||
<p>{ item.Description }</p>
|
<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">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<a
|
<a
|
||||||
href={ templ.URL("/courses/" + item.ID) }
|
href={ templ.URL("/courses/" + item.ID) }
|
||||||
@ -34,7 +45,7 @@ templ courseItemCard(item IndexCourseCategoryItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templ courseCategory(items []IndexCourseCategoryItem) {
|
templ courseCategory(items []IndexCourseCategoryItem) {
|
||||||
<div class="container w-75">
|
<div class="container w-75 mb-4">
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
<div class="col-12 col-md-8 col-lg-4">
|
<div class="col-12 col-md-8 col-lg-4">
|
||||||
@ -45,14 +56,57 @@ templ courseCategory(items []IndexCourseCategoryItem) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
type MainPageParams struct{
|
type Pagination struct {
|
||||||
|
Page int
|
||||||
|
TotalPages int
|
||||||
|
BaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ pagination(p Pagination) {
|
||||||
|
if p.Page > 0 {
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
<li
|
||||||
|
class={
|
||||||
|
"page-item",
|
||||||
|
templ.KV("disabled", p.Page > 0 && 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
|
Breadcrumbs BreadcrumbsParams
|
||||||
Categories []IndexCourseCategoryItem
|
Categories []IndexCourseCategoryItem
|
||||||
|
Pagination Pagination
|
||||||
}
|
}
|
||||||
|
|
||||||
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
|
templ MainPage(pageType PageKind, s stats, params MainPageParams) {
|
||||||
@root(pageType, s) {
|
@root(pageType, s) {
|
||||||
@listCoursesSectionHeader(params.Breadcrumbs)
|
@listCoursesSectionHeader(params.Breadcrumbs)
|
||||||
@courseCategory(params.Categories)
|
@courseCategory(params.Categories)
|
||||||
|
@pagination(params.Pagination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,10 +13,11 @@ import "bytes"
|
|||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
type IndexCourseCategoryItem struct {
|
type IndexCourseCategoryItem struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Count int
|
ExampleThemes []string
|
||||||
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
// courseItemCard is a card that renders a single course thematic item
|
// courseItemCard is a card that renders a single course thematic item
|
||||||
@ -42,7 +43,7 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
|||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 17, Col: 37}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 18, Col: 37}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@ -55,18 +56,60 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
|||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Description)
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Description)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 19, Col: 24}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 20, Col: 24}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"d-flex justify-content-between align-items-center\"><a href=\"")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var4 templ.SafeURL = templ.URL("/courses/" + item.ID)
|
if len(item.ExampleThemes) > 0 {
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var4 := `В данной категории вы можете найти курсы по темам:`
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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_Var5 string
|
||||||
|
templ_7745c5c3_Var5, 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: 26, Col: 69}
|
||||||
|
}
|
||||||
|
_, 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("</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_Var6 templ.SafeURL = templ.URL("/courses/" + item.ID)
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -74,8 +117,8 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Var5 := `Open`
|
templ_7745c5c3_Var7 := `Open`
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -83,12 +126,12 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(item.Count))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 28, Col: 31}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/main.templ`, Line: 39, Col: 31}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -96,8 +139,8 @@ func courseItemCard(item IndexCourseCategoryItem) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Var7 := `items.`
|
templ_7745c5c3_Var9 := `items.`
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -120,12 +163,12 @@ func courseCategory(items []IndexCourseCategoryItem) templ.Component {
|
|||||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var8 == nil {
|
if templ_7745c5c3_Var10 == nil {
|
||||||
templ_7745c5c3_Var8 = templ.NopComponent
|
templ_7745c5c3_Var10 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75\"><div class=\"row g-4\">")
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container w-75 mb-4\"><div class=\"row g-4\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@ -154,9 +197,162 @@ func courseCategory(items []IndexCourseCategoryItem) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_Var11 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var11 == nil {
|
||||||
|
templ_7745c5c3_Var11 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
if p.Page > 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_Var12 = []any{
|
||||||
|
"page-item",
|
||||||
|
templ.KV("disabled", p.Page > 0 && p.Page == 1),
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var12).String()))
|
||||||
|
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_Var13 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page-1))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var13)))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var14 := `Previous`
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
for i := max(p.Page-2, 1); i < min(p.TotalPages, 10); i++ {
|
||||||
|
var templ_7745c5c3_Var15 = []any{
|
||||||
|
"page-item",
|
||||||
|
templ.KV("active", p.Page == i),
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var15).String()))
|
||||||
|
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_Var16 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(i))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var16)))
|
||||||
|
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_Var17 string
|
||||||
|
templ_7745c5c3_Var17, 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: 83, Col: 101}
|
||||||
|
}
|
||||||
|
_, 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></li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 = []any{
|
||||||
|
"page-item",
|
||||||
|
templ.KV("disabled", p.Page == p.TotalPages),
|
||||||
|
}
|
||||||
|
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("<li class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var18).String()))
|
||||||
|
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_Var19 templ.SafeURL = templ.URL(p.BaseURL + "?page=" + strconv.Itoa(p.Page+1))
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var19)))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Var20 := `Next`
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</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 {
|
type MainPageParams struct {
|
||||||
Breadcrumbs BreadcrumbsParams
|
Breadcrumbs BreadcrumbsParams
|
||||||
Categories []IndexCourseCategoryItem
|
Categories []IndexCourseCategoryItem
|
||||||
|
Pagination Pagination
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component {
|
func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component {
|
||||||
@ -167,12 +363,12 @@ func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component
|
|||||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var9 == nil {
|
if templ_7745c5c3_Var21 == nil {
|
||||||
templ_7745c5c3_Var9 = templ.NopComponent
|
templ_7745c5c3_Var21 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Var10 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
templ_7745c5c3_Var22 := 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)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
templ_7745c5c3_Buffer = templ.GetBuffer()
|
templ_7745c5c3_Buffer = templ.GetBuffer()
|
||||||
@ -190,12 +386,20 @@ func MainPage(pageType PageKind, s stats, params MainPageParams) templ.Component
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
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 {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
|
||||||
}
|
}
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
})
|
})
|
||||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var22), templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,8 @@ type SubcategoryContainer struct {
|
|||||||
type ListCoursesParams struct {
|
type ListCoursesParams struct {
|
||||||
FilterForm FilterFormParams
|
FilterForm FilterFormParams
|
||||||
Categories []CategoryContainer
|
Categories []CategoryContainer
|
||||||
|
Pagination Pagination
|
||||||
|
Items int
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOrFallback[T comparable](value T, fallback T) T {
|
func GetOrFallback[T comparable](value T, fallback T) T {
|
||||||
@ -105,3 +107,25 @@ func GetOrFallback[T comparable](value T, fallback T) T {
|
|||||||
}
|
}
|
||||||
return value
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -2,11 +2,9 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.loyso.art/frx/kurious/internal/common/xslices"
|
"git.loyso.art/frx/kurious/internal/common/xslices"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
"git.loyso.art/frx/kurious/internal/kurious/app/query"
|
||||||
@ -16,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ type courseTemplServer struct {
|
|||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) bootstrap.ListCoursesParams {
|
func makeTemplListCoursesParams(counts map[string]domain.LearningTypeStat, in ...domain.Course) bootstrap.ListCoursesParams {
|
||||||
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
|
coursesBySubcategory := make(map[string][]bootstrap.CourseInfo, len(in))
|
||||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||||
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
|
categoryByID := make(map[string]bootstrap.CategoryBaseInfo, len(in))
|
||||||
@ -61,7 +60,7 @@ func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) boot
|
|||||||
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
|
categoryByID[c.ThematicID] = bootstrap.CategoryBaseInfo{
|
||||||
ID: c.ThematicID,
|
ID: c.ThematicID,
|
||||||
Name: c.Thematic,
|
Name: c.Thematic,
|
||||||
Count: counts[c.ThematicID],
|
Count: counts[c.LearningTypeID].CourseThematic[c.ThematicID],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -91,7 +90,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
var span trace.Span
|
var span trace.Span
|
||||||
ctx, span = webtracer.Start(ctx, "list")
|
ctx, span = webtracer.Start(ctx, "http_server.list")
|
||||||
defer func() {
|
defer func() {
|
||||||
span.End()
|
span.End()
|
||||||
}()
|
}()
|
||||||
@ -106,17 +105,26 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonParams, _ := json.Marshal(pathParams)
|
jsonParams, _ := json.Marshal(pathParams)
|
||||||
span.SetAttributes(paramsAttr.String(string(jsonParams)))
|
span.SetAttributes(paramsAttr.String(string(jsonParams)))
|
||||||
|
|
||||||
|
var offset int
|
||||||
|
if pathParams.Page > 0 {
|
||||||
|
offset = (pathParams.Page - 1) * pathParams.PerPage
|
||||||
|
}
|
||||||
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
listCoursesResult, err := c.app.Queries.ListCourses.Handle(ctx, query.ListCourse{
|
||||||
CourseThematic: pathParams.CourseThematic,
|
CourseThematic: pathParams.CourseThematic,
|
||||||
LearningType: pathParams.LearningType,
|
LearningType: pathParams.LearningType,
|
||||||
Limit: pathParams.PerPage,
|
Limit: pathParams.PerPage,
|
||||||
NextPageToken: pathParams.NextPageToken,
|
NextPageToken: pathParams.NextPageToken,
|
||||||
|
Offset: offset,
|
||||||
})
|
})
|
||||||
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
if handleError(ctx, err, w, c.log, "unable to list courses") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
statsresult, err := c.app.Queries.ListCourseStatistics.Handle(ctx, query.ListCoursesStats{})
|
||||||
|
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{})
|
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||||
@ -156,12 +164,6 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.log.DebugContext(
|
|
||||||
ctx, "using bootstrap",
|
|
||||||
slog.Int("course_thematic", len(params.FilterForm.AvailableCourseThematics)),
|
|
||||||
slog.Int("learning_type", len(params.FilterForm.AvailableLearningTypes)),
|
|
||||||
)
|
|
||||||
|
|
||||||
params = bootstrap.ListCoursesParams{
|
params = bootstrap.ListCoursesParams{
|
||||||
FilterForm: bootstrap.FilterFormParams{
|
FilterForm: bootstrap.FilterFormParams{
|
||||||
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
BreadcrumbsParams: bootstrap.BreadcrumbsParams{
|
||||||
@ -172,8 +174,22 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
|
AvailableCourseThematics: params.FilterForm.AvailableCourseThematics,
|
||||||
},
|
},
|
||||||
Categories: params.Categories,
|
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 {
|
slices.SortFunc(params.Categories, func(lhs, rhs bootstrap.CategoryContainer) int {
|
||||||
if lhs.Count > rhs.Count {
|
if lhs.Count > rhs.Count {
|
||||||
return 1
|
return 1
|
||||||
@ -191,13 +207,15 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
|||||||
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
if handleError(ctx, err, w, c.log, "unable to render list courses") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.SetStatus(codes.Ok, "request completed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
var span trace.Span
|
var span trace.Span
|
||||||
ctx, span = webtracer.Start(ctx, "index")
|
ctx, span = webtracer.Start(ctx, "http_server.index")
|
||||||
defer func() {
|
defer func() {
|
||||||
span.End()
|
span.End()
|
||||||
}()
|
}()
|
||||||
@ -238,13 +256,7 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
|||||||
return in.Name
|
return in.Name
|
||||||
})
|
})
|
||||||
|
|
||||||
namesStr := strings.Join(names, ",")
|
category.ExampleThemes = names
|
||||||
|
|
||||||
category.Description = fmt.Sprintf(
|
|
||||||
"Here you can find courses"+
|
|
||||||
" such as %s",
|
|
||||||
namesStr,
|
|
||||||
)
|
|
||||||
|
|
||||||
params.Categories = append(params.Categories, category)
|
params.Categories = append(params.Categories, category)
|
||||||
}
|
}
|
||||||
@ -265,4 +277,5 @@ func (c courseTemplServer) Index(w http.ResponseWriter, r *http.Request) {
|
|||||||
if handleError(ctx, err, w, c.log, "rendeting template") {
|
if handleError(ctx, err, w, c.log, "rendeting template") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
span.SetStatus(codes.Ok, "request completed")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,10 @@ import (
|
|||||||
"git.loyso.art/frx/kurious/internal/common/errors"
|
"git.loyso.art/frx/kurious/internal/common/errors"
|
||||||
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
"git.loyso.art/frx/kurious/internal/common/xcontext"
|
||||||
"git.loyso.art/frx/kurious/internal/kurious/service"
|
"git.loyso.art/frx/kurious/internal/kurious/service"
|
||||||
"go.opentelemetry.io/otel/codes"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@ -38,8 +38,8 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
|||||||
}
|
}
|
||||||
|
|
||||||
span := trace.SpanFromContext(ctx)
|
span := trace.SpanFromContext(ctx)
|
||||||
span.SetStatus(codes.Error, "error during handling request")
|
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
|
span.SetStatus(codes.Error, "error during handling request")
|
||||||
|
|
||||||
var errorString string
|
var errorString string
|
||||||
var code int
|
var code int
|
||||||
@ -66,10 +66,16 @@ func handleError(ctx context.Context, err error, w http.ResponseWriter, log *slo
|
|||||||
type pagination struct {
|
type pagination struct {
|
||||||
NextPageToken string
|
NextPageToken string
|
||||||
PerPage int
|
PerPage int
|
||||||
|
Page int
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
||||||
query := r.URL.Query()
|
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")
|
out.NextPageToken = query.Get("next")
|
||||||
|
|
||||||
if query.Has("per_page") {
|
if query.Has("per_page") {
|
||||||
@ -80,6 +86,14 @@ func parsePaginationFromQuery(r *http.Request) (out pagination, err error) {
|
|||||||
} else {
|
} else {
|
||||||
out.PerPage = 50
|
out.PerPage = 50
|
||||||
}
|
}
|
||||||
|
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
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,10 +82,11 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
|
|||||||
InsertOrganization: command.NewCreateOrganizationHandler(organizationrepo, log),
|
InsertOrganization: command.NewCreateOrganizationHandler(organizationrepo, log),
|
||||||
},
|
},
|
||||||
Queries: app.Queries{
|
Queries: app.Queries{
|
||||||
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
|
ListCourses: query.NewListCourseHandler(courseadapter, mapper, log),
|
||||||
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
|
ListLearningTypes: query.NewListLearningTypesHandler(courseadapter, mapper, log),
|
||||||
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
|
ListCourseThematics: query.NewListCourseThematicsHandler(courseadapter, mapper, log),
|
||||||
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
|
ListCourseStatistics: query.NewListCoursesStatsHandler(mapper, log),
|
||||||
|
GetCourse: query.NewGetCourseHandler(courseadapter, mapper, log),
|
||||||
|
|
||||||
ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log),
|
ListOrganzations: query.NewListOrganizationsHandler(organizationrepo, log),
|
||||||
GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log),
|
GetOrganization: query.NewGetOrganizationHandler(organizationrepo, log),
|
||||||
|
|||||||
Reference in New Issue
Block a user