diff --git a/config/config.go b/config/config.go index f26eb34..c061e5a 100644 --- a/config/config.go +++ b/config/config.go @@ -1,8 +1,8 @@ package config type Config struct { - Env string `env:"ENV" env-default:"prod"` - //Pandoc string `env:"PANDOC" required:"true"` + Env string `env:"ENV" env-default:"prod"` + Pandoc string `env:"PANDOC" required:"true"` Address string `env:"ADDRESS" required:"true"` PostgresDSN string `env:"POSTGRES_DSN" required:"true"` JWTSecret string `env:"JWT_SECRET" required:"true"` diff --git a/internal/models/problem.go b/internal/models/problem.go index 85a3577..92c1d01 100644 --- a/internal/models/problem.go +++ b/internal/models/problem.go @@ -3,18 +3,26 @@ package models import "time" type Problem struct { - Id int32 `db:"id"` - Title string `db:"title"` - Legend string `db:"legend"` - InputFormat string `db:"input_format"` - OutputFormat string `db:"output_format"` - Notes string `db:"notes"` - Tutorial string `db:"tutorial"` - LatexSummary string `db:"latex_summary"` - TimeLimit int32 `db:"time_limit"` - MemoryLimit int32 `db:"memory_limit"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + Id int32 `db:"id"` + Title string `db:"title"` + TimeLimit int32 `db:"time_limit"` + MemoryLimit int32 `db:"memory_limit"` + + Legend string `db:"legend"` + InputFormat string `db:"input_format"` + OutputFormat string `db:"output_format"` + Notes string `db:"notes"` + Scoring string `db:"scoring"` + LatexSummary string `db:"latex_summary"` + + LegendHtml string `db:"legend_html"` + InputFormatHtml string `db:"input_format_html"` + OutputFormatHtml string `db:"output_format_html"` + NotesHtml string `db:"notes_html"` + ScoringHtml string `db:"scoring_html"` + + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type ProblemListItem struct { @@ -27,24 +35,35 @@ type ProblemListItem struct { } type ProblemUpdate struct { - Title *string `db:"title"` + Title *string `db:"title"` + MemoryLimit *int32 `db:"memory_limit"` + TimeLimit *int32 `db:"time_limit"` + Legend *string `db:"legend"` InputFormat *string `db:"input_format"` OutputFormat *string `db:"output_format"` Notes *string `db:"notes"` - Tutorial *string `db:"tutorial"` - LatexSummary *string `db:"latex_summary"` - MemoryLimit *int32 `db:"memory_limit"` - TimeLimit *int32 `db:"time_limit"` + Scoring *string `db:"scoring"` + + LegendHtml *string `db:"legend_html"` + InputFormatHtml *string `db:"input_format_html"` + OutputFormatHtml *string `db:"output_format_html"` + NotesHtml *string `db:"notes_html"` + ScoringHtml *string `db:"scoring_html"` } type ProblemStatement struct { - Title string `db:"title"` Legend string `db:"legend"` InputFormat string `db:"input_format"` OutputFormat string `db:"output_format"` Notes string `db:"notes"` - Tutorial string `db:"tutorial"` - TimeLimit int32 `db:"time_limit"` - MemoryLimit int32 `db:"memory_limit"` + Scoring string `db:"scoring"` +} + +type Html5ProblemStatement struct { + LegendHtml string `db:"legend_html"` + InputFormatHtml string `db:"input_format_html"` + OutputFormatHtml string `db:"output_format_html"` + NotesHtml string `db:"notes_html"` + ScoringHtml string `db:"scoring_html"` } diff --git a/internal/tester/delivery/rest/handlers.go b/internal/tester/delivery/rest/handlers.go index 1bbcdbc..f9a8487 100644 --- a/internal/tester/delivery/rest/handlers.go +++ b/internal/tester/delivery/rest/handlers.go @@ -220,18 +220,25 @@ func (h *TesterHandlers) GetProblem(c *fiber.Ctx, id int32) error { return c.JSON( testerv1.GetProblemResponse{Problem: testerv1.Problem{ - Id: problem.Id, - Title: problem.Title, + Id: problem.Id, + Title: problem.Title, + TimeLimit: problem.TimeLimit, + MemoryLimit: problem.MemoryLimit, + Legend: problem.Legend, InputFormat: problem.InputFormat, OutputFormat: problem.OutputFormat, Notes: problem.Notes, - Tutorial: problem.Tutorial, - LatexSummary: problem.LatexSummary, - TimeLimit: problem.TimeLimit, - MemoryLimit: problem.MemoryLimit, - CreatedAt: problem.CreatedAt, - UpdatedAt: problem.UpdatedAt, + Scoring: problem.Scoring, + + LegendHtml: problem.LegendHtml, + InputFormatHtml: problem.InputFormatHtml, + OutputFormatHtml: problem.OutputFormatHtml, + NotesHtml: problem.NotesHtml, + ScoringHtml: problem.ScoringHtml, + + CreatedAt: problem.CreatedAt, + UpdatedAt: problem.UpdatedAt, }}, ) } @@ -274,14 +281,15 @@ func (h *TesterHandlers) UpdateProblem(c *fiber.Ctx, id int32) error { } err = h.problemsUC.UpdateProblem(c.Context(), id, models.ProblemUpdate{ - Title: req.Title, + Title: req.Title, + MemoryLimit: req.MemoryLimit, + TimeLimit: req.TimeLimit, + Legend: req.Legend, InputFormat: req.InputFormat, OutputFormat: req.OutputFormat, Notes: req.Notes, - Tutorial: req.Tutorial, - MemoryLimit: req.MemoryLimit, - TimeLimit: req.TimeLimit, + Scoring: req.Scoring, }) if err != nil { diff --git a/internal/tester/repository/pg_problems_repository.go b/internal/tester/repository/pg_problems_repository.go index 57045f5..4fff46e 100644 --- a/internal/tester/repository/pg_problems_repository.go +++ b/internal/tester/repository/pg_problems_repository.go @@ -114,15 +114,22 @@ func (r *ProblemRepository) ListProblems(ctx context.Context, q tester.Querier, const ( UpdateProblemQuery = `UPDATE problems -SET title = COALESCE(?, title), - legend = COALESCE(?, legend), - input_format = COALESCE(?, input_format), - output_format = COALESCE(?, output_format), - notes = COALESCE(?, notes), - tutorial = COALESCE(?, tutorial), - latex_summary = COALESCE(?, latex_summary), - time_limit = COALESCE(?, time_limit), - memory_limit = COALESCE(?, memory_limit) +SET title = COALESCE(?, title), + time_limit = COALESCE(?, time_limit), + memory_limit = COALESCE(?, memory_limit), + + legend = COALESCE(?, legend), + input_format = COALESCE(?, input_format), + output_format = COALESCE(?, output_format), + notes = COALESCE(?, notes), + scoring = COALESCE(?, scoring), + + legend_html = COALESCE(?, legend_html), + input_format_html = COALESCE(?, input_format_html), + output_format_html = COALESCE(?, output_format_html), + notes_html = COALESCE(?, notes_html), + scoring_html = COALESCE(?, scoring_html) + WHERE id=?` ) @@ -132,14 +139,21 @@ func (r *ProblemRepository) UpdateProblem(ctx context.Context, q tester.Querier, query := q.Rebind(UpdateProblemQuery) _, err := q.ExecContext(ctx, query, problem.Title, + problem.TimeLimit, + problem.MemoryLimit, + problem.Legend, problem.InputFormat, problem.OutputFormat, problem.Notes, - problem.Tutorial, - problem.LatexSummary, - problem.TimeLimit, - problem.MemoryLimit, + problem.Scoring, + + problem.LegendHtml, + problem.InputFormatHtml, + problem.OutputFormatHtml, + problem.NotesHtml, + problem.ScoringHtml, + id, ) if err != nil { diff --git a/internal/tester/usecase/problems_usecase.go b/internal/tester/usecase/problems_usecase.go index 4671152..d4eecb0 100644 --- a/internal/tester/usecase/problems_usecase.go +++ b/internal/tester/usecase/problems_usecase.go @@ -3,23 +3,25 @@ package usecase import ( "context" "errors" + "fmt" "git.sch9.ru/new_gate/ms-tester/internal/models" "git.sch9.ru/new_gate/ms-tester/internal/tester" "git.sch9.ru/new_gate/ms-tester/pkg" + "strings" ) type ProblemUseCase struct { - problemRepo tester.ProblemPostgresRepository - //pandocClient pkg.PandocClient + problemRepo tester.ProblemPostgresRepository + pandocClient pkg.PandocClient } func NewProblemUseCase( problemRepo tester.ProblemPostgresRepository, - // pandocClient pkg.PandocClient, + pandocClient pkg.PandocClient, ) *ProblemUseCase { return &ProblemUseCase{ - problemRepo: problemRepo, - //pandocClient: pandocClient, + problemRepo: problemRepo, + pandocClient: pandocClient, } } @@ -45,14 +47,82 @@ func isEmpty(p models.ProblemUpdate) bool { p.InputFormat == nil && p.OutputFormat == nil && p.Notes == nil && - p.Tutorial == nil && - p.LatexSummary == nil && + p.Scoring == nil && p.MemoryLimit == nil && p.TimeLimit == nil } -func build(p models.ProblemStatement) string { - return "" +const heading = ` +\newcommand{\InputFile}{\subsection*{Входные данные}} +\newcommand{\OutputFile}{\subsection*{Выходные данные}} +\newcommand{\Scoring}{\subsection*{Система оценки}} +\newcommand{\Note}{\subsection*{Примечание}} +\newcommand{\Examples}{\subsection*{Примеры}} +` + +func wrap(s string) string { + return fmt.Sprintf("%s\n\\begin{document}\n%s\n\\end{document}\n", heading, s) +} + +func trimSpaces(statement models.ProblemStatement) models.ProblemStatement { + return models.ProblemStatement{ + Legend: strings.TrimSpace(statement.Legend), + InputFormat: strings.TrimSpace(statement.InputFormat), + OutputFormat: strings.TrimSpace(statement.OutputFormat), + Notes: strings.TrimSpace(statement.Notes), + Scoring: strings.TrimSpace(statement.Scoring), + } +} + +func build(ctx context.Context, pandocClient pkg.PandocClient, p models.ProblemStatement) (models.Html5ProblemStatement, error) { + p = trimSpaces(p) + + latex := models.ProblemStatement{} + + if p.Legend != "" { + latex.Legend = wrap(fmt.Sprintf("\\InputFile\n%s\n", p.Legend)) + } + + if p.InputFormat != "" { + latex.InputFormat = wrap(fmt.Sprintf("\\InputFile\n%s\n", p.InputFormat)) + } + + if p.OutputFormat != "" { + latex.OutputFormat = wrap(fmt.Sprintf("\\OutputFile\n%s\n", p.OutputFormat)) + } + + if p.Notes != "" { + latex.Notes = wrap(fmt.Sprintf("\\Note\n%s\n", p.Notes)) + } + + if p.Scoring != "" { + latex.Scoring = wrap(fmt.Sprintf("\\Scoring\n%s\n", p.Scoring)) + } + + req := []string{ + latex.Legend, + latex.InputFormat, + latex.OutputFormat, + latex.Notes, + latex.Scoring, + } + + res, err := pandocClient.BatchConvertLatexToHtml5(ctx, req) + if err != nil { + return models.Html5ProblemStatement{}, err + } + + if len(res) != len(req) { + return models.Html5ProblemStatement{}, fmt.Errorf("wrong number of fieilds returned: %d", len(res)) + } + + return models.Html5ProblemStatement{ + LegendHtml: res[0], + InputFormatHtml: res[1], + OutputFormatHtml: res[2], + NotesHtml: res[3], + ScoringHtml: res[4], + }, nil } func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpdate models.ProblemUpdate) error { @@ -71,19 +141,13 @@ func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpd } statement := models.ProblemStatement{ - Title: problem.Title, Legend: problem.Legend, InputFormat: problem.InputFormat, OutputFormat: problem.OutputFormat, Notes: problem.Notes, - Tutorial: problem.Tutorial, - TimeLimit: problem.TimeLimit, - MemoryLimit: problem.MemoryLimit, + Scoring: problem.Scoring, } - if problemUpdate.Title != nil { - statement.Title = *problemUpdate.Title - } if problemUpdate.Legend != nil { statement.Legend = *problemUpdate.Legend } @@ -96,19 +160,29 @@ func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpd if problemUpdate.Notes != nil { statement.Notes = *problemUpdate.Notes } - if problemUpdate.Tutorial != nil { - statement.Tutorial = *problemUpdate.Tutorial - } - if problemUpdate.TimeLimit != nil { - statement.TimeLimit = *problemUpdate.TimeLimit - } - if problemUpdate.MemoryLimit != nil { - statement.MemoryLimit = *problemUpdate.MemoryLimit + if problemUpdate.Scoring != nil { + statement.Scoring = *problemUpdate.Scoring } - builtStatement := build(statement) - if builtStatement != problem.LatexSummary { - problemUpdate.LatexSummary = &builtStatement + builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement)) + if err != nil { + return errors.Join(err, tx.Rollback()) + } + + if builtStatement.LegendHtml != problem.LegendHtml { + problemUpdate.LegendHtml = &builtStatement.LegendHtml + } + if builtStatement.InputFormatHtml != problem.InputFormatHtml { + problemUpdate.InputFormatHtml = &builtStatement.InputFormatHtml + } + if builtStatement.OutputFormatHtml != problem.OutputFormatHtml { + problemUpdate.OutputFormatHtml = &builtStatement.OutputFormatHtml + } + if builtStatement.NotesHtml != problem.NotesHtml { + problemUpdate.NotesHtml = &builtStatement.NotesHtml + } + if builtStatement.ScoringHtml != problem.ScoringHtml { + problemUpdate.ScoringHtml = &builtStatement.ScoringHtml } err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate) diff --git a/main.go b/main.go index e28941b..cfb5686 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" "github.com/ilyakaznacheev/cleanenv" "go.uber.org/zap" + "net/http" "os" "os/signal" "syscall" @@ -41,10 +42,10 @@ func main() { defer db.Close() logger.Info("successfully connected to postgres") - //pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc) + pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc) problemRepo := problemsRepository.NewProblemRepository(db) - problemUC := testerUseCase.NewProblemUseCase(problemRepo) + problemUC := testerUseCase.NewProblemUseCase(problemRepo, pandocClient) contestRepo := problemsRepository.NewContestRepository(db) contestUC := testerUseCase.NewContestUseCase(contestRepo) diff --git a/migrations/20240727123308_initial.sql b/migrations/20240727123308_initial.sql index d8058da..e5723b7 100644 --- a/migrations/20240727123308_initial.sql +++ b/migrations/20240727123308_initial.sql @@ -11,22 +11,30 @@ $$; CREATE TABLE IF NOT EXISTS problems ( - id serial NOT NULL, - title varchar(64) NOT NULL, - legend varchar(10240) NOT NULL DEFAULT '', - input_format varchar(10240) NOT NULL DEFAULT '', - output_format varchar(10240) NOT NULL DEFAULT '', - notes varchar(10240) NOT NULL DEFAULT '', - tutorial varchar(10240) NOT NULL DEFAULT '', - latex_summary varchar(10240) NOT NULL DEFAULT '', - time_limit integer NOT NULL DEFAULT 1000, - memory_limit integer NOT NULL DEFAULT 64, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), + id serial NOT NULL, + title varchar(64) NOT NULL, + time_limit integer NOT NULL DEFAULT 1000, + memory_limit integer NOT NULL DEFAULT 64, + + legend varchar(10240) NOT NULL DEFAULT '', + input_format varchar(10240) NOT NULL DEFAULT '', + output_format varchar(10240) NOT NULL DEFAULT '', + notes varchar(10240) NOT NULL DEFAULT '', + scoring varchar(10240) NOT NULL DEFAULT '', + + legend_html varchar(10240) NOT NULL DEFAULT '', + input_format_html varchar(10240) NOT NULL DEFAULT '', + output_format_html varchar(10240) NOT NULL DEFAULT '', + notes_html varchar(10240) NOT NULL DEFAULT '', + scoring_html varchar(10240) NOT NULL DEFAULT '', + + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (id), CHECK (length(title) != 0), CHECK (memory_limit BETWEEN 4 and 1024), - CHECK (time_limit BETWEEN 250 and 15000) + CHECK (time_limit BETWEEN 250 and 5000) ); CREATE TRIGGER on_problems_update diff --git a/pkg/pandoc-client.go b/pkg/pandoc-client.go index 903dfff..a5f088e 100644 --- a/pkg/pandoc-client.go +++ b/pkg/pandoc-client.go @@ -4,8 +4,11 @@ import ( "bytes" "context" "encoding/json" + "errors" + "fmt" "io" "net/http" + "net/url" ) type Client struct { @@ -15,6 +18,7 @@ type Client struct { type PandocClient interface { ConvertLatexToHtml5(ctx context.Context, text string) (string, error) + BatchConvertLatexToHtml5(ctx context.Context, texts []string) ([]string, error) } func NewPandocClient(client *http.Client, address string) *Client { @@ -24,44 +28,126 @@ func NewPandocClient(client *http.Client, address string) *Client { } } -type convertRequest struct { +type conversation struct { Text string `json:"text"` From string `json:"from"` To string `json:"to"` + Math string `json:"html-math-method"` } -func (client *Client) convert(ctx context.Context, text, from, to string) (string, error) { - body, err := json.Marshal(convertRequest{ - Text: text, - From: from, - To: to, - }) +type message struct { + Message string `json:"message"` + Verbosity string `json:"verbosity"` +} + +type output struct { + Error string `json:"error"` + Output string `json:"output"` + Base64 bool `json:"base64"` + Messages []message `json:"messages"` +} + +func (client *Client) sendRaw(ctx context.Context, path string, body []byte) ([]byte, error) { + path, err := url.JoinPath(client.address, path) if err != nil { - return "", err + return nil, err } buf := bytes.NewBuffer(body) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.address, buf) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, buf) if err != nil { - return "", err + return nil, err } req.Header.Set("Content-Type", "application/json") + resp, err := client.client.Do(req) if err != nil { - return "", err + return nil, err } defer resp.Body.Close() body, err = io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} + +func (client *Client) convert(ctx context.Context, text, from, to, math string) (string, error) { + body, err := json.Marshal(conversation{ + Text: text, + From: from, + To: to, + Math: math, + }) if err != nil { return "", err } - return string(body), nil + resp, err := client.sendRaw(ctx, "/", body) + if err != nil { + return "", err + } + + return string(resp), nil +} + +func (client *Client) batchConvert(ctx context.Context, texts []string, from, to, math string) ([]string, error) { + list := make([]conversation, len(texts)) + for i, text := range texts { + list[i] = conversation{ + Text: text, + From: from, + To: to, + Math: math, + } + } + + body, err := json.Marshal(list) + if err != nil { + return nil, err + } + + resp, err := client.sendRaw(ctx, "/batch", body) + if err != nil { + return nil, err + } + + var result []output + err = json.Unmarshal(resp, &result) + if err != nil { + return nil, err + } + + if len(result) != len(texts) { + return nil, fmt.Errorf("wrong number of fieilds returned: %d", len(result)) + } + + err = nil + for _, o := range result { + if o.Error != "" { + err = errors.Join(err, errors.New(o.Error)) + } + } + + if err != nil { + return nil, Wrap(ErrBadInput, err, "BatchConvertLatexToHtml5", "invalid input") + } + + res := make([]string, len(result)) + for i, o := range result { + res[i] = o.Output + } + + return res, nil } func (client *Client) ConvertLatexToHtml5(ctx context.Context, text string) (string, error) { - return client.convert(ctx, text, "latex", "html5") + return client.convert(ctx, text, "latex", "html5", "katex") +} + +func (client *Client) BatchConvertLatexToHtml5(ctx context.Context, texts []string) ([]string, error) { + return client.batchConvert(ctx, texts, "latex", "html5", "katex") } diff --git a/proto b/proto index d021cf2..fb65ba8 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit d021cf202fde8ba13409b9031d01e8b068b82ca9 +Subproject commit fb65ba8ce2220e7e47990f0f52214126839b3d78