diff --git a/src/award.go b/src/award.go index 5885ad5..acea0cd 100644 --- a/src/award.go +++ b/src/award.go @@ -14,6 +14,25 @@ type Award struct { Points int } +func ParseAward(s string) (*Award, error) { + ret := Award{} + + s = strings.Trim(s, " \t\n") + + var whenEpoch int64 + + n, err := fmt.Sscanf(s, "%d %s %s %d", &whenEpoch, &ret.TeamId, &ret.Category, &ret.Points) + if err != nil { + return nil, err + } else if n != 4 { + return nil, fmt.Errorf("Malformed award string: only parsed %d fields", n) + } + + ret.When = time.Unix(whenEpoch, 0) + + return &ret, nil +} + func (a *Award) String() string { return fmt.Sprintf("%d %s %s %d", a.When.Unix(), a.TeamId, a.Category, a.Points) } @@ -40,21 +59,14 @@ func (a *Award) MarshalJSON() ([]byte, error) { return []byte(ret), nil } -func ParseAward(s string) (*Award, error) { - ret := Award{} - - s = strings.Trim(s, " \t\n") - - var whenEpoch int64 - - n, err := fmt.Sscanf(s, "%d %s %s %d", &whenEpoch, &ret.TeamId, &ret.Category, &ret.Points) - if err != nil { - return nil, err - } else if n != 4 { - return nil, fmt.Errorf("Malformed award string: only parsed %d fields", n) +func (a *Award) Same(o *Award) bool { + switch { + case a.TeamId != o.TeamId: + return false + case a.Category != o.Category: + return false + case a.Points != o.Points: + return false } - - ret.When = time.Unix(whenEpoch, 0) - - return &ret, nil -} + return true +} \ No newline at end of file diff --git a/src/handlers.go b/src/handlers.go index 41150f7..2f5f223 100644 --- a/src/handlers.go +++ b/src/handlers.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/json" "fmt" + "io" "log" "net/http" "os" @@ -22,6 +23,32 @@ func respond(w http.ResponseWriter, req *http.Request, status Status, short stri } } +// anchoredSearch looks for needle in r, +// skipping the first skip space-delimited words +func anchoredSearch(r io.Reader, needle string, skip int) bool { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + parts := strings.SplitN(line, " ", skip+1) + if (len(parts) > skip) && (parts[skip] == needle) { + return true + } + } + + return false +} + +// anchoredSearchFile performs an anchoredSearch on a given filename +func anchoredSearchFile(filename string, needle string, skip int) bool { + r, err := os.Open(filename) + if err != nil { + return false + } + defer r.Close() + + return anchoredSearch(r, needle, skip) +} + func (ctx Instance) registerHandler(w http.ResponseWriter, req *http.Request) { teamname := req.FormValue("name") teamid := req.FormValue("id") @@ -172,7 +199,7 @@ func (ctx Instance) answerHandler(w http.ResponseWriter, req *http.Request) { return } - if err := ctx.AwardPoints(teamid, category, points); err != nil { + if err := ctx.AwardPointsUniquely(teamid, category, points); err != nil { respond( w, req, Error, "Error awarding points", diff --git a/src/instance.go b/src/instance.go index 771fefd..dfb4168 100644 --- a/src/instance.go +++ b/src/instance.go @@ -89,8 +89,8 @@ func (ctx *Instance) StatePath(parts ...string) string { return path.Join(ctx.StateDir, tail) } -func (ctx *Instance) PointsLog() []Award { - var ret []Award +func (ctx *Instance) PointsLog() []*Award { + var ret []*Award fn := ctx.StatePath("points.log") f, err := os.Open(fn) @@ -108,14 +108,14 @@ func (ctx *Instance) PointsLog() []Award { log.Printf("Skipping malformed award line %s: %s", line, err) continue } - ret = append(ret, *cur) + ret = append(ret, cur) } return ret } // awardPoints gives points points to team teamid in category category -func (ctx *Instance) AwardPoints(teamid string, category string, points int) error { +func (ctx *Instance) AwardPoints(teamid, category string, points int) error { fn := fmt.Sprintf("%s-%s-%d", teamid, category, points) tmpfn := ctx.StatePath("points.tmp", fn) newfn := ctx.StatePath("points.new", fn) @@ -134,6 +134,23 @@ func (ctx *Instance) AwardPoints(teamid string, category string, points int) err return nil } +func (ctx *Instance) AwardPointsUniquely(teamid, category string, points int) error { + a := Award{ + When: time.Now(), + TeamId: teamid, + Category: category, + Points: points, + } + + for _, e := range ctx.PointsLog() { + if a.Same(e) { + return fmt.Errorf("Points already awarded to this team in this category") + } + } + + return ctx.AwardPoints(teamid, category, points) +} + func (ctx *Instance) OpenCategoryFile(category string, parts ...string) (io.ReadCloser, error) { mb, ok := ctx.Categories[category] if !ok { diff --git a/src/maintenance.go b/src/maintenance.go index dd231c7..75d839b 100644 --- a/src/maintenance.go +++ b/src/maintenance.go @@ -90,8 +90,21 @@ func (ctx *Instance) CollectPoints() { log.Printf("Can't parse award file %s: %s", filename, err) continue } - fmt.Fprintf(logf, "%s\n", award.String()) - log.Print("XXX: check for duplicates", award.String()) + + duplicate := false + for _, e := range ctx.PointsLog() { + if award.Same(e) { + duplicate = true + break + } + } + + if duplicate { + log.Printf("Skipping duplicate points: %s", award.String()) + } else { + fmt.Fprintf(logf, "%s\n", award.String()) + } + logf.Sync() if err := os.Remove(filename); err != nil { log.Printf("Unable to remove %s: %s", filename, err) diff --git a/src/static.go b/src/static.go index 44ca41e..ef209d3 100644 --- a/src/static.go +++ b/src/static.go @@ -1,42 +1,14 @@ package main import ( - "bufio" "encoding/json" "fmt" - "io" "net/http" "os" "path/filepath" "strings" ) -// anchoredSearch looks for needle in r, -// skipping the first skip space-delimited words -func anchoredSearch(r io.Reader, needle string, skip int) bool { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := scanner.Text() - parts := strings.SplitN(line, " ", skip+1) - if (len(parts) > skip) && (parts[skip] == needle) { - return true - } - } - - return false -} - -// anchoredSearchFile performs an anchoredSearch on a given filename -func anchoredSearchFile(filename string, needle string, skip int) bool { - r, err := os.Open(filename) - if err != nil { - return false - } - defer r.Close() - - return anchoredSearch(r, needle, skip) -} - type Status int const (