Skip to content

Commit

Permalink
feat: userside half-cycle review (#6)
Browse files Browse the repository at this point in the history
* feat: userside full-cycle review

* feat: true source in codereview
  • Loading branch information
Gornak40 authored Apr 26, 2024
1 parent adb094b commit 2c884bc
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 47 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ GIN_SECRET = <SECRET>
JWT_SECRET = <SECRET>
POLL_BATCH_SIZE = 50
POLL_DELAY_SECONDS = 10
REVIEW_LIMIT = 3
```

## Build
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type ServerConfig struct {
JWTSecret string `ini:"JWT_SECRET"`
PollBatchSize int64 `ini:"POLL_BATCH_SIZE"`
PollDelaySeconds int64 `ini:"POLL_DELAY_SECONDS"`
ReviewLimit int64 `ini:"REVIEW_LIMIT"`
}

func NewConfig() (*Config, error) {
Expand Down
2 changes: 1 addition & 1 deletion controller/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (s *Server) adminMiddleware(c *gin.Context) {
token, ok := session.Get("jwt").(string)
if !ok {
_ = alerts.Add(session, alerts.Alert{
Message: "You should enter JWT",
Message: "Enter admin JWT",
Type: alerts.TypeWarning,
})
c.Redirect(http.StatusFound, "/admin")
Expand Down
34 changes: 23 additions & 11 deletions controller/codereview.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controller

import (
"encoding/json"
"net/http"

"github.com/Gornak40/crosspawn/internal/alerts"
Expand All @@ -12,25 +13,36 @@ func (s *Server) CodereviewGET(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

contest := session.Get("contest")
problem := session.Get("problem")

if contest == nil || problem == nil {
submitEn, ok := session.Get("submit").(string)
if !ok {
_ = alerts.Add(session, alerts.Alert{
Message: "Please select contest and problem",
Message: "Select contest and problem",
Type: alerts.TypeInfo,
})
c.Redirect(http.StatusFound, "/")

return
}

// TODO: get code from ejudge
var submit reviewContext
if err := json.Unmarshal([]byte(submitEn), &submit); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to unmarshal submit"})

return
}

source, err := s.ej.GetRunSource(submit.ContestID, submit.RunID)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}

submit.Source = source
c.HTML(http.StatusOK, "codereview.html", gin.H{
"Title": "Review",
"User": user,
"CodeTitle": contest,
"Code": problem,
"Flashes": alerts.Get(session),
"Title": "Review",
"User": user,
"Submit": submit,
"Flashes": alerts.Get(session),
})
}
108 changes: 101 additions & 7 deletions controller/index.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
package controller

import (
"encoding/json"
"errors"
"fmt"
"net/http"

"github.com/Gornak40/crosspawn/internal/alerts"
"github.com/Gornak40/crosspawn/models"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

var (
errNoOKSubmit = errors.New("no OK submit")
)

type reviewForm struct {
Contest int `binding:"required" form:"ejContest"`
Contest uint `binding:"required" form:"ejContest"`
Problem string `binding:"required" form:"ejProblem"`
}

func (s *Server) IndexGET(c *gin.Context) {
session := sessions.Default(c)
user := session.Get("user")

contest := session.Get("contest")
problem := session.Get("problem")

if contest != nil && problem != nil {
if session.Get("submit") != nil {
_ = alerts.Add(session, alerts.Alert{
Message: "Finish your current review",
Type: alerts.TypeWarning,
})
c.Redirect(http.StatusFound, "/codereview")

return
Expand All @@ -33,6 +42,14 @@ func (s *Server) IndexGET(c *gin.Context) {
})
}

type reviewContext struct {
RunID uint `json:"run_id"`
ContestID uint `json:"contest_id"`
Problem string `json:"problem"`
Source string `json:"source"`
}

//nolint:funlen // Life is hard
func (s *Server) IndexPOST(c *gin.Context) {
var form reviewForm
if err := c.ShouldBind(&form); err != nil {
Expand All @@ -42,9 +59,86 @@ func (s *Server) IndexPOST(c *gin.Context) {
}

session := sessions.Default(c)
user, ok := session.Get("user").(string)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user is not authenticated"})

return
}

// User must not have current review
if session.Get("submit") != nil {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": "user has unclosed review"})

return
}

// Contest must be valid
if s.db.Where(&models.Contest{EjudgeID: form.Contest, ReviewActive: true}).First(&models.Contest{}).Error != nil {
_ = alerts.Add(session, alerts.Alert{
Message: fmt.Sprintf("Contest %d is not in pool", form.Contest),
Type: alerts.TypeDanger,
})
c.Redirect(http.StatusFound, "/")

return
}

// User must have OK submit
if err := s.reviewValid(user, form.Contest, form.Problem); err != nil {
if errors.Is(err, errNoOKSubmit) {
_ = alerts.Add(session, alerts.Alert{
Message: fmt.Sprintf("Solve %s in %d first", form.Problem, form.Contest),
Type: alerts.TypeDanger,
})
c.Redirect(http.StatusFound, "/")

session.Set("contest", form.Contest)
session.Set("problem", form.Problem)
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}

// Good submit must exist
var dbRun models.Run
magicFilter := "ejudge_contest_id = ? AND ejudge_name = ? AND ejudge_user_login != ? AND review_count <= ?"
if err := s.db.Where(magicFilter, form.Contest, form.Problem, user, s.cfg.ReviewLimit).
Order("RANDOM()").First(&dbRun).Error; err != nil {
_ = alerts.Add(session, alerts.Alert{
Message: "No runs to review",
Type: alerts.TypeWarning,
})
c.Redirect(http.StatusFound, "/")

return
}

rctx := reviewContext{
RunID: dbRun.EjudgeID,
ContestID: dbRun.EjudgeContestID,
Problem: dbRun.EjudgeName,
}
data, _ := json.Marshal(rctx) //nolint:errchkjson // It's my JSON
session.Set("submit", string(data))
_ = session.Save()

_ = alerts.Add(session, alerts.Alert{
Message: "Review started",
Type: alerts.TypeSuccess,
})
c.Redirect(http.StatusFound, "/codereview")
}

func (s *Server) reviewValid(user string, contestID uint, problem string) error {
filter := fmt.Sprintf("login == '%s' && prob == '%s' && (status == OK || status == PR)", user, problem)
runs, err := s.ej.GetContestRuns(contestID, filter)
if err != nil {
return err
}
if len(runs.Runs) == 0 {
return fmt.Errorf("%w: %d", errNoOKSubmit, contestID)
}

return nil
}
6 changes: 6 additions & 0 deletions controller/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/Gornak40/crosspawn/internal/alerts"
"github.com/Gornak40/crosspawn/models"
"github.com/Gornak40/crosspawn/pkg/ejudge"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -46,6 +47,11 @@ func (s *Server) LoginPOST(c *gin.Context) {
return
}

dbUser := models.NewUserFromForm(form.Login, form.Password)
if err := s.db.Where(&models.User{EjudgeLogin: form.Login}).FirstOrCreate(&dbUser).Error; err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}

session.Set("user", form.Login)
_ = session.Save()

Expand Down
15 changes: 8 additions & 7 deletions controller/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ func (s *Server) ManageGET(c *gin.Context) {
user := session.Get("user")

var contests []models.Contest
if res := s.db.Find(&contests); res.Error != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})
if err := s.db.Find(&contests).Error; err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}
Expand All @@ -32,6 +32,7 @@ func (s *Server) ManageGET(c *gin.Context) {
})
}

// TODO: add check for judge credentials.
// TODO: add check for acm format.
func (s *Server) ManagePOST(c *gin.Context) {
var form manageForm
Expand All @@ -49,8 +50,8 @@ func (s *Server) ManagePOST(c *gin.Context) {
}

dbContest := models.NewContestFromEj(contest)
if res := s.db.Create(dbContest); res.Error != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})
if err := s.db.Create(dbContest).Error; err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}
Expand All @@ -67,15 +68,15 @@ func (s *Server) ManageFlipPOST(c *gin.Context) {
}

dbContest := models.Contest{EjudgeID: form.ContestID}
if err := s.db.Where("ejudge_id = ?", form.ContestID).First(&dbContest).Error; err != nil {
if err := s.db.Where(&dbContest).First(&dbContest).Error; err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}

dbContest.ReviewActive = !dbContest.ReviewActive
if res := s.db.Save(&dbContest); res.Error != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})
if err := s.db.Save(&dbContest).Error; err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

return
}
Expand Down
10 changes: 5 additions & 5 deletions controller/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (

func (s *Server) Poll() error {
var contests []models.Contest
if res := s.db.Where("review_active = 1").Find(&contests); res.Error != nil {
return res.Error
if err := s.db.Where(&models.Contest{ReviewActive: true}).Find(&contests).Error; err != nil {
return err
}

for _, contest := range contests {
Expand All @@ -34,9 +34,9 @@ func (s *Server) pollContest(dbContest *models.Contest) error {
}

for _, run := range runs.Runs {
logrus.Info(run) // TODO: remove
dbRun := models.NewRunFromEj(&run, "babayka") //nolint:gosec // G601: Implicit memory aliasing in for loop.
if res := s.db.Create(dbRun); res.Error != nil {
logrus.Info(run) // TODO: move to debug
dbRun := models.NewRunFromEj(&run) //nolint:gosec // G601: Implicit memory aliasing in for loop.
if err := s.db.Create(dbRun).Error; err != nil {
logrus.WithError(err).WithFields(logrus.Fields{"contestID": run.ContestID, "runID": run.RunID}).
Error("failed to save run")
}
Expand Down
13 changes: 6 additions & 7 deletions models/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@ import (
type Run struct {
gorm.Model

EjudgeID uint `gorm:"not null"`
EjudgeContestID uint `gorm:"not null"`
EjudgeUserLogin string `gorm:"not null;type:varchar(128)"`
EjudgeSource string `gorm:"not null"`
EjudgeContest Contest `gorm:"foreignKey:EjudgeContestID;not null"`
EjudgeID uint `gorm:"not null"`
EjudgeContestID uint `gorm:"not null"`
EjudgeUserLogin string `gorm:"not null;type:varchar(128)"`
EjudgeName string `gorm:"not null;type:varchar(32)"`

ReviewCount uint `gorm:"not null"`
}

func NewRunFromEj(run *ejudge.EjRun, source string) *Run {
func NewRunFromEj(run *ejudge.EjRun) *Run {
return &Run{
EjudgeID: run.RunID,
EjudgeContestID: run.ContestID,
EjudgeUserLogin: run.UserLogin,
EjudgeSource: source,
EjudgeName: run.ProbName,
}
}
10 changes: 9 additions & 1 deletion models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ package models

import "gorm.io/gorm"

// TODO: set EjudgeID and hash password.
type User struct {
gorm.Model

EjudgeID uint `gorm:"unique;not null"`
EjudgeID uint `gorm:"not null"`
EjudgeLogin string `gorm:"unique;not null;type:varchar(128)"`
EjudgePassword string `gorm:"not null;type:varchar(128)"`

ReviewApproveCount uint `gorm:"not null"`
ReviewRejectCount uint `gorm:"not null"`
}

func NewUserFromForm(login, password string) *User {
return &User{
EjudgeLogin: login,
EjudgePassword: password,
}
}
Loading

0 comments on commit 2c884bc

Please sign in to comment.