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
|
||||
_, 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 {
|
||||
e.Pong[any](c, status, nil)
|
||||
return
|
||||
|
@ -3,6 +3,7 @@ package problem
|
||||
import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/service/problem"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@ -34,7 +35,7 @@ func (h *handler) Details(c *gin.Context) {
|
||||
claim, exist := c.Get("claim")
|
||||
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 {
|
||||
e.Pong[any](c, status, nil)
|
||||
return
|
||||
|
@ -2,13 +2,16 @@ package problem
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
type searchRequest struct {
|
||||
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
|
||||
@ -19,7 +22,9 @@ type searchRequest struct {
|
||||
// @Produce json
|
||||
// @Param keyword formData string false "keyword"
|
||||
// @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]
|
||||
func (h *handler) Search(c *gin.Context) {
|
||||
req := new(searchRequest)
|
||||
@ -28,15 +33,15 @@ func (h *handler) Search(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: pagination
|
||||
if req.Keyword == "" {
|
||||
// TODO: query without LIKE
|
||||
problems, status := h.problemService.QueryFuzz(req.Keyword, req.Tag, true, true)
|
||||
e.Pong(c, status, problems)
|
||||
return
|
||||
} else {
|
||||
problems, status := h.problemService.QueryFuzz(req.Keyword, req.Tag, true, true)
|
||||
e.Pong(c, status, problems)
|
||||
return
|
||||
var count int64
|
||||
param := problem.QueryData{
|
||||
Keyword: req.Keyword,
|
||||
Tag: req.Tag,
|
||||
Offset: req.Offset,
|
||||
Limit: req.Limit,
|
||||
Count: &count,
|
||||
}
|
||||
|
||||
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
|
||||
} else { // update problem
|
||||
// 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 {
|
||||
e.Pong[any](c, status, nil)
|
||||
return
|
||||
|
@ -9,23 +9,24 @@ import (
|
||||
"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)
|
||||
|
||||
query := s.db.Get()
|
||||
if associations {
|
||||
|
||||
if data.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) {
|
||||
return nil, e.ProblemNotFound
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if shouldEnable && !problem.IsEnabled {
|
||||
if data.ShouldEnable && !problem.IsEnabled {
|
||||
return nil, e.ProblemNotAvailable
|
||||
}
|
||||
return problem, e.Success
|
||||
|
@ -7,28 +7,52 @@ import (
|
||||
"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)
|
||||
query := s.db.Get()
|
||||
|
||||
if associations {
|
||||
if data.Associations {
|
||||
query = query.Preload(clause.Associations)
|
||||
}
|
||||
if shouldEnable {
|
||||
|
||||
if data.ShouldEnable {
|
||||
query = query.Where("is_enabled = true")
|
||||
}
|
||||
|
||||
query = query.
|
||||
Where(s.db.Get().Where("title LIKE ?", "%"+keyword+"%").
|
||||
Or("statement LIKE ?", "%"+keyword+"%"))
|
||||
|
||||
if tag != "" {
|
||||
query = query.Where("EXISTS(SELECT 1 FROM unnest(tags) AS elem WHERE elem LIKE ?)", "%"+tag+"%")
|
||||
if data.Keyword != "" {
|
||||
query = query.
|
||||
Where(s.db.Get().Where("title LIKE ?", "%"+data.Keyword+"%").
|
||||
Or("statement LIKE ?", "%"+data.Keyword+"%"))
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -14,8 +14,8 @@ var _ Service = (*service)(nil)
|
||||
type Service interface {
|
||||
Create(data *CreateData) (*model.Problem, e.Status)
|
||||
Update(problem *model.Problem) (*model.Problem, e.Status)
|
||||
Query(pid uint, associations bool, shouldEnable bool) (*model.Problem, e.Status)
|
||||
QueryFuzz(keyword string, tag string, associations bool, shouldEnable bool) ([]*model.Problem, e.Status)
|
||||
Query(data *QueryData) (*model.Problem, e.Status)
|
||||
QueryFuzz(data *QueryData) ([]*model.Problem, e.Status)
|
||||
|
||||
CreateVersion(data *CreateVersionData) (*model.ProblemVersion, 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