mirror of https://github.com/dirtbags/moth.git
Remove participant IDs, add team ID links
This commit is contained in:
parent
8e0f4561a5
commit
ec2a547637
|
@ -44,9 +44,8 @@ func (h *HTTPServer) HandleMothFunc(
|
||||||
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
|
mothHandler func(MothRequestHandler, http.ResponseWriter, *http.Request),
|
||||||
) {
|
) {
|
||||||
handler := func(w http.ResponseWriter, req *http.Request) {
|
handler := func(w http.ResponseWriter, req *http.Request) {
|
||||||
participantID := req.FormValue("pid")
|
|
||||||
teamID := req.FormValue("id")
|
teamID := req.FormValue("id")
|
||||||
mh := h.server.NewHandler(participantID, teamID)
|
mh := h.server.NewHandler(teamID)
|
||||||
mothHandler(mh, w, req)
|
mothHandler(mh, w, req)
|
||||||
}
|
}
|
||||||
h.HandleFunc(h.base+pattern, handler)
|
h.HandleFunc(h.base+pattern, handler)
|
||||||
|
|
|
@ -11,11 +11,8 @@ import (
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TestParticipantID = "shipox"
|
|
||||||
|
|
||||||
func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest.ResponseRecorder {
|
func (hs *HTTPServer) TestRequest(path string, args map[string]string) *httptest.ResponseRecorder {
|
||||||
vals := url.Values{}
|
vals := url.Values{}
|
||||||
vals.Set("pid", TestParticipantID)
|
|
||||||
vals.Set("id", TestTeamID)
|
vals.Set("id", TestTeamID)
|
||||||
for k, v := range args {
|
for k, v := range args {
|
||||||
vals.Set(k, v)
|
vals.Set(k, v)
|
||||||
|
|
|
@ -15,7 +15,7 @@ func TestIssue156(t *testing.T) {
|
||||||
afero.WriteFile(state, "teams/bloop", []byte("bloop: the team"), 0644)
|
afero.WriteFile(state, "teams/bloop", []byte("bloop: the team"), 0644)
|
||||||
state.refresh()
|
state.refresh()
|
||||||
|
|
||||||
handler := server.NewHandler("", "bloop")
|
handler := server.NewHandler("bloop")
|
||||||
es := handler.ExportState()
|
es := handler.ExportState()
|
||||||
if _, ok := es.TeamNames["self"]; !ok {
|
if _, ok := es.TeamNames["self"]; !ok {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
|
|
@ -55,13 +55,19 @@ type ThemeProvider interface {
|
||||||
type StateProvider interface {
|
type StateProvider interface {
|
||||||
Messages() string
|
Messages() string
|
||||||
PointsLog() award.List
|
PointsLog() award.List
|
||||||
TeamName(teamID string) (string, error)
|
TeamName(teamID string) (Team, error)
|
||||||
SetTeamName(teamID, teamName string) error
|
SetTeamName(teamID, teamName string) error
|
||||||
AwardPoints(teamID string, cat string, points int) error
|
AwardPoints(team Team, cat string, points int) error
|
||||||
LogEvent(event, participantID, teamID, cat string, points int, extra ...string)
|
LogEvent(event, teamID, cat string, points int, extra ...string)
|
||||||
Maintainer
|
Maintainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Team defines a team entry
|
||||||
|
type Team struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
// Maintainer is something that can be maintained.
|
// Maintainer is something that can be maintained.
|
||||||
type Maintainer interface {
|
type Maintainer interface {
|
||||||
// Maintain is the maintenance loop.
|
// Maintain is the maintenance loop.
|
||||||
|
@ -92,10 +98,9 @@ func NewMothServer(config Configuration, theme ThemeProvider, state StateProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler returns a new http.RequestHandler for the provided teamID.
|
// NewHandler returns a new http.RequestHandler for the provided teamID.
|
||||||
func (s *MothServer) NewHandler(participantID, teamID string) MothRequestHandler {
|
func (s *MothServer) NewHandler(teamID string) MothRequestHandler {
|
||||||
return MothRequestHandler{
|
return MothRequestHandler{
|
||||||
MothServer: s,
|
MothServer: s,
|
||||||
participantID: participantID,
|
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +108,6 @@ func (s *MothServer) NewHandler(participantID, teamID string) MothRequestHandler
|
||||||
// MothRequestHandler provides http.RequestHandler for a MothServer.
|
// MothRequestHandler provides http.RequestHandler for a MothServer.
|
||||||
type MothRequestHandler struct {
|
type MothRequestHandler struct {
|
||||||
*MothServer
|
*MothServer
|
||||||
participantID string
|
|
||||||
teamID string
|
teamID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,7 +135,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
|
||||||
|
|
||||||
// Log puzzle.json loads
|
// Log puzzle.json loads
|
||||||
if path == "puzzle.json" {
|
if path == "puzzle.json" {
|
||||||
mh.State.LogEvent("load", mh.participantID, mh.teamID, cat, points)
|
mh.State.LogEvent("load", mh.teamID, cat, points)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -139,6 +143,11 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
|
||||||
|
|
||||||
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
|
// CheckAnswer returns an error if answer is not a correct answer for puzzle points in category cat
|
||||||
func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error {
|
func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string) error {
|
||||||
|
team, err := mh.State.TeamName(mh.teamID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid team ID")
|
||||||
|
}
|
||||||
|
|
||||||
correct := false
|
correct := false
|
||||||
for _, provider := range mh.PuzzleProviders {
|
for _, provider := range mh.PuzzleProviders {
|
||||||
if ok, err := provider.CheckAnswer(cat, points, answer); err != nil {
|
if ok, err := provider.CheckAnswer(cat, points, answer); err != nil {
|
||||||
|
@ -148,19 +157,16 @@ func (mh *MothRequestHandler) CheckAnswer(cat string, points int, answer string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !correct {
|
if !correct {
|
||||||
mh.State.LogEvent("wrong", mh.participantID, mh.teamID, cat, points)
|
mh.State.LogEvent("wrong", mh.teamID, cat, points)
|
||||||
return fmt.Errorf("incorrect answer")
|
return fmt.Errorf("incorrect answer")
|
||||||
}
|
}
|
||||||
|
|
||||||
mh.State.LogEvent("correct", mh.participantID, mh.teamID, cat, points)
|
if err := mh.State.AwardPoints(team, cat, points); err != nil {
|
||||||
|
|
||||||
if _, err := mh.State.TeamName(mh.teamID); err != nil {
|
|
||||||
return fmt.Errorf("invalid team ID")
|
|
||||||
}
|
|
||||||
if err := mh.State.AwardPoints(mh.teamID, cat, points); err != nil {
|
|
||||||
return fmt.Errorf("error awarding points: %s", err)
|
return fmt.Errorf("error awarding points: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mh.State.LogEvent("correct", mh.teamID, cat, points)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +181,7 @@ func (mh *MothRequestHandler) Register(teamName string) error {
|
||||||
if teamName == "" {
|
if teamName == "" {
|
||||||
return fmt.Errorf("empty team name")
|
return fmt.Errorf("empty team name")
|
||||||
}
|
}
|
||||||
mh.State.LogEvent("register", mh.participantID, mh.teamID, "", 0)
|
mh.State.LogEvent("register", mh.teamID, "", 0)
|
||||||
return mh.State.SetTeamName(mh.teamID, teamName)
|
return mh.State.SetTeamName(mh.teamID, teamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +200,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(forceRegistered bool) *Sta
|
||||||
export := StateExport{}
|
export := StateExport{}
|
||||||
export.Config = mh.Config
|
export.Config = mh.Config
|
||||||
|
|
||||||
teamName, err := mh.State.TeamName(mh.teamID)
|
team, err := mh.State.TeamName(mh.teamID)
|
||||||
registered := forceRegistered || mh.Config.Devel || (err == nil)
|
registered := forceRegistered || mh.Config.Devel || (err == nil)
|
||||||
|
|
||||||
export.Messages = mh.State.Messages()
|
export.Messages = mh.State.Messages()
|
||||||
|
@ -207,7 +213,7 @@ func (mh *MothRequestHandler) exportStateIfRegistered(forceRegistered bool) *Sta
|
||||||
export.PointsLog = make(award.List, len(pointsLog))
|
export.PointsLog = make(award.List, len(pointsLog))
|
||||||
|
|
||||||
if registered {
|
if registered {
|
||||||
export.TeamNames["self"] = teamName
|
export.TeamNames["self"] = team.Name
|
||||||
exportIDs[mh.teamID] = "self"
|
exportIDs[mh.teamID] = "self"
|
||||||
}
|
}
|
||||||
for logno, awd := range pointsLog {
|
for logno, awd := range pointsLog {
|
||||||
|
@ -215,10 +221,10 @@ func (mh *MothRequestHandler) exportStateIfRegistered(forceRegistered bool) *Sta
|
||||||
awd.TeamID = id
|
awd.TeamID = id
|
||||||
} else {
|
} else {
|
||||||
exportID := strconv.Itoa(logno)
|
exportID := strconv.Itoa(logno)
|
||||||
name, _ := mh.State.TeamName(awd.TeamID)
|
team, _ := mh.State.TeamName(awd.TeamID)
|
||||||
exportIDs[awd.TeamID] = exportID
|
exportIDs[awd.TeamID] = exportID
|
||||||
awd.TeamID = exportID
|
awd.TeamID = exportID
|
||||||
export.TeamNames[exportID] = name
|
export.TeamNames[exportID] = team.Name
|
||||||
}
|
}
|
||||||
export.PointsLog[logno] = awd
|
export.PointsLog[logno] = awd
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (ts TestServer) refresh() {
|
||||||
func TestDevelServer(t *testing.T) {
|
func TestDevelServer(t *testing.T) {
|
||||||
server := NewTestServer()
|
server := NewTestServer()
|
||||||
server.Config.Devel = true
|
server.Config.Devel = true
|
||||||
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
|
anonHandler := server.NewHandler("badTeamId")
|
||||||
|
|
||||||
{
|
{
|
||||||
es := anonHandler.ExportState()
|
es := anonHandler.ExportState()
|
||||||
|
@ -57,12 +57,11 @@ func TestDevelServer(t *testing.T) {
|
||||||
|
|
||||||
func TestProdServer(t *testing.T) {
|
func TestProdServer(t *testing.T) {
|
||||||
teamName := "OurTeam"
|
teamName := "OurTeam"
|
||||||
participantID := "participantID"
|
|
||||||
teamID := TestTeamID
|
teamID := TestTeamID
|
||||||
|
|
||||||
server := NewTestServer()
|
server := NewTestServer()
|
||||||
handler := server.NewHandler(participantID, teamID)
|
handler := server.NewHandler(teamID)
|
||||||
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
|
anonHandler := server.NewHandler("badTeamId")
|
||||||
|
|
||||||
{
|
{
|
||||||
es := handler.ExportState()
|
es := handler.ExportState()
|
||||||
|
|
|
@ -27,6 +27,10 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy="
|
||||||
// This is also a valid RFC3339 format.
|
// This is also a valid RFC3339 format.
|
||||||
const RFC3339Space = "2006-01-02 15:04:05Z07:00"
|
const RFC3339Space = "2006-01-02 15:04:05Z07:00"
|
||||||
|
|
||||||
|
// TeamLinkPrefix is prepended to a Team ID and stored as a team name
|
||||||
|
// to handle "linked" team IDs, used for participant ID to team mapping.
|
||||||
|
const TeamLinkPrefix = "link>"
|
||||||
|
|
||||||
// ErrAlreadyRegistered means a team cannot be registered because it was registered previously.
|
// ErrAlreadyRegistered means a team cannot be registered because it was registered previously.
|
||||||
var ErrAlreadyRegistered = errors.New("team ID has already been registered")
|
var ErrAlreadyRegistered = errors.New("team ID has already been registered")
|
||||||
|
|
||||||
|
@ -45,8 +49,8 @@ type State struct {
|
||||||
eventWriterFile afero.File
|
eventWriterFile afero.File
|
||||||
|
|
||||||
// Caches, so we're not hammering NFS with metadata operations
|
// Caches, so we're not hammering NFS with metadata operations
|
||||||
teamNamesLastChange time.Time
|
teamsLastChange time.Time
|
||||||
teamNames map[string]string
|
teams map[string]Team
|
||||||
pointsLog award.List
|
pointsLog award.List
|
||||||
messages string
|
messages string
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
|
@ -60,7 +64,7 @@ func NewState(fs afero.Fs) *State {
|
||||||
refreshNow: make(chan bool, 5),
|
refreshNow: make(chan bool, 5),
|
||||||
eventStream: make(chan []string, 80),
|
eventStream: make(chan []string, 80),
|
||||||
|
|
||||||
teamNames: make(map[string]string),
|
teams: make(map[string]Team),
|
||||||
}
|
}
|
||||||
if err := s.reopenEventLog(); err != nil {
|
if err := s.reopenEventLog(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -121,29 +125,33 @@ func (s *State) updateEnabled() {
|
||||||
s.Enabled = nextEnabled
|
s.Enabled = nextEnabled
|
||||||
log.Printf("Setting enabled=%v: %s", s.Enabled, why)
|
log.Printf("Setting enabled=%v: %s", s.Enabled, why)
|
||||||
if s.Enabled {
|
if s.Enabled {
|
||||||
s.LogEvent("enabled", "", "", "", 0, why)
|
s.LogEvent("enabled", "", "", 0, why)
|
||||||
} else {
|
} else {
|
||||||
s.LogEvent("disabled", "", "", "", 0, why)
|
s.LogEvent("disabled", "", "", 0, why)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamName returns team name given a team ID.
|
// TeamName returns a Team given a team ID.
|
||||||
func (s *State) TeamName(teamID string) (string, error) {
|
//
|
||||||
|
// The returned team ID will be the dereferenced filename of the team.
|
||||||
|
// This allows you to have symbolic links to team IDs,
|
||||||
|
// in order to implement participant IDs
|
||||||
|
func (s *State) TeamName(teamID string) (Team, error) {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
name, ok := s.teamNames[teamID]
|
team, ok := s.teams[teamID]
|
||||||
s.lock.RUnlock()
|
s.lock.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("unregistered team ID: %s", teamID)
|
return Team{}, fmt.Errorf("unregistered team ID: %s", teamID)
|
||||||
}
|
}
|
||||||
return name, nil
|
return team, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTeamName writes out team name.
|
// SetTeamName writes out team name.
|
||||||
// This can only be done once per team.
|
// This can only be done once per team.
|
||||||
func (s *State) SetTeamName(teamID, teamName string) error {
|
func (s *State) SetTeamName(teamID, teamName string) error {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
_, ok := s.teamNames[teamID]
|
_, ok := s.teams[teamID]
|
||||||
s.lock.RUnlock()
|
s.lock.RUnlock()
|
||||||
if ok {
|
if ok {
|
||||||
return ErrAlreadyRegistered
|
return ErrAlreadyRegistered
|
||||||
|
@ -166,6 +174,9 @@ func (s *State) SetTeamName(teamID, teamName string) error {
|
||||||
return fmt.Errorf("team ID not found in list of valid team IDs")
|
return fmt.Errorf("team ID not found in list of valid team IDs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for strings.HasPrefix(teamName, TeamLinkPrefix) {
|
||||||
|
teamName = teamName[len(TeamLinkPrefix):]
|
||||||
|
}
|
||||||
teamFilename := filepath.Join("teams", teamID)
|
teamFilename := filepath.Join("teams", teamID)
|
||||||
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
|
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
|
||||||
if os.IsExist(err) {
|
if os.IsExist(err) {
|
||||||
|
@ -199,20 +210,20 @@ func (s *State) Messages() string {
|
||||||
return s.messages
|
return s.messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// AwardPoints gives points to teamID in category.
|
// AwardPoints gives points to team in category.
|
||||||
// This doesn't attempt to ensure the teamID has been registered.
|
// This doesn't attempt to ensure the team ID has been registered.
|
||||||
// It first checks to make sure these are not duplicate points.
|
// It first checks to make sure these are not duplicate points.
|
||||||
// This is not a perfect check, you can trigger a race condition here.
|
// This is not a perfect check, you can trigger a race condition here.
|
||||||
// It's just a courtesy to the user.
|
// It's just a courtesy to the user.
|
||||||
// The update task makes sure we never have duplicate points in the log.
|
// The update task makes sure we never have duplicate points in the log.
|
||||||
func (s *State) AwardPoints(teamID, category string, points int) error {
|
func (s *State) AwardPoints(team Team, category string, points int) error {
|
||||||
return s.awardPointsAtTime(time.Now().Unix(), teamID, category, points)
|
return s.awardPointsAtTime(time.Now().Unix(), team, category, points)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) awardPointsAtTime(when int64, teamID string, category string, points int) error {
|
func (s *State) awardPointsAtTime(when int64, team Team, category string, points int) error {
|
||||||
a := award.T{
|
a := award.T{
|
||||||
When: when,
|
When: when,
|
||||||
TeamID: teamID,
|
TeamID: team.ID,
|
||||||
Category: category,
|
Category: category,
|
||||||
Points: points,
|
Points: points,
|
||||||
}
|
}
|
||||||
|
@ -322,7 +333,7 @@ func (s *State) maybeInitialize() {
|
||||||
if err := s.reopenEventLog(); err != nil {
|
if err := s.reopenEventLog(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
s.LogEvent("init", "", "", "", 0)
|
s.LogEvent("init", "", "", 0)
|
||||||
|
|
||||||
// Make sure various subdirectories exist
|
// Make sure various subdirectories exist
|
||||||
s.Mkdir("points.tmp", 0755)
|
s.Mkdir("points.tmp", 0755)
|
||||||
|
@ -381,12 +392,11 @@ func (s *State) maybeInitialize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogEvent writes to the event log
|
// LogEvent writes to the event log
|
||||||
func (s *State) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) {
|
func (s *State) LogEvent(event, teamID, cat string, points int, extra ...string) {
|
||||||
s.eventStream <- append(
|
s.eventStream <- append(
|
||||||
[]string{
|
[]string{
|
||||||
strconv.FormatInt(time.Now().Unix(), 10),
|
strconv.FormatInt(time.Now().Unix(), 10),
|
||||||
event,
|
event,
|
||||||
participantID,
|
|
||||||
teamID,
|
teamID,
|
||||||
cat,
|
cat,
|
||||||
strconv.Itoa(points),
|
strconv.Itoa(points),
|
||||||
|
@ -443,12 +453,12 @@ func (s *State) updateCaches() {
|
||||||
_, ismmfs := s.Fs.(*afero.MemMapFs) // Tests run so quickly that the time check isn't precise enough
|
_, ismmfs := s.Fs.(*afero.MemMapFs) // Tests run so quickly that the time check isn't precise enough
|
||||||
if fi, err := s.Fs.Stat("teams"); err != nil {
|
if fi, err := s.Fs.Stat("teams"); err != nil {
|
||||||
log.Printf("Getting modification time of teams directory: %v", err)
|
log.Printf("Getting modification time of teams directory: %v", err)
|
||||||
} else if ismmfs || s.teamNamesLastChange.Before(fi.ModTime()) {
|
} else if ismmfs || s.teamsLastChange.Before(fi.ModTime()) {
|
||||||
s.teamNamesLastChange = fi.ModTime()
|
s.teamsLastChange = fi.ModTime()
|
||||||
|
|
||||||
// The compiler recognizes this as an optimization case
|
// The compiler recognizes this as an optimization case
|
||||||
for k := range s.teamNames {
|
for k := range s.teams {
|
||||||
delete(s.teamNames, k)
|
delete(s.teams, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
teamsFs := afero.NewBasePathFs(s.Fs, "teams")
|
teamsFs := afero.NewBasePathFs(s.Fs, "teams")
|
||||||
|
@ -456,13 +466,31 @@ func (s *State) updateCaches() {
|
||||||
log.Printf("Reading team ids: %v", err)
|
log.Printf("Reading team ids: %v", err)
|
||||||
} else {
|
} else {
|
||||||
for _, dirent := range dirents {
|
for _, dirent := range dirents {
|
||||||
teamID := dirent.Name()
|
team := Team{
|
||||||
if teamNameBytes, err := afero.ReadFile(teamsFs, teamID); err != nil {
|
Name: "",
|
||||||
log.Printf("Reading team %s: %v", teamID, err)
|
ID: dirent.Name(),
|
||||||
} else {
|
|
||||||
teamName := strings.TrimSpace(string(teamNameBytes))
|
|
||||||
s.teamNames[teamID] = teamName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dereference links a few times before giving up
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
teamNameBytes, err := afero.ReadFile(teamsFs, team.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Reading team %s: %v", team.ID, err)
|
||||||
|
team.Name = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
team.Name = string(teamNameBytes)
|
||||||
|
if !strings.HasPrefix(team.Name, TeamLinkPrefix) {
|
||||||
|
team.Name = strings.TrimSpace(team.Name)
|
||||||
|
if team.Name == "" {
|
||||||
|
team.Name = "∅" // Empty set
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
team.ID = team.Name[len(TeamLinkPrefix):]
|
||||||
|
team.Name = "[Too Many Links]"
|
||||||
|
}
|
||||||
|
s.teams[team.ID] = team
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -520,11 +548,15 @@ func NewDevelState(sp StateProvider) *DevelState {
|
||||||
//
|
//
|
||||||
// If one's registered, it will use it.
|
// If one's registered, it will use it.
|
||||||
// Otherwise, it returns "<devel:$ID>"
|
// Otherwise, it returns "<devel:$ID>"
|
||||||
func (ds *DevelState) TeamName(teamID string) (string, error) {
|
func (ds *DevelState) TeamName(teamID string) (Team, error) {
|
||||||
if name, err := ds.StateProvider.TeamName(teamID); err == nil {
|
if team, err := ds.StateProvider.TeamName(teamID); err == nil {
|
||||||
return name, nil
|
return team, nil
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("«devel:%s»", teamID), nil
|
team := Team{
|
||||||
|
Name: fmt.Sprintf("«devel:%s»", teamID),
|
||||||
|
ID: teamID,
|
||||||
|
}
|
||||||
|
return team, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTeamName associates a team name with any teamID
|
// SetTeamName associates a team name with any teamID
|
||||||
|
|
|
@ -17,8 +17,16 @@ func NewTestState() *State {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func slurp(c chan bool) {
|
||||||
|
for range c {
|
||||||
|
// Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestState(t *testing.T) {
|
func TestState(t *testing.T) {
|
||||||
s := NewTestState()
|
s := NewTestState()
|
||||||
|
defer close(s.refreshNow)
|
||||||
|
go slurp(s.refreshNow)
|
||||||
|
|
||||||
mustExist := func(path string) {
|
mustExist := func(path string) {
|
||||||
_, err := s.Fs.Stat(path)
|
_, err := s.Fs.Stat(path)
|
||||||
|
@ -63,15 +71,19 @@ func TestState(t *testing.T) {
|
||||||
t.Errorf("Registering team a second time didn't fail")
|
t.Errorf("Registering team a second time didn't fail")
|
||||||
}
|
}
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if name, err := s.TeamName(teamID); err != nil {
|
|
||||||
|
team, err := s.TeamName(teamID)
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if name != teamName {
|
} else if teamName != team.Name {
|
||||||
t.Error("Incorrect team name:", name)
|
t.Errorf("Incorrect team name: %#v != %#v", teamName, team.Name)
|
||||||
|
} else if teamID != team.ID {
|
||||||
|
t.Error("Incorrect team ID", team.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
category := "poot"
|
category := "poot"
|
||||||
points := 3928
|
points := 3928
|
||||||
if err := s.AwardPoints(teamID, category, points); err != nil {
|
if err := s.AwardPoints(team, category, points); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
// Flex duplicate detection with different timestamp
|
// Flex duplicate detection with different timestamp
|
||||||
|
@ -82,7 +94,7 @@ func TestState(t *testing.T) {
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.AwardPoints(teamID, category, points)
|
s.AwardPoints(team, category, points)
|
||||||
s.refresh()
|
s.refresh()
|
||||||
pl = s.PointsLog()
|
pl = s.PointsLog()
|
||||||
if len(pl) != 1 {
|
if len(pl) != 1 {
|
||||||
|
@ -94,11 +106,11 @@ func TestState(t *testing.T) {
|
||||||
t.Errorf("Incorrect logged award %v", pl)
|
t.Errorf("Incorrect logged award %v", pl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.AwardPoints(teamID, category, points); err == nil {
|
if err := s.AwardPoints(team, category, points); err == nil {
|
||||||
t.Error("Duplicate points award after refresh didn't fail")
|
t.Error("Duplicate points award after refresh didn't fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.AwardPoints(teamID, category, points+1); err != nil {
|
if err := s.AwardPoints(team, category, points+1); err != nil {
|
||||||
t.Error("Awarding more points:", err)
|
t.Error("Awarding more points:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +124,7 @@ func TestState(t *testing.T) {
|
||||||
if len(s.PointsLog()) != 0 {
|
if len(s.PointsLog()) != 0 {
|
||||||
t.Errorf("Intentional parse error breaks pointslog")
|
t.Errorf("Intentional parse error breaks pointslog")
|
||||||
}
|
}
|
||||||
if err := s.AwardPoints(teamID, category, points); err != nil {
|
if err := s.AwardPoints(team, category, points); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
s.refresh()
|
s.refresh()
|
||||||
|
@ -139,10 +151,10 @@ func TestStateOutOfOrderAward(t *testing.T) {
|
||||||
points := 100
|
points := 100
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if err := s.awardPointsAtTime(now+20, "AA", category, points); err != nil {
|
if err := s.awardPointsAtTime(now+20, Team{ID: "AA"}, category, points); err != nil {
|
||||||
t.Error("Awarding points to team ZZ:", err)
|
t.Error("Awarding points to team ZZ:", err)
|
||||||
}
|
}
|
||||||
if err := s.awardPointsAtTime(now+10, "ZZ", category, points); err != nil {
|
if err := s.awardPointsAtTime(now+10, Team{ID: "ZZ"}, category, points); err != nil {
|
||||||
t.Error("Awarding points to team AA:", err)
|
t.Error("Awarding points to team AA:", err)
|
||||||
}
|
}
|
||||||
s.refresh()
|
s.refresh()
|
||||||
|
@ -157,16 +169,16 @@ func TestStateOutOfOrderAward(t *testing.T) {
|
||||||
|
|
||||||
func TestStateEvents(t *testing.T) {
|
func TestStateEvents(t *testing.T) {
|
||||||
s := NewTestState()
|
s := NewTestState()
|
||||||
s.LogEvent("moo", "", "", "", 0)
|
s.LogEvent("moo", "", "", 0)
|
||||||
s.LogEvent("moo 2", "", "", "", 0)
|
s.LogEvent("moo 2", "", "", 0)
|
||||||
|
|
||||||
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init::::0" {
|
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "init:::0" {
|
||||||
t.Error("Wrong message from event stream:", msg)
|
t.Error("Wrong message from event stream:", msg)
|
||||||
}
|
}
|
||||||
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo::::0" {
|
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo:::0" {
|
||||||
t.Error("Wrong message from event stream:", msg)
|
t.Error("Wrong message from event stream:", msg)
|
||||||
}
|
}
|
||||||
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo 2::::0" {
|
if msg := <-s.eventStream; strings.Join(msg[1:], ":") != "moo 2:::0" {
|
||||||
t.Error("Wrong message from event stream:", msg)
|
t.Error("Wrong message from event stream:", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,7 +278,7 @@ func TestStateMaintainer(t *testing.T) {
|
||||||
t.Error("Team ID too short:", teamID)
|
t.Error("Team ID too short:", teamID)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.LogEvent("Hello!", "", "", "", 0)
|
s.LogEvent("Hello!", "", "", 0)
|
||||||
|
|
||||||
if len(s.PointsLog()) != 0 {
|
if len(s.PointsLog()) != 0 {
|
||||||
t.Error("Points log is not empty")
|
t.Error("Points log is not empty")
|
||||||
|
@ -274,7 +286,7 @@ func TestStateMaintainer(t *testing.T) {
|
||||||
if err := s.SetTeamName(teamID, "The Patricks"); err != nil {
|
if err := s.SetTeamName(teamID, "The Patricks"); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if err := s.AwardPoints(teamID, "pategory", 31337); err != nil {
|
if err := s.AwardPoints(Team{ID: teamID}, "pategory", 31337); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
time.Sleep(updateInterval)
|
time.Sleep(updateInterval)
|
||||||
|
@ -307,13 +319,14 @@ func TestDevelState(t *testing.T) {
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
t.Error("Registering a team that doesn't exist didn't return ErrAlreadyRegistered")
|
t.Error("Registering a team that doesn't exist didn't return ErrAlreadyRegistered")
|
||||||
}
|
}
|
||||||
if n, err := ds.TeamName("boog"); err != nil {
|
team, err := ds.TeamName("boog")
|
||||||
|
if err != nil {
|
||||||
t.Error("Devel State returned error on team name lookup")
|
t.Error("Devel State returned error on team name lookup")
|
||||||
} else if n != "«devel:boog»" {
|
} else if team.Name != "«devel:boog»" {
|
||||||
t.Error("Wrong team name", n)
|
t.Error("Wrong team name", team.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ds.AwardPoints("blerg", "dog", 82); err != nil {
|
if err := ds.AwardPoints(team, "dog", 82); err != nil {
|
||||||
t.Error("Devel State AwardPoints returned an error", err)
|
t.Error("Devel State AwardPoints returned an error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="login">
|
<form id="login">
|
||||||
<!--
|
|
||||||
<span id="pid">
|
|
||||||
Participant ID: <input name="pid"> (optional) <br>
|
|
||||||
</span>
|
|
||||||
-->
|
|
||||||
Team ID: <input name="id"> <br>
|
Team ID: <input name="id"> <br>
|
||||||
Team name: <input name="name"> <br>
|
Team name: <input name="name"> <br>
|
||||||
<input type="submit" value="Sign In">
|
<input type="submit" value="Sign In">
|
||||||
|
|
|
@ -83,7 +83,6 @@ function renderPuzzles(obj) {
|
||||||
let url = new URL("puzzle.html", window.location)
|
let url = new URL("puzzle.html", window.location)
|
||||||
url.searchParams.set("cat", cat)
|
url.searchParams.set("cat", cat)
|
||||||
url.searchParams.set("points", points)
|
url.searchParams.set("points", points)
|
||||||
if (id) { url.searchParams.set("pid", id) }
|
|
||||||
a.href = url.toString()
|
a.href = url.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +104,6 @@ function renderState(obj) {
|
||||||
if (devel) {
|
if (devel) {
|
||||||
let params = new URLSearchParams(window.location.search)
|
let params = new URLSearchParams(window.location.search)
|
||||||
sessionStorage.id = "1"
|
sessionStorage.id = "1"
|
||||||
sessionStorage.pid = "rodney"
|
|
||||||
renderPuzzles(obj.Puzzles)
|
renderPuzzles(obj.Puzzles)
|
||||||
} else if (Object.keys(obj.Puzzles).length > 0) {
|
} else if (Object.keys(obj.Puzzles).length > 0) {
|
||||||
renderPuzzles(obj.Puzzles)
|
renderPuzzles(obj.Puzzles)
|
||||||
|
@ -115,12 +113,8 @@ function renderState(obj) {
|
||||||
|
|
||||||
function heartbeat() {
|
function heartbeat() {
|
||||||
let teamId = sessionStorage.id || ""
|
let teamId = sessionStorage.id || ""
|
||||||
let participantId = sessionStorage.pid
|
|
||||||
let url = new URL("state", window.location)
|
let url = new URL("state", window.location)
|
||||||
url.searchParams.set("id", teamId)
|
url.searchParams.set("id", teamId)
|
||||||
if (participantId) {
|
|
||||||
url.searchParams.set("pid", participantId)
|
|
||||||
}
|
|
||||||
let fd = new FormData()
|
let fd = new FormData()
|
||||||
fd.append("id", teamId)
|
fd.append("id", teamId)
|
||||||
fetch(url)
|
fetch(url)
|
||||||
|
@ -152,8 +146,6 @@ function login(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
let name = document.querySelector("[name=name]").value
|
let name = document.querySelector("[name=name]").value
|
||||||
let teamId = document.querySelector("[name=id]").value
|
let teamId = document.querySelector("[name=id]").value
|
||||||
let pide = document.querySelector("[name=pid]")
|
|
||||||
let participantId = pide?pide.value:Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
|
|
||||||
|
|
||||||
fetch("register", {
|
fetch("register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -166,7 +158,6 @@ function login(e) {
|
||||||
if ((obj.status == "success") || (obj.data.short == "Already registered")) {
|
if ((obj.status == "success") || (obj.data.short == "Already registered")) {
|
||||||
toast("Logged in")
|
toast("Logged in")
|
||||||
sessionStorage.id = teamId
|
sessionStorage.id = teamId
|
||||||
sessionStorage.pid = participantId
|
|
||||||
showPuzzles()
|
showPuzzles()
|
||||||
heartbeat()
|
heartbeat()
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue