feat(tester): integrate pandoc

This commit is contained in:
Vyacheslav1557 2025-03-16 19:16:27 +05:00
parent ffacc9e3ac
commit 94fc50e272
9 changed files with 315 additions and 105 deletions

View file

@ -2,7 +2,7 @@ package config
type Config struct { type Config struct {
Env string `env:"ENV" env-default:"prod"` Env string `env:"ENV" env-default:"prod"`
//Pandoc string `env:"PANDOC" required:"true"` Pandoc string `env:"PANDOC" required:"true"`
Address string `env:"ADDRESS" required:"true"` Address string `env:"ADDRESS" required:"true"`
PostgresDSN string `env:"POSTGRES_DSN" required:"true"` PostgresDSN string `env:"POSTGRES_DSN" required:"true"`
JWTSecret string `env:"JWT_SECRET" required:"true"` JWTSecret string `env:"JWT_SECRET" required:"true"`

View file

@ -5,14 +5,22 @@ import "time"
type Problem struct { type Problem struct {
Id int32 `db:"id"` Id int32 `db:"id"`
Title string `db:"title"` Title string `db:"title"`
TimeLimit int32 `db:"time_limit"`
MemoryLimit int32 `db:"memory_limit"`
Legend string `db:"legend"` Legend string `db:"legend"`
InputFormat string `db:"input_format"` InputFormat string `db:"input_format"`
OutputFormat string `db:"output_format"` OutputFormat string `db:"output_format"`
Notes string `db:"notes"` Notes string `db:"notes"`
Tutorial string `db:"tutorial"` Scoring string `db:"scoring"`
LatexSummary string `db:"latex_summary"` LatexSummary string `db:"latex_summary"`
TimeLimit int32 `db:"time_limit"`
MemoryLimit int32 `db:"memory_limit"` 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"` CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt time.Time `db:"updated_at"`
} }
@ -28,23 +36,34 @@ type ProblemListItem struct {
type ProblemUpdate 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"` Legend *string `db:"legend"`
InputFormat *string `db:"input_format"` InputFormat *string `db:"input_format"`
OutputFormat *string `db:"output_format"` OutputFormat *string `db:"output_format"`
Notes *string `db:"notes"` Notes *string `db:"notes"`
Tutorial *string `db:"tutorial"` Scoring *string `db:"scoring"`
LatexSummary *string `db:"latex_summary"`
MemoryLimit *int32 `db:"memory_limit"` LegendHtml *string `db:"legend_html"`
TimeLimit *int32 `db:"time_limit"` 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 { type ProblemStatement struct {
Title string `db:"title"`
Legend string `db:"legend"` Legend string `db:"legend"`
InputFormat string `db:"input_format"` InputFormat string `db:"input_format"`
OutputFormat string `db:"output_format"` OutputFormat string `db:"output_format"`
Notes string `db:"notes"` Notes string `db:"notes"`
Tutorial string `db:"tutorial"` Scoring string `db:"scoring"`
TimeLimit int32 `db:"time_limit"` }
MemoryLimit int32 `db:"memory_limit"`
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"`
} }

View file

@ -222,14 +222,21 @@ func (h *TesterHandlers) GetProblem(c *fiber.Ctx, id int32) error {
testerv1.GetProblemResponse{Problem: testerv1.Problem{ testerv1.GetProblemResponse{Problem: testerv1.Problem{
Id: problem.Id, Id: problem.Id,
Title: problem.Title, Title: problem.Title,
TimeLimit: problem.TimeLimit,
MemoryLimit: problem.MemoryLimit,
Legend: problem.Legend, Legend: problem.Legend,
InputFormat: problem.InputFormat, InputFormat: problem.InputFormat,
OutputFormat: problem.OutputFormat, OutputFormat: problem.OutputFormat,
Notes: problem.Notes, Notes: problem.Notes,
Tutorial: problem.Tutorial, Scoring: problem.Scoring,
LatexSummary: problem.LatexSummary,
TimeLimit: problem.TimeLimit, LegendHtml: problem.LegendHtml,
MemoryLimit: problem.MemoryLimit, InputFormatHtml: problem.InputFormatHtml,
OutputFormatHtml: problem.OutputFormatHtml,
NotesHtml: problem.NotesHtml,
ScoringHtml: problem.ScoringHtml,
CreatedAt: problem.CreatedAt, CreatedAt: problem.CreatedAt,
UpdatedAt: problem.UpdatedAt, UpdatedAt: problem.UpdatedAt,
}}, }},
@ -275,13 +282,14 @@ func (h *TesterHandlers) UpdateProblem(c *fiber.Ctx, id int32) error {
err = h.problemsUC.UpdateProblem(c.Context(), id, models.ProblemUpdate{ err = h.problemsUC.UpdateProblem(c.Context(), id, models.ProblemUpdate{
Title: req.Title, Title: req.Title,
MemoryLimit: req.MemoryLimit,
TimeLimit: req.TimeLimit,
Legend: req.Legend, Legend: req.Legend,
InputFormat: req.InputFormat, InputFormat: req.InputFormat,
OutputFormat: req.OutputFormat, OutputFormat: req.OutputFormat,
Notes: req.Notes, Notes: req.Notes,
Tutorial: req.Tutorial, Scoring: req.Scoring,
MemoryLimit: req.MemoryLimit,
TimeLimit: req.TimeLimit,
}) })
if err != nil { if err != nil {

View file

@ -115,14 +115,21 @@ func (r *ProblemRepository) ListProblems(ctx context.Context, q tester.Querier,
const ( const (
UpdateProblemQuery = `UPDATE problems UpdateProblemQuery = `UPDATE problems
SET title = COALESCE(?, title), SET title = COALESCE(?, title),
time_limit = COALESCE(?, time_limit),
memory_limit = COALESCE(?, memory_limit),
legend = COALESCE(?, legend), legend = COALESCE(?, legend),
input_format = COALESCE(?, input_format), input_format = COALESCE(?, input_format),
output_format = COALESCE(?, output_format), output_format = COALESCE(?, output_format),
notes = COALESCE(?, notes), notes = COALESCE(?, notes),
tutorial = COALESCE(?, tutorial), scoring = COALESCE(?, scoring),
latex_summary = COALESCE(?, latex_summary),
time_limit = COALESCE(?, time_limit), legend_html = COALESCE(?, legend_html),
memory_limit = COALESCE(?, memory_limit) 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=?` WHERE id=?`
) )
@ -132,14 +139,21 @@ func (r *ProblemRepository) UpdateProblem(ctx context.Context, q tester.Querier,
query := q.Rebind(UpdateProblemQuery) query := q.Rebind(UpdateProblemQuery)
_, err := q.ExecContext(ctx, query, _, err := q.ExecContext(ctx, query,
problem.Title, problem.Title,
problem.TimeLimit,
problem.MemoryLimit,
problem.Legend, problem.Legend,
problem.InputFormat, problem.InputFormat,
problem.OutputFormat, problem.OutputFormat,
problem.Notes, problem.Notes,
problem.Tutorial, problem.Scoring,
problem.LatexSummary,
problem.TimeLimit, problem.LegendHtml,
problem.MemoryLimit, problem.InputFormatHtml,
problem.OutputFormatHtml,
problem.NotesHtml,
problem.ScoringHtml,
id, id,
) )
if err != nil { if err != nil {

View file

@ -3,23 +3,25 @@ package usecase
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models" "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/internal/tester"
"git.sch9.ru/new_gate/ms-tester/pkg" "git.sch9.ru/new_gate/ms-tester/pkg"
"strings"
) )
type ProblemUseCase struct { type ProblemUseCase struct {
problemRepo tester.ProblemPostgresRepository problemRepo tester.ProblemPostgresRepository
//pandocClient pkg.PandocClient pandocClient pkg.PandocClient
} }
func NewProblemUseCase( func NewProblemUseCase(
problemRepo tester.ProblemPostgresRepository, problemRepo tester.ProblemPostgresRepository,
// pandocClient pkg.PandocClient, pandocClient pkg.PandocClient,
) *ProblemUseCase { ) *ProblemUseCase {
return &ProblemUseCase{ return &ProblemUseCase{
problemRepo: problemRepo, problemRepo: problemRepo,
//pandocClient: pandocClient, pandocClient: pandocClient,
} }
} }
@ -45,14 +47,82 @@ func isEmpty(p models.ProblemUpdate) bool {
p.InputFormat == nil && p.InputFormat == nil &&
p.OutputFormat == nil && p.OutputFormat == nil &&
p.Notes == nil && p.Notes == nil &&
p.Tutorial == nil && p.Scoring == nil &&
p.LatexSummary == nil &&
p.MemoryLimit == nil && p.MemoryLimit == nil &&
p.TimeLimit == nil p.TimeLimit == nil
} }
func build(p models.ProblemStatement) string { const heading = `
return "" \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 { 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{ statement := models.ProblemStatement{
Title: problem.Title,
Legend: problem.Legend, Legend: problem.Legend,
InputFormat: problem.InputFormat, InputFormat: problem.InputFormat,
OutputFormat: problem.OutputFormat, OutputFormat: problem.OutputFormat,
Notes: problem.Notes, Notes: problem.Notes,
Tutorial: problem.Tutorial, Scoring: problem.Scoring,
TimeLimit: problem.TimeLimit,
MemoryLimit: problem.MemoryLimit,
} }
if problemUpdate.Title != nil {
statement.Title = *problemUpdate.Title
}
if problemUpdate.Legend != nil { if problemUpdate.Legend != nil {
statement.Legend = *problemUpdate.Legend statement.Legend = *problemUpdate.Legend
} }
@ -96,19 +160,29 @@ func (u *ProblemUseCase) UpdateProblem(ctx context.Context, id int32, problemUpd
if problemUpdate.Notes != nil { if problemUpdate.Notes != nil {
statement.Notes = *problemUpdate.Notes statement.Notes = *problemUpdate.Notes
} }
if problemUpdate.Tutorial != nil { if problemUpdate.Scoring != nil {
statement.Tutorial = *problemUpdate.Tutorial statement.Scoring = *problemUpdate.Scoring
}
if problemUpdate.TimeLimit != nil {
statement.TimeLimit = *problemUpdate.TimeLimit
}
if problemUpdate.MemoryLimit != nil {
statement.MemoryLimit = *problemUpdate.MemoryLimit
} }
builtStatement := build(statement) builtStatement, err := build(ctx, u.pandocClient, trimSpaces(statement))
if builtStatement != problem.LatexSummary { if err != nil {
problemUpdate.LatexSummary = &builtStatement 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) err = u.problemRepo.UpdateProblem(ctx, tx, id, problemUpdate)

View file

@ -12,6 +12,7 @@ import (
fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" fiberlogger "github.com/gofiber/fiber/v2/middleware/logger"
"github.com/ilyakaznacheev/cleanenv" "github.com/ilyakaznacheev/cleanenv"
"go.uber.org/zap" "go.uber.org/zap"
"net/http"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -41,10 +42,10 @@ func main() {
defer db.Close() defer db.Close()
logger.Info("successfully connected to postgres") logger.Info("successfully connected to postgres")
//pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc) pandocClient := pkg.NewPandocClient(&http.Client{}, cfg.Pandoc)
problemRepo := problemsRepository.NewProblemRepository(db) problemRepo := problemsRepository.NewProblemRepository(db)
problemUC := testerUseCase.NewProblemUseCase(problemRepo) problemUC := testerUseCase.NewProblemUseCase(problemRepo, pandocClient)
contestRepo := problemsRepository.NewContestRepository(db) contestRepo := problemsRepository.NewContestRepository(db)
contestUC := testerUseCase.NewContestUseCase(contestRepo) contestUC := testerUseCase.NewContestUseCase(contestRepo)

View file

@ -13,20 +13,28 @@ CREATE TABLE IF NOT EXISTS problems
( (
id serial NOT NULL, id serial NOT NULL,
title varchar(64) 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 '', legend varchar(10240) NOT NULL DEFAULT '',
input_format varchar(10240) NOT NULL DEFAULT '', input_format varchar(10240) NOT NULL DEFAULT '',
output_format varchar(10240) NOT NULL DEFAULT '', output_format varchar(10240) NOT NULL DEFAULT '',
notes varchar(10240) NOT NULL DEFAULT '', notes varchar(10240) NOT NULL DEFAULT '',
tutorial varchar(10240) NOT NULL DEFAULT '', scoring varchar(10240) NOT NULL DEFAULT '',
latex_summary varchar(10240) NOT NULL DEFAULT '',
time_limit integer NOT NULL DEFAULT 1000, legend_html varchar(10240) NOT NULL DEFAULT '',
memory_limit integer NOT NULL DEFAULT 64, 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(), created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (length(title) != 0), CHECK (length(title) != 0),
CHECK (memory_limit BETWEEN 4 and 1024), 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 CREATE TRIGGER on_problems_update

View file

@ -4,8 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url"
) )
type Client struct { type Client struct {
@ -15,6 +18,7 @@ type Client struct {
type PandocClient interface { type PandocClient interface {
ConvertLatexToHtml5(ctx context.Context, text string) (string, error) ConvertLatexToHtml5(ctx context.Context, text string) (string, error)
BatchConvertLatexToHtml5(ctx context.Context, texts []string) ([]string, error)
} }
func NewPandocClient(client *http.Client, address string) *Client { 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"` Text string `json:"text"`
From string `json:"from"` From string `json:"from"`
To string `json:"to"` To string `json:"to"`
Math string `json:"html-math-method"`
} }
func (client *Client) convert(ctx context.Context, text, from, to string) (string, error) { type message struct {
body, err := json.Marshal(convertRequest{ Message string `json:"message"`
Text: text, Verbosity string `json:"verbosity"`
From: from, }
To: to,
}) 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 { if err != nil {
return "", err return nil, err
} }
buf := bytes.NewBuffer(body) buf := bytes.NewBuffer(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, buf)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.address, buf)
if err != nil { if err != nil {
return "", err return nil, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := client.client.Do(req) resp, err := client.client.Do(req)
if err != nil { if err != nil {
return "", err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err = io.ReadAll(resp.Body) 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 { if err != nil {
return "", err 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) { 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")
} }

2
proto

@ -1 +1 @@
Subproject commit d021cf202fde8ba13409b9031d01e8b068b82ca9 Subproject commit fb65ba8ce2220e7e47990f0f52214126839b3d78