From 3e0754335d915f89f30b65c458522a1765815195 Mon Sep 17 00:00:00 2001
From: RandomChars <random@chars.jp>
Date: Sat, 18 Dec 2021 18:48:02 +0900
Subject: [PATCH] safe concurrent access, destroy team, export as CSV

---
 assets/templates/admin.tmpl |  2 +
 routes.go                   | 30 +++++++++++++++
 tournament.go               | 56 +++++++++++++++++++++++++++
 user.go                     | 75 +++++++++++++++++++++++++++++++++++--
 4 files changed, 159 insertions(+), 4 deletions(-)

diff --git a/assets/templates/admin.tmpl b/assets/templates/admin.tmpl
index ad90885..331cc75 100644
--- a/assets/templates/admin.tmpl
+++ b/assets/templates/admin.tmpl
@@ -23,6 +23,8 @@
                                             </li>
                                             <li>
                                                 <div class="my-match-info">
+                                                    <a class="live-btn" type="submit" style="cursor: default"
+                                                       href="/admin/{{.ID}}/csv">Export</a>
                                                     <form id="destroy-{{.ID}}" method="post" action="/admin/{{.ID}}">
                                                         <input type="hidden" name="Delete" value="true">
                                                         <a class="live-btn" type="submit" style="cursor: default"
diff --git a/routes.go b/routes.go
index bc09d6d..88a7d68 100644
--- a/routes.go
+++ b/routes.go
@@ -257,6 +257,36 @@ func registerRoutes() {
 		})
 	})
 
+	router.GET("/admin/:id/csv", func(context *gin.Context) {
+		user := oauth.GetSelf(context)
+		if user == nil {
+			context.Redirect(http.StatusTemporaryRedirect, "/auth/login")
+			return
+		}
+
+		if !administrators[user.ID] {
+			context.Redirect(http.StatusTemporaryRedirect, "/")
+			return
+		}
+
+		t := contextTournament(context)
+		if t == nil {
+			return
+		}
+
+		if c, err := csvTournament(t.ID); err != nil {
+			log.Printf("error rendering CSV for tournament %s: %s", t.ID.String(), err)
+			context.String(http.StatusInternalServerError, "Internal Server Error")
+			return
+		} else {
+			context.Writer.WriteHeader(http.StatusOK)
+			context.Header("Content-Disposition", "attachment; filename="+t.Attributes["title"]+".csv")
+			context.Header("Content-Type", "application/x-download")
+			context.Header("Content-Length", strconv.Itoa(len(c)))
+			_, _ = context.Writer.Write(c)
+		}
+	})
+
 	router.POST("/admin/:id", func(context *gin.Context) {
 		user := oauth.GetSelf(context)
 		if user == nil {
diff --git a/tournament.go b/tournament.go
index 34e5f4f..78ccf86 100644
--- a/tournament.go
+++ b/tournament.go
@@ -1,6 +1,9 @@
 package main
 
 import (
+	"bytes"
+	"encoding/csv"
+	"fmt"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
 	json "github.com/json-iterator/go"
@@ -289,3 +292,56 @@ func validatePayloadTournament(payload *tournamentPayload) bool {
 		payload.StartTime().After(payload.Deadline()) &&
 		payload.Deadline().After(time.Now())
 }
+
+func csvTournament(t uuid.UUID) ([]byte, error) {
+	var captains []string
+	if cs, err := getTournamentTeam(t); err != nil {
+		return nil, err
+	} else {
+		captains = cs
+	}
+
+	if len(captains) == 0 {
+		return []byte{}, nil
+	}
+
+	teams := make([]*teamPayload, len(captains))
+	for i, captain := range captains {
+		if team, err := getTeam(t, captain); err != nil {
+			return nil, err
+		} else {
+			teams[i] = team
+		}
+	}
+
+	width := 5 + len(teams[0].Players)
+	records := make([][]string, len(teams)+1)
+	records[0] = make([]string, width)
+	records[0][0] = "ID"
+	records[0][1] = "Team Name"
+	records[0][2] = "Submitter Discord ID"
+	records[0][3] = "Submitter First Name"
+	records[0][4] = "Submitter Last Name"
+	for i := 3; i < width; i++ {
+		records[0][i] = fmt.Sprintf("Member %d", i-2)
+	}
+
+	for i, team := range teams {
+		records[i+1] = make([]string, width)
+		records[i+1][0] = strconv.Itoa(i + 1)
+		records[i+1][1] = team.Name
+		records[i+1][2] = team.Captain
+		records[i+1][3] = team.SubmitterFirstName
+		records[i+1][4] = team.SubmitterLastName
+
+		for j, player := range team.Players {
+			records[i+1][j+5] = player
+		}
+	}
+
+	buf := bytes.NewBuffer(nil)
+	if err := csv.NewWriter(buf).WriteAll(records); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
diff --git a/user.go b/user.go
index 774424b..5e042bc 100644
--- a/user.go
+++ b/user.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	"github.com/bwmarrin/discordgo"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
@@ -8,6 +9,7 @@ import (
 	"github.com/syndtr/goleveldb/leveldb"
 	"log"
 	"net/http"
+	"sync"
 )
 
 type userEnrollment struct {
@@ -30,6 +32,8 @@ type teamPayload struct {
 	Tournament         uuid.UUID `json:"tournament"`
 }
 
+var userLock = sync.RWMutex{}
+
 func putTeam(t *teamPayload) error {
 	var b []byte
 	if p, err := getBytesTournament(t.Tournament); err != nil {
@@ -38,6 +42,9 @@ func putTeam(t *teamPayload) error {
 		b = p
 	}
 
+	userLock.Lock()
+	defer userLock.Unlock()
+
 	var enrollments []string
 	if data, err := userDB.Get(b, nil); err != nil && !isNotFound(err) {
 		return err
@@ -57,7 +64,7 @@ func putTeam(t *teamPayload) error {
 		}
 	}
 
-	var tournaments = make(map[string]*teamPayload)
+	var tournaments = make(map[uuid.UUID]*teamPayload)
 	if data, err := userDB.Get([]byte(t.Captain), nil); err != nil && !isNotFound(err) {
 		return err
 	} else {
@@ -66,7 +73,7 @@ func putTeam(t *teamPayload) error {
 				return err
 			}
 		}
-		tournaments[string(b)] = t
+		tournaments[t.Tournament] = t
 		if data, err = json.Marshal(tournaments); err != nil {
 			return err
 		} else {
@@ -79,6 +86,64 @@ func putTeam(t *teamPayload) error {
 	return nil
 }
 
+func destroyTeam(t uuid.UUID, captain string) error {
+	var b []byte
+	if p, err := getBytesTournament(t); err != nil {
+		return err
+	} else {
+		b = p
+	}
+
+	userLock.Lock()
+	defer userLock.Unlock()
+
+	var enrollments []string
+	if data, err := userDB.Get(b, nil); err != nil {
+		return err
+	} else {
+		if data != nil {
+			if err = json.Unmarshal(data, &enrollments); err != nil {
+				return err
+			}
+		}
+		for i, c := range enrollments {
+			if c == captain {
+				enrollments[i] = enrollments[len(enrollments)-1]
+				enrollments = enrollments[:len(enrollments)-1]
+				break
+			}
+		}
+		if data, err = json.Marshal(enrollments); err != nil {
+			return err
+		} else {
+			if err = userDB.Put(b, data, nil); err != nil {
+				return err
+			}
+		}
+	}
+
+	var tournaments = make(map[uuid.UUID]*teamPayload)
+	if data, err := userDB.Get([]byte(captain), nil); err != nil {
+		return err
+	} else {
+		if data != nil {
+			if err = json.Unmarshal(data, &tournaments); err != nil {
+				return err
+			}
+		}
+		delete(tournaments, t)
+		if data, err = json.Marshal(tournaments); err != nil {
+			return err
+		} else {
+			if err = userDB.Put([]byte(captain), data, nil); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
 func existsTeam(t uuid.UUID, captain string) (bool, error) {
 	if teams, err := getTournamentTeam(t); err != nil {
 		if isNotFound(err) {
@@ -103,7 +168,7 @@ func getTeam(t uuid.UUID, captain string) (*teamPayload, error) {
 		b = p
 	}
 
-	var payload = make(map[string]*teamPayload)
+	var payload = make(map[uuid.UUID]*teamPayload)
 	if data, err := userDB.Get([]byte(captain), nil); err != nil && !isNotFound(err) {
 		return nil, err
 	} else {
@@ -112,7 +177,9 @@ func getTeam(t uuid.UUID, captain string) (*teamPayload, error) {
 				return nil, err
 			}
 		}
-		if p, ok := payload[string(b)]; !ok {
+		if p, ok := payload[t]; !ok {
+			fmt.Println(payload)
+			fmt.Println(string(b))
 			return nil, leveldb.ErrNotFound
 		} else {
 			return p, nil
-- 
GitLab