feat: support pagination on search, close #4
This commit is contained in:
parent
e508c6f4e8
commit
297d031e5b
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(¶m)
|
||||||
|
e.Pong(c, status, e.WithCount[*model.Problem]{Count: count, Data: problems})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
103
resource/runner/scripts/import.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user