add sqlite support
This commit is contained in:
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"`
|
||||
}
|
||||
@ -1,21 +1,73 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
)
|
||||
|
||||
type inMemoryMapper struct {
|
||||
courseThematicsByID map[string]string
|
||||
learningTypeByID map[string]string
|
||||
|
||||
courseThematicsCountByID map[string]int
|
||||
learningTypeCountByID map[string]int
|
||||
totalCount int
|
||||
}
|
||||
|
||||
func NewMemoryMapper(courseThematics, learningType map[string]string) inMemoryMapper {
|
||||
return inMemoryMapper{
|
||||
func NewMemoryMapper(courseThematics, learningType map[string]string) *inMemoryMapper {
|
||||
return &inMemoryMapper{
|
||||
courseThematicsByID: courseThematics,
|
||||
learningTypeByID: learningType,
|
||||
}
|
||||
}
|
||||
|
||||
func (m inMemoryMapper) CourseThematicNameByID(id string) string {
|
||||
func (m *inMemoryMapper) CollectCounts(ctx context.Context, cr domain.CourseRepository) error {
|
||||
const batchSize = 1000
|
||||
|
||||
m.courseThematicsCountByID = map[string]int{}
|
||||
m.learningTypeCountByID = map[string]int{}
|
||||
|
||||
var nextPageToken string
|
||||
for {
|
||||
result, err := cr.List(ctx, domain.ListCoursesParams{
|
||||
LearningType: "",
|
||||
CourseThematic: "",
|
||||
NextPageToken: nextPageToken,
|
||||
Limit: batchSize,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing courses: %w", err)
|
||||
}
|
||||
m.totalCount += len(result.Courses)
|
||||
for _, course := range result.Courses {
|
||||
m.courseThematicsCountByID[course.ThematicID]++
|
||||
m.learningTypeCountByID[course.LearningTypeID]++
|
||||
}
|
||||
if len(result.Courses) < batchSize {
|
||||
break
|
||||
}
|
||||
nextPageToken = result.NextPageToken
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) GetCounts(byCourseThematic, byLearningType string) int {
|
||||
if byCourseThematic != "" {
|
||||
return m.courseThematicsCountByID[byCourseThematic]
|
||||
} else if byLearningType != "" {
|
||||
return m.learningTypeCountByID[byLearningType]
|
||||
} else {
|
||||
return m.totalCount
|
||||
}
|
||||
}
|
||||
|
||||
func (m *inMemoryMapper) CourseThematicNameByID(id string) string {
|
||||
return m.courseThematicsByID[id]
|
||||
}
|
||||
|
||||
func (m inMemoryMapper) LearningTypeNameByID(id string) string {
|
||||
func (m *inMemoryMapper) LearningTypeNameByID(id string) string {
|
||||
return m.learningTypeByID[id]
|
||||
}
|
||||
|
||||
377
internal/kurious/adapters/sqlite_course_repository.go
Normal file
377
internal/kurious/adapters/sqlite_course_repository.go
Normal file
@ -0,0 +1,377 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/migrations/sqlite"
|
||||
"git.loyso.art/frx/kurious/pkg/xdefault"
|
||||
|
||||
"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 (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`
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
||||
args := make([]any, 0, 1)
|
||||
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.NextPageToken != "" {
|
||||
args = append(args, params.NextPageToken)
|
||||
query += " AND id > ?"
|
||||
}
|
||||
|
||||
query += " ORDER BY id ASC"
|
||||
|
||||
if params.Limit > 0 {
|
||||
query += " LIMIT ?"
|
||||
args = append(args, params.Limit)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *sqliteCourseRepository) ListLearningTypes(
|
||||
ctx context.Context,
|
||||
) (result domain.ListLearningTypeResult, err error) {
|
||||
const query = "SELECT DISTINCT learning_type FROM courses"
|
||||
|
||||
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 = ?"
|
||||
}
|
||||
|
||||
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) Get(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (course domain.Course, err error) {
|
||||
const queryTemplate = `SELECT %s FROM courses WHERE id = ?`
|
||||
|
||||
query := fmt.Sprintf(queryTemplate, coursesFieldsStr)
|
||||
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")
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
119
internal/kurious/adapters/sqlite_course_repository_test.go
Normal file
119
internal/kurious/adapters/sqlite_course_repository_test.go
Normal file
@ -0,0 +1,119 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.loyso.art/frx/kurious/internal/common/config"
|
||||
"git.loyso.art/frx/kurious/internal/common/nullable"
|
||||
"git.loyso.art/frx/kurious/internal/kurious/domain"
|
||||
"git.loyso.art/frx/kurious/migrations/sqlite"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestSqliteCourseRepository(t *testing.T) {
|
||||
suite.Run(t, new(sqliteCourseRepositorySuite))
|
||||
}
|
||||
|
||||
type sqliteCourseRepositorySuite 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 *sqliteCourseRepositorySuite) 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 *sqliteCourseRepositorySuite) TearDownSuite() {
|
||||
s.cancel()
|
||||
err := s.connection.Close()
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
|
||||
func (s *sqliteCourseRepositorySuite) TearDownTest() {
|
||||
db := s.connection.db
|
||||
_, err := db.ExecContext(s.ctx, "DELETE FROM courses")
|
||||
s.Require().NoError(err, "cleaning up database")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -41,6 +41,7 @@ func NewListCourseHandler(
|
||||
}
|
||||
|
||||
func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out domain.ListCoursesResult, err error) {
|
||||
out.AvailableCoursesOfSub = map[string]int{}
|
||||
out.NextPageToken = query.NextPageToken
|
||||
drainFull := query.Limit == 0
|
||||
if !drainFull {
|
||||
@ -76,5 +77,11 @@ func (h listCourseHandler) Handle(ctx context.Context, query ListCourse) (out do
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type CourseMapper interface {
|
||||
CourseThematicNameByID(string) string
|
||||
LearningTypeNameByID(string) string
|
||||
|
||||
CollectCounts(context.Context, CourseRepository) error
|
||||
GetCounts(byCourseThematic, byLearningType string) int
|
||||
}
|
||||
|
||||
@ -35,8 +35,9 @@ type CreateCourseParams struct {
|
||||
}
|
||||
|
||||
type ListCoursesResult struct {
|
||||
Courses []Course
|
||||
NextPageToken string
|
||||
Courses []Course
|
||||
AvailableCoursesOfSub map[string]int
|
||||
NextPageToken string
|
||||
}
|
||||
|
||||
type ListLearningTypeResult struct {
|
||||
|
||||
@ -11,6 +11,10 @@ templ head(title string) {
|
||||
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"
|
||||
|
||||
@ -36,7 +36,7 @@ func head(title string) templ.Component {
|
||||
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\"><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL\" crossorigin=\"anonymous\">")
|
||||
_, 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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
id="learning-type-filter"
|
||||
class={"form-select"}
|
||||
>
|
||||
<option selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||
for _, learningType := range params.AvailableLearningTypes {
|
||||
<option
|
||||
selected?={params.ActiveLearningType.ID==learningType.ID}
|
||||
@ -102,7 +102,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
id="course-thematic-filter"
|
||||
class={"form-select", templ.KV("d-none", len(params.AvailableCourseThematics) == 0)}
|
||||
>
|
||||
<option selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||
<option value="" selected?={params.ActiveLearningType.ID==""}>All</option>
|
||||
for _, courseThematic := range params.AvailableCourseThematics {
|
||||
<option
|
||||
selected?={params.ActiveCourseThematic.ID==courseThematic.ID}
|
||||
@ -110,7 +110,7 @@ templ listCoursesSectionFilters(params FilterFormParams) {
|
||||
>{courseThematic.Name}</option>
|
||||
}
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button">Go</button>
|
||||
<button id="filter-course-thematic" class="btn btn-outline-secondary" type="submit">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@ -120,8 +120,6 @@ templ listCoursesLearning(containers []CategoryContainer) {
|
||||
for _, container := range containers {
|
||||
<section class="row first-class-group">
|
||||
<h1 class="title">{container.Name}</h1>
|
||||
<p>{container.Description}</p>
|
||||
<div class="block">Placeholder for filter</div>
|
||||
|
||||
for _, subcategory := range container.Subcategories {
|
||||
@listCoursesThematicRow(subcategory)
|
||||
@ -134,9 +132,9 @@ templ listCoursesLearning(containers []CategoryContainer) {
|
||||
templ listCoursesThematicRow(subcategory SubcategoryContainer) {
|
||||
<div class="block second-class-group">
|
||||
<h2 class="title">{subcategory.Name}</h2>
|
||||
<p>{subcategory.Description}</p>
|
||||
<p>В категогрии {subcategory.Name} собраны {strconv.Itoa(subcategory.Count)} курсов. Раз в неделю мы обновляем информацию о всех курсах.</p>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-4">
|
||||
for _, info := range subcategory.Courses {
|
||||
@listCoursesCard(info)
|
||||
}
|
||||
@ -151,16 +149,17 @@ css myImg() {
|
||||
|
||||
|
||||
css cardTextSize() {
|
||||
min-height: 16rem;
|
||||
min-height: 12rem;
|
||||
}
|
||||
|
||||
templ listCoursesCard(info CourseInfo) {
|
||||
<div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<img src={ GetOrFallback(info.ImageLink, "https://placehold.co/128x128")} alt="Course picture" class={"card-img-top", myImg()}/>
|
||||
<div class={"card-body", cardTextSize()}>
|
||||
// <div class="col-12 col-md-6 col-lg-3">
|
||||
<div class="col">
|
||||
<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">
|
||||
<div class="input-group d-flex align-self-end">
|
||||
<a
|
||||
href={ templ.URL(info.OriginLink) }
|
||||
class="btn text btn-outline-primary flex-grow-1"
|
||||
|
||||
@ -273,7 +273,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -350,7 +350,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><option value=\"\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -410,7 +410,7 @@ func listCoursesSectionFilters(params FilterFormParams) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <button class=\"btn btn-outline-secondary\" type=\"button\">")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select> <button id=\"filter-course-thematic\" class=\"btn btn-outline-secondary\" type=\"submit\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -457,29 +457,7 @@ func listCoursesLearning(containers []CategoryContainer) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(container.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 122, Col: 28}
|
||||
}
|
||||
_, 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("</p><div class=\"block\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var21 := `Placeholder for filter`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -509,21 +487,21 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
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
|
||||
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var20 == nil {
|
||||
templ_7745c5c3_Var20 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"block second-class-group\"><h2 class=\"title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Name)
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, 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: 135, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 133, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -531,16 +509,48 @@ func listCoursesThematicRow(subcategory SubcategoryContainer) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(subcategory.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 136, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
templ_7745c5c3_Var22 := `В категогрии `
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p><div class=\"row g-4\">")
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, 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: 134, Col: 46}
|
||||
}
|
||||
_, 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(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var24 := `собраны `
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, 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: 134, Col: 95}
|
||||
}
|
||||
_, 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(" ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var26 := `курсов. Раз в неделю мы обновляем информацию о всех курсах.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
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
|
||||
}
|
||||
@ -574,7 +584,7 @@ func myImg() templ.CSSClass {
|
||||
|
||||
func cardTextSize() templ.CSSClass {
|
||||
var templ_7745c5c3_CSSBuilder strings.Builder
|
||||
templ_7745c5c3_CSSBuilder.WriteString(`min-height:16rem;`)
|
||||
templ_7745c5c3_CSSBuilder.WriteString(`min-height:12rem;`)
|
||||
templ_7745c5c3_CSSID := templ.CSSID(`cardTextSize`, templ_7745c5c3_CSSBuilder.String())
|
||||
return templ.ComponentCSSClass{
|
||||
ID: templ_7745c5c3_CSSID,
|
||||
@ -590,17 +600,17 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
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=\"col-12 col-md-6 col-lg-3\"><div class=\"card\">")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col\"><div class=\"card h-100\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 = []any{"card-img-top", myImg()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var26...)
|
||||
var templ_7745c5c3_Var28 = []any{"card-img-top"}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -616,7 +626,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var26).String()))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var28).String()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -624,8 +634,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 = []any{"card-body", cardTextSize()}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...)
|
||||
var templ_7745c5c3_Var29 = []any{"card-body", cardTextSize(), "row"}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -633,7 +643,7 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var27).String()))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var29).String()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -641,21 +651,21 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(info.Name)
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, 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: 161, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 160, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h5><div class=\"input-group d-flex\"><a href=\"")
|
||||
_, 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_Var29 templ.SafeURL = templ.URL(info.OriginLink)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var29)))
|
||||
var templ_7745c5c3_Var31 templ.SafeURL = templ.URL(info.OriginLink)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var31)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -663,8 +673,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var30 := `Go!`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
templ_7745c5c3_Var32 := `Go!`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -672,12 +682,12 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(info.FullPrice))
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, 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: 168, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/kurious/ports/http/bootstrap/list.templ`, Line: 167, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -685,8 +695,8 @@ func listCoursesCard(info CourseInfo) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var32 := `rub.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||
templ_7745c5c3_Var34 := `rub.`
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@ -709,12 +719,12 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
|
||||
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var33 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var33 == nil {
|
||||
templ_7745c5c3_Var33 = templ.NopComponent
|
||||
templ_7745c5c3_Var35 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var35 == nil {
|
||||
templ_7745c5c3_Var35 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var34 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_Var36 := 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()
|
||||
@ -745,7 +755,7 @@ func ListCourses(pageType PageKind, s stats, params ListCoursesParams) templ.Com
|
||||
}
|
||||
return templ_7745c5c3_Err
|
||||
})
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var34), templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = root(pageType, s).Render(templ.WithChildren(ctx, templ_7745c5c3_Var36), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@ -78,6 +78,7 @@ type CategoryBaseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
}
|
||||
|
||||
type CategoryContainer struct {
|
||||
@ -94,7 +95,6 @@ type SubcategoryContainer struct {
|
||||
|
||||
type ListCoursesParams struct {
|
||||
FilterForm FilterFormParams
|
||||
|
||||
Categories []CategoryContainer
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ type courseTemplServer struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func makeTemplListCoursesParams(in ...domain.Course) xtempl.ListCoursesParams {
|
||||
func makeTemplListCoursesParams(counts map[string]int, in ...domain.Course) xtempl.ListCoursesParams {
|
||||
coursesBySubcategory := make(map[string][]xtempl.CourseInfo, len(in))
|
||||
subcategoriesByCategories := make(map[string]map[string]struct{}, len(in))
|
||||
categoryByID := make(map[string]xtempl.CategoryBaseInfo, len(in))
|
||||
@ -46,8 +46,9 @@ func makeTemplListCoursesParams(in ...domain.Course) xtempl.ListCoursesParams {
|
||||
}
|
||||
if _, ok := categoryByID[c.ThematicID]; !ok {
|
||||
categoryByID[c.ThematicID] = xtempl.CategoryBaseInfo{
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
ID: c.ThematicID,
|
||||
Name: c.Thematic,
|
||||
Count: counts[c.ThematicID],
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -95,7 +96,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
params := makeTemplListCoursesParams(listCoursesResult.Courses...)
|
||||
params := makeTemplListCoursesParams(listCoursesResult.AvailableCoursesOfSub, listCoursesResult.Courses...)
|
||||
|
||||
learningTypeResult, err := c.app.Queries.ListLearningTypes.Handle(ctx, query.ListLearningTypes{})
|
||||
if handleError(ctx, err, w, c.log, "unable to list learning types") {
|
||||
@ -118,7 +119,7 @@ func (c courseTemplServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
courseThematicsResult, err := c.app.Queries.ListCourseThematics.Handle(ctx, query.ListCourseThematics{
|
||||
LearningTypeID: pathParams.learningType,
|
||||
})
|
||||
if handleError(ctx, err, w, c.log, "unab;e to list course thematics") {
|
||||
if handleError(ctx, err, w, c.log, "unable to list course thematics") {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@ type CategoryBaseInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Count int
|
||||
}
|
||||
|
||||
type CategoryContainer struct {
|
||||
|
||||
@ -2,11 +2,13 @@ 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"
|
||||
@ -14,9 +16,19 @@ import (
|
||||
"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 {
|
||||
@ -26,14 +38,36 @@ type Application struct {
|
||||
closers []io.Closer
|
||||
}
|
||||
|
||||
func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.CourseMapper) (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
|
||||
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()
|
||||
repoCloser = sqliteConnection
|
||||
case RepositoryEngineYDB:
|
||||
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)
|
||||
}
|
||||
|
||||
courseadapter = ydbConnection.CourseRepository()
|
||||
repoCloser = ydbConnection
|
||||
default:
|
||||
return Application{}, errors.New("unable to decide which 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{
|
||||
@ -50,8 +84,8 @@ func NewApplication(ctx context.Context, cfg ApplicationConfig, mapper domain.Co
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user