feat: support pagination on search, close #4

This commit is contained in:
Paul Pan 2023-12-27 22:17:08 +08:00
parent e508c6f4e8
commit 297d031e5b
Signed by: Paul
GPG Key ID: D639BDF5BA578AF4
8 changed files with 169 additions and 35 deletions

View File

@ -44,7 +44,7 @@ func (h *handler) CreateVersion(c *gin.Context) {
} }
// make sure problem exists // make sure problem exists
_, status := h.problemService.Query(req.ProblemID, false, false) _, status := h.problemService.Query(&problem.QueryData{ID: req.ProblemID, Associations: false, ShouldEnable: false})
if status != e.Success { if status != e.Success {
e.Pong[any](c, status, nil) e.Pong[any](c, status, nil)
return return

View File

@ -3,6 +3,7 @@ package problem
import ( import (
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
"git.0x7f.app/WOJ/woj-server/internal/model" "git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -34,7 +35,7 @@ func (h *handler) Details(c *gin.Context) {
claim, exist := c.Get("claim") claim, exist := c.Get("claim")
shouldEnable := !exist || claim.(*model.Claim).Role < model.RoleAdmin shouldEnable := !exist || claim.(*model.Claim).Role < model.RoleAdmin
p, status := h.problemService.Query(req.Pid, true, shouldEnable) p, status := h.problemService.Query(&problem.QueryData{ID: req.Pid, Associations: true, ShouldEnable: shouldEnable})
if status != e.Success { if status != e.Success {
e.Pong[any](c, status, nil) e.Pong[any](c, status, nil)
return return

View File

@ -2,13 +2,16 @@ package problem
import ( import (
"git.0x7f.app/WOJ/woj-server/internal/e" "git.0x7f.app/WOJ/woj-server/internal/e"
_ "git.0x7f.app/WOJ/woj-server/internal/model" // swag requires this "git.0x7f.app/WOJ/woj-server/internal/model"
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type searchRequest struct { type searchRequest struct {
Keyword string `form:"keyword" json:"keyword"` Keyword string `form:"keyword" json:"keyword"`
Tag string `form:"tag" json:"tag"` Tag string `form:"tag" json:"tag"`
Offset int `form:"offset" json:"offset"`
Limit int `form:"limit" json:"limit" binding:"required"`
} }
// Search // Search
@ -19,7 +22,9 @@ type searchRequest struct {
// @Produce json // @Produce json
// @Param keyword formData string false "keyword" // @Param keyword formData string false "keyword"
// @Param tag formData string false "tag" // @Param tag formData string false "tag"
// @Response 200 {object} e.Response[[]model.Problem] "problems found" // @Param offset formData int false "start position"
// @Param limit formData int true "limit number of records"
// @Response 200 {object} e.Response[e.WithCount[model.Problem]] "problems found"
// @Router /v1/problem/search [post] // @Router /v1/problem/search [post]
func (h *handler) Search(c *gin.Context) { func (h *handler) Search(c *gin.Context) {
req := new(searchRequest) req := new(searchRequest)
@ -28,15 +33,15 @@ func (h *handler) Search(c *gin.Context) {
return return
} }
// TODO: pagination var count int64
if req.Keyword == "" { param := problem.QueryData{
// TODO: query without LIKE Keyword: req.Keyword,
problems, status := h.problemService.QueryFuzz(req.Keyword, req.Tag, true, true) Tag: req.Tag,
e.Pong(c, status, problems) Offset: req.Offset,
return Limit: req.Limit,
} else { Count: &count,
problems, status := h.problemService.QueryFuzz(req.Keyword, req.Tag, true, true)
e.Pong(c, status, problems)
return
} }
problems, status := h.problemService.QueryFuzz(&param)
e.Pong(c, status, e.WithCount[*model.Problem]{Count: count, Data: problems})
} }

View File

@ -72,7 +72,7 @@ func (h *handler) Update(c *gin.Context) {
return return
} else { // update problem } else { // update problem
// check if problem exists // check if problem exists
p, status := h.problemService.Query(req.Pid, true, false) p, status := h.problemService.Query(&problem.QueryData{ID: req.Pid, Associations: true, ShouldEnable: false})
if status != e.Success { if status != e.Success {
e.Pong[any](c, status, nil) e.Pong[any](c, status, nil)
return return

View File

@ -9,23 +9,24 @@ import (
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
func (s *service) Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) { func (s *service) Query(data *QueryData) (*model.Problem, e.Status) {
problem := new(model.Problem) problem := new(model.Problem)
query := s.db.Get() query := s.db.Get()
if associations {
if data.Associations {
query = query.Preload(clause.Associations) query = query.Preload(clause.Associations)
} }
err := query.First(&problem, pid).Error err := query.First(&problem, data.ID).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, e.ProblemNotFound return nil, e.ProblemNotFound
} }
if err != nil { if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", pid)) s.log.Warn("DatabaseError", zap.Error(err), zap.Any("pid", data.ID))
return nil, e.DatabaseError return nil, e.DatabaseError
} }
if shouldEnable && !problem.IsEnabled { if data.ShouldEnable && !problem.IsEnabled {
return nil, e.ProblemNotAvailable return nil, e.ProblemNotAvailable
} }
return problem, e.Success return problem, e.Success

View File

@ -7,28 +7,52 @@ import (
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
) )
func (s *service) QueryFuzz(keyword string, tag string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) { type QueryData struct {
// precise
ID uint
// fuzz
Keyword string
Tag string
// common
Associations bool
ShouldEnable bool
// paging
Offset int
Limit int
Count *int64
}
func (s *service) QueryFuzz(data *QueryData) ([]*model.Problem, e.Status) {
problems := make([]*model.Problem, 0) problems := make([]*model.Problem, 0)
query := s.db.Get() query := s.db.Get()
if associations { if data.Associations {
query = query.Preload(clause.Associations) query = query.Preload(clause.Associations)
} }
if shouldEnable {
if data.ShouldEnable {
query = query.Where("is_enabled = true") query = query.Where("is_enabled = true")
} }
query = query. if data.Keyword != "" {
Where(s.db.Get().Where("title LIKE ?", "%"+keyword+"%"). query = query.
Or("statement LIKE ?", "%"+keyword+"%")) Where(s.db.Get().Where("title LIKE ?", "%"+data.Keyword+"%").
Or("statement LIKE ?", "%"+data.Keyword+"%"))
if tag != "" {
query = query.Where("EXISTS(SELECT 1 FROM unnest(tags) AS elem WHERE elem LIKE ?)", "%"+tag+"%")
} }
err := query.Find(&problems).Error if data.Tag != "" {
query = query.Where("EXISTS(SELECT 1 FROM unnest(tags) AS elem WHERE elem LIKE ?)", "%"+data.Tag+"%")
}
err := query.Order("created_at ASC").
Offset(data.Offset).Limit(data.Limit).Find(&problems).
Offset(-1).Limit(-1).Count(data.Count).
Error
if err != nil { if err != nil {
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("keyword", keyword), zap.Any("tag", tag)) s.log.Warn("DatabaseError", zap.Error(err), zap.Any("QueryData", data))
return nil, e.DatabaseError return nil, e.DatabaseError
} }

View File

@ -14,8 +14,8 @@ var _ Service = (*service)(nil)
type Service interface { type Service interface {
Create(data *CreateData) (*model.Problem, e.Status) Create(data *CreateData) (*model.Problem, e.Status)
Update(problem *model.Problem) (*model.Problem, e.Status) Update(problem *model.Problem) (*model.Problem, e.Status)
Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status) Query(data *QueryData) (*model.Problem, e.Status)
QueryFuzz(keyword string, tag string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status) QueryFuzz(data *QueryData) ([]*model.Problem, e.Status)
CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status) CreateVersion(data *CreateVersionData) (*model.ProblemVersion, e.Status)
UpdateVersion(pvid uint, values interface{}) e.Status UpdateVersion(pvid uint, values interface{}) e.Status

103
resource/runner/scripts/import.sh Executable file
View File

@ -0,0 +1,103 @@
#!/usr/bin/env bash
WORKSPACE=$(cd "$(dirname "$0")"/.. && pwd)
. "$WORKSPACE"/scripts/common.sh
# read -p "Enter HTTP API Endpoint: " -r endpoint
# if [ -z "$endpoint" ]; then
# log_error "[-] HTTP API Endpoint cannot be empty"
# exit 1
# fi
#
# read -p "Enter token: " -r token
# if [ -z "$token" ]; then
# log_error "[-] Token cannot be empty"
# exit 1
# fi
endpoint="http://localhost:8000"
token="eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicm9sZSI6MzAsInZlcnNpb24iOjEsImV4cCI6MTcwNDAwMzg4MCwibmJmIjoxNzAzMzk5MDgwLCJpYXQiOjE3MDMzOTkwODAsImp0aSI6ImVoOU92dVVNRktaZnV3ZngifQ.5F92odFZ-KZolHNIAc5f93exyugTBt5_4PpjYIdZxcHBfQRq_xAUOnJEdCfOBQtNy0-mfdvylkD7oR288Zew2w"
for problem in "$WORKSPACE/problem/"*; do
if [ -d "$problem" ]; then
dir_name=$(basename "$problem")
log_info "[+] Importing problem $dir_name"
if [ ! -f "$problem/config.json" ]; then
log_warn "[-] Skipping: $dir_name/config.json not found"
continue
fi
if [ ! -f "$problem/description.md" ]; then
log_warn "[-] Skipping: $dir_name/description.md not found"
continue
fi
read -p "Are you sure you want to import $dir_name? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_warn "[-] Skipping: user cancelled"
continue
fi
title=$(head -n 1 "$problem/description.md" | sed -e 's/^# //' | xargs)
description=$(cat "$problem/description.md")
log_info "[*] Title: $title"
zip_file=$(mktemp -u --suffix .zip)
log_info "[*] Compressing $problem into $zip_file" >/dev/null
cd "$problem" && zip -9rq "$zip_file" . -x ".mark.prebuild" && cd ..
payload=$(jq -nc \
--arg title "$title" \
--arg description "$description" \
'{ pid: 0, title: $title, statement: $description, is_enable: false }')
# log_info "[*] Creating problem statement"
# response=$(curl -s \
# -H "Content-Type: application/json" \
# -H "Authorization: $token" \
# -X POST \
# -d "$payload" \
# "$endpoint/api/v1/problem/update")
# code=$(echo "$response" | jq -r '.code')
# if [ "$code" != "0" ]; then
# log_error "[-] Failed to create problem statement"
# log_error "[-] Response: $response"
# continue
# fi
# id=$(echo "$response" | jq -r '.body.meta.ID')
# log_info "[*] Problem statement created with id: $id"
log_info "[*] Uploading problem package"
response=$(curl -s \
-H "Authorization: $token" \
-X POST \
"$endpoint/api/v1/problem/upload")
code=$(echo "$response" | jq -r '.code')
if [ "$code" != "0" ]; then
log_error "[-] Failed to get upload url"
log_error "[-] Response: $response"
continue
fi
upload_url=$(echo "$response" | jq -r '.body.url')
storage_key=$(echo "$response" | jq -r '.body.key')
curl -s -X PUT -T "$zip_file" "$upload_url"
log_info "[*] Creating problem version"
payload=$(jq -nc \
--arg pid "$title" \
--arg storage_key "$storage_key" \
'{ pid: $pid, storage_key: $storage_key }')
echo "$response"
fi
done