This repository has been archived on 2022-08-15. You can view files and clone it, but cannot push or open issues or pull requests.
shemu/main.go

324 lines
9.0 KiB
Go

package main
import (
"bufio"
"encoding/csv"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
)
var gdprExceptions arrayFlags
var students [][]string
var rendusDir string
var title string
func Serve(w http.ResponseWriter, r *http.Request) {
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html>
<html>
<head>
<title>` + title + `</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<style>.table > tbody > tr > th, .table > tbody > tr > td { vertical-align: middle; }
td.danger { text-align: center; }
.hash { max-width: 150px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; display: inline-block; }
</style>
<script type="text/javascript">
function disp(rendus) {
Object.keys(rendus).map(function(login) {
student = rendus[login];
var row = document.createElement("tr");
var col = document.createElement("th");
col.id = login;
col.innerHTML = login;
row.appendChild(col);
Object.keys(student).map(function(wn) {
work = student[wn];
if (work) {
var col = document.createElement("td");
col.className = "success";
col.innerHTML = (new Intl.DateTimeFormat('default', { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(new Date(work["date"]))) + '<br><span class="hash" title="' + work["hash"] + '">' + work["hash"] + '</span>';
row.appendChild(col);
} else {
var col = document.createElement("td");
col.className = "danger";
col.innerHTML = "<span class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span>";
row.appendChild(col);
}
});
document.getElementById("students").appendChild(row);
});
Object.keys(student).map(function(wn) {
var col = document.createElement("th");
col.innerHTML = wn;
document.getElementById("head").appendChild(col);
});
}
function disp_std(rendus, login) {
Object.keys(rendus).map(function(label) {
var work = rendus[label];
var row = document.createElement("tr");
var col = document.createElement("th");
col.innerHTML = label;
row.appendChild(col);
if (work) {
var col = document.createElement("td");
col.className = "success";
col.innerHTML = (new Intl.DateTimeFormat('default', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(new Date(work["date"]))) + '<br>' + work["hash"];
row.appendChild(col);
} else {
var col = document.createElement("td");
col.className = "danger";
col.innerHTML = "<span class=\"glyphicon glyphicon-remove\" aria-hidden=\"true\"></span>";
row.appendChild(col);
}
document.getElementById("students").appendChild(row);
});
var col = document.createElement("th");
col.innerHTML = login;
document.getElementById("head").appendChild(col);
}
</script>
</head>
<body class="container">
<h1>` + title + `</h1>
<table class="table table-striped table-hover table-condensed">
<thead>
<tr id="head"><th></th></tr>
</thead>
<tbody id="students">
</tbody>
</table>
<script type="text/javascript">
if (window.location.pathname[window.location.pathname.length - 1] != "/")
fetch(window.location.pathname.replace(".html", "") + ".json")
.then(function(response) {
return response.json();
})
.then(function(submissions) {
var spl = window.location.pathname.split("/")
disp_std(submissions, spl[spl.length - 1]);
});
else
fetch('rendus.json') // You can also fetch login_x.json
.then(function(response) {
return response.json();
})
.then(function(submissions) {
disp(submissions);
});
</script>
</body>
</html>
`))
}
func genStudents() map[string]map[string]*Submission {
ret := map[string]map[string]*Submission{}
for _, student := range students {
login := student[2]
if gdprExceptions.Contains(login) {
continue
}
ret[login] = genStudent(login)
}
return ret
}
func ServeJSON(w http.ResponseWriter, r *http.Request) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
}
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
if j, err := json.Marshal(genStudents()); err != nil {
http.Error(w, fmt.Sprintf("{errmsg:%q}", err), http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
w.Write(j)
}
}
type Submission struct {
Date time.Time `json:"date"`
Hash string `json:"hash"`
}
func genStudent(login string) map[string]*Submission {
ret := map[string]*Submission{}
if dirs, err := ioutil.ReadDir(rendusDir); err == nil {
for _, dir := range dirs {
if dir.IsDir() {
p := path.Join(rendusDir, dir.Name(), login)
if fi, err := os.Stat(p); err == nil {
ret[dir.Name()] = new(Submission)
ret[dir.Name()].Date = fi.ModTime()
if lnk, err := os.Readlink(p); err == nil {
ret[dir.Name()].Hash = strings.TrimPrefix(lnk, login+".")
}
} else {
ret[dir.Name()] = nil
}
}
}
}
return ret
}
func ServeJSONStudent(w http.ResponseWriter, r *http.Request) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
}
log.Printf("Handling %s request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
login := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".json")
var ret []byte
status := http.StatusOK
if len(r.URL.Query().Get("rendu")) > 0 {
limit := r.URL.Query().Get("rendu")
for k, v := range genStudent(login) {
if limit == k {
r, err := json.Marshal(v)
if err != nil {
http.Error(w, fmt.Sprintf("{errmsg:%q}", err), http.StatusInternalServerError)
return
}
if v == nil {
status = http.StatusNotFound
}
ret = append(ret, r...)
}
}
} else {
var err error
ret, err = json.Marshal(genStudent(login))
if err != nil {
http.Error(w, fmt.Sprintf("{errmsg:%q}", err), http.StatusInternalServerError)
return
}
}
w.WriteHeader(status)
w.Write(ret)
}
type arrayFlags []string
func (i *arrayFlags) String() string {
return fmt.Sprintf("%v", *i)
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}
func (i *arrayFlags) Contains(value string) bool {
for _, v := range *i {
if v == value {
return true
}
}
return false
}
func main() {
var studentsFiles arrayFlags
var bind = flag.String("bind", "0.0.0.0:8081", "Bind port/socket")
flag.Var(&studentsFiles, "students", "Path to a CSV file containing in the third column the name of the directory to look for (can be repeated multiple times)")
flag.Var(&gdprExceptions, "gdpr-exception", "Login that should not be listed in global rendus.json (can be repeated multiple times)")
flag.StringVar(&rendusDir, "path", "./rendu/", "Path to the submissions directory (each subdirectory is a project)")
flag.StringVar(&title, "title", "Rendus VIRLI", "Title of the page")
flag.Parse()
var err error
if rendusDir, err = filepath.Abs(rendusDir); err != nil {
log.Fatal(err)
} else if dirs, err := ioutil.ReadDir(rendusDir); err != nil {
log.Fatal(err)
} else {
var projects []string
nbProj := 0
for _, dir := range dirs {
if dir.IsDir() {
projects = append(projects, dir.Name())
nbProj++
}
}
log.Println(nbProj, "projects found:", projects)
}
// Read and parse students list
log.Println("Reading students files...")
for _, studentsFile := range studentsFiles {
log.Println("Reading", studentsFile, "...")
if studentsFile, err = filepath.Abs(studentsFile); err != nil {
log.Fatal(err)
} else if fi, err := os.Open(studentsFile); err != nil {
log.Fatal(err)
} else {
var nstudents [][]string
r := csv.NewReader(bufio.NewReader(fi))
if nstudents, err = r.ReadAll(); err != nil {
log.Fatal(err)
}
students = append(students, nstudents...)
}
}
log.Println(len(students), "students loaded.")
log.Println("Registering handlers...")
http.HandleFunc("/", Serve)
http.HandleFunc("/rendus.json", ServeJSON)
for _, student := range students {
http.HandleFunc("/"+student[2]+".json", ServeJSONStudent)
}
log.Println(fmt.Sprintf("Ready, listening on %s", *bind))
if err := http.ListenAndServe(*bind, nil); err != nil {
log.Fatal("Unable to listen and serve: ", err)
}
}