Compare commits

...

9 Commits

11 changed files with 210 additions and 87 deletions

View File

@ -1,8 +1,11 @@
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/drone/drone-go/drone"
"github.com/gin-gonic/gin"
@ -30,3 +33,74 @@ func declareAPIAdminGradationRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, result)
})
}
type TestsWebhook struct {
Login string `json:"login"`
RepositoryId int `json:"repository_id"`
BuildNumber int `json:"build_number"`
Steps map[string]float64 `json:"steps,omitempty"`
}
func (r *Repository) fetchRepoTests(build_number int, steps map[string]float64) error {
tmp := strings.Split(r.TestsRef, "/")
if len(tmp) < 3 {
return fmt.Errorf("This repository tests reference is not filled properly.")
}
work, err := getWork(r.IdWork)
if err != nil {
return fmt.Errorf("Unable to retrieve the related work: %w", err)
}
client := drone.NewClient(droneEndpoint, droneConfig)
result, err := client.Build(tmp[0], tmp[1], build_number)
if err != nil {
return fmt.Errorf("Unable to find the referenced build (%d): %w", build_number, err)
}
if result.Finished > 0 {
return fmt.Errorf("The test phase is not finished")
}
var grade float64
for _, stage := range result.Stages {
for _, step := range stage.Steps {
if g, ok := steps[fmt.Sprintf("%d", step.Number)]; ok {
log.Printf("Step %q (%d) in status %q", step.Name, step.Number, step.Status)
// Give the point if it succeed
if step.Status == "success" {
grade += g
}
continue
}
logs, err := client.Logs(tmp[0], tmp[1], build_number, stage.Number, step.Number)
if err != nil {
log.Printf("Unable to retrieve build logs %s/%s/%d/%d/%d: %s", tmp[0], tmp[1], build_number, stage.Number, step.Number, err.Error())
continue
}
if len(logs) < 1 {
continue
}
line := logs[len(logs)-1]
if strings.HasPrefix(line.Message, "grade:") {
g, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(line.Message, "grade:")), 64)
if err == nil {
grade += g
} else {
log.Println("Unable to parse grade:", err.Error())
}
}
}
}
work.AddGrade(WorkGrade{
IdUser: r.IdUser,
IdWork: work.Id,
Grade: grade,
})
return nil
}

View File

@ -334,12 +334,6 @@ type GitLabWebhook struct {
Repository GitLabRepository
}
type TestsWebhook struct {
Login string `json:"login"`
RepositoryId int `json:"repository_id"`
BuildNumber int `json:"build_number"`
}
func declareCallbacksRoutes(router *gin.RouterGroup) {
router.POST("/callbacks/trigger.json", func(c *gin.Context) {
// Check event type
@ -446,7 +440,7 @@ func declareCallbacksRoutes(router *gin.RouterGroup) {
return
}
err = repo.fetchRepoTests(hook.BuildNumber)
err = repo.fetchRepoTests(hook.BuildNumber, hook.Steps)
if err != nil {
log.Printf("Unable to fetchRepoTests(%d): %s", hook.RepositoryId, err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to fetch tests results."})
@ -576,7 +570,7 @@ func TriggerTests(c *gin.Context, work *Work, repo *Repository, u *User) {
Key: aws.String(filepath.Join(fmt.Sprintf("%d", work.Id), fmt.Sprintf("rendu-%s.tar.xz", login))),
})
url, err := req.Presign(SharingTime)
url, err := req.Presign(SharingTime * 20)
if err != nil {
log.Println("Unable to create presign URL:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Something goes wrong when creating the presigned URL."})
@ -609,6 +603,42 @@ func TriggerTests(c *gin.Context, work *Work, repo *Repository, u *User) {
c.JSON(http.StatusOK, repo)
}
func (w *Work) stopTests() error {
repos, err := w.GetRepositories()
if err != nil {
return err
}
client := drone.NewClient(droneEndpoint, droneConfig)
for _, repo := range repos {
slug := strings.Split(repo.TestsRef, "/")
if len(slug) < 3 {
continue
}
buildn, err := strconv.ParseInt(slug[2], 10, 32)
if err != nil {
continue
}
build, err := client.Build(slug[0], slug[1], int(buildn))
if err != nil {
log.Println("Unable to communicate with Drone:", err.Error())
continue
}
if build.Status == "pending" {
err := client.BuildCancel(slug[0], slug[1], int(buildn))
if err != nil {
log.Println("Unable to cancel the build:", err.Error())
continue
}
}
}
return nil
}
type Repository struct {
Id int64 `json:"id"`
IdUser int64 `json:"id_user"`
@ -643,6 +673,27 @@ func (u *User) GetRepositories() (repositories []*Repository, err error) {
}
}
func (w *Work) GetRepositories() (repositories []*Repository, err error) {
if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, secret, last_check, droneref, last_tests, testsref FROM user_work_repositories WHERE id_work=?", w.Id); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var repo Repository
if err = rows.Scan(&repo.Id, &repo.IdUser, &repo.IdWork, &repo.URI, &repo.Secret, &repo.LastCheck, &repo.DroneRef, &repo.LastTests, &repo.TestsRef); err != nil {
return
}
repositories = append(repositories, &repo)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func getRepositoriesByURI(uri string) (repositories []*Repository, err error) {
if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, secret, last_check, droneref, last_tests, testsref FROM user_work_repositories WHERE uri=?", uri); errr != nil {
return nil, errr
@ -689,7 +740,7 @@ func (u *User) NewRepository(w *Work, uri string) (*Repository, error) {
return nil, err
}
if res, err := DBExec("INSERT INTO user_work_repositories (id_user, id_work, uri, secret, droneref) VALUES (?, ?, ?, ?, ?)", u.Id, w.Id, uri, secret, ""); err != nil {
if res, err := DBExec("INSERT INTO user_work_repositories (id_user, id_work, uri, secret, droneref, testsref) VALUES (?, ?, ?, ?, ?, '')", u.Id, w.Id, uri, secret, ""); err != nil {
return nil, err
} else if rid, err := res.LastInsertId(); err != nil {
return nil, err
@ -716,58 +767,6 @@ func (r Repository) Delete() (int64, error) {
}
}
func (r *Repository) fetchRepoTests(build_number int) error {
tmp := strings.Split(r.TestsRef, "/")
if len(tmp) < 3 {
return fmt.Errorf("This repository tests reference is not filled properly.")
}
work, err := getWork(r.IdWork)
if err != nil {
return fmt.Errorf("Unable to retrieve the related work: %w", err)
}
client := drone.NewClient(droneEndpoint, droneConfig)
result, err := client.Build(tmp[0], tmp[1], build_number)
if err != nil {
return fmt.Errorf("Unable to find the referenced build (%d): %w", build_number, err)
}
if result.Finished > 0 {
return fmt.Errorf("The test phase is not finished")
}
var grade float64
for _, stage := range result.Stages {
for _, step := range stage.Steps {
logs, err := client.Logs(tmp[0], tmp[1], build_number, stage.Number, step.Number)
if err != nil {
log.Printf("Unable to retrieve build logs %s/%s/%d/%d/%d: %s", tmp[0], tmp[1], build_number, stage.Number, step.Number, err.Error())
continue
}
for _, line := range logs {
if strings.HasPrefix(line.Message, "grade:") {
g, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(line.Message, "grade:")), 64)
if err == nil {
grade += g
} else {
log.Println("Unable to parse grade:", err.Error())
}
}
}
}
}
work.AddGrade(WorkGrade{
IdUser: r.IdUser,
IdWork: work.Id,
Grade: grade,
})
return nil
}
func ClearRepositories() (int64, error) {
if res, err := DBExec("DELETE FROM user_work_repositories"); err != nil {
return 0, err

View File

@ -17,7 +17,7 @@ import (
"github.com/gin-gonic/gin"
)
const SharingTime = 15 * time.Minute
const SharingTime = 10 * time.Minute
var (
s3_endpoint string

View File

@ -0,0 +1,14 @@
<script>
export let score = 0;
</script>
<span
class="badge"
class:bg-success={score >= 18}
class:bg-info={score < 18 && score >= 15}
class:bg-warning={score < 15 && score >= 9}
class:bg-danger={score < 9}
class:bg-dark={score == "N/A"}
>
{score}
</span>

View File

@ -4,6 +4,7 @@
import { user } from '$lib/stores/user';
import DateFormat from '$lib/components/DateFormat.svelte';
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
import ScoreBadge from '$lib/components/ScoreBadge.svelte';
import SubmissionStatus from '$lib/components/SubmissionStatus.svelte';
import { getCategories } from '$lib/categories';
import { getSurveys } from '$lib/surveys';
@ -144,16 +145,7 @@
{/if}
</span>
{:else}
<span
class="badge"
class:bg-success={score.score >= 18}
class:bg-info={score.score < 18 && score.score >= 15}
class:bg-warning={score.score < 15 && score.score >= 9}
class:bg-danger={score.score < 9}
class:bg-dark={score.score == "N/A"}
>
{score.score}
</span>
<ScoreBadge score={score.score} />
{/if}
{:catch error}
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>

View File

@ -160,7 +160,7 @@
</div>
{:then rrepos}
<select id="repolist" class="form-select col" disabled={readonly} bind:value={repo_used.uri}>
{#each rrepos as r (r.Id)}
{#each rrepos as r (r.ssh_url_to_repo)}
<option value={r.ssh_url_to_repo}>{r.path_with_namespace}</option>
{/each}
</select>

View File

@ -95,6 +95,20 @@ export class Work {
}
}
async stopTests() {
if (this.id) {
const res = await fetch(`api/works/${this.id}/tests`, {
method: 'DELETE',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
async getSubmission(uid) {
const res = await fetch(uid?`api/users/${uid}/works/${this.id}/submission`:`api/works/${this.id}/submission`, {
headers: {'Accept': 'application/json'}

View File

@ -3,6 +3,7 @@
import { user } from '$lib/stores/user';
import DateFormat from '$lib/components/DateFormat.svelte';
import ScoreBadge from '$lib/components/ScoreBadge.svelte';
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
import SubmissionStatus from '$lib/components/SubmissionStatus.svelte';
import { getCategories } from '$lib/categories';
@ -120,16 +121,7 @@
{#await getScore(work)}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then score}
<span
class="badge"
class:bg-success={score.score >= 18}
class:bg-info={score.score < 18 && score.score >= 15}
class:bg-warning={score.score < 15 && score.score >= 9}
class:bg-danger={score.score < 9}
class:bg-dark={score.score == "N/A"}
>
{score.score}
</span>
<ScoreBadge score={score.score} />
{:catch error}
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
{/await}

View File

@ -3,6 +3,7 @@
import { user } from '$lib/stores/user';
import DateFormat from '$lib/components/DateFormat.svelte';
import ScoreBadge from '$lib/components/ScoreBadge.svelte';
import SubmissionStatus from '$lib/components/SubmissionStatus.svelte';
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
import WorkAdmin from '$lib/components/WorkAdmin.svelte';
@ -60,7 +61,15 @@
{/if}
<hr>
<h3 class="mt-3">Notes</h3>
<div class="d-flex justify-content-between align-items-center">
<h3 class="mt-3">Notes</h3>
<button
class="btn btn-light"
on:click={() => refresh_grades(w)}
>
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="card mt-3 mb-5">
{#await gradesP}
<div class="text-center mb-5">
@ -91,7 +100,7 @@
<td>
<a href="users/{grade.id_user}">{grade.login}</a>
</td>
<td>{grade.score}</td>
<td><ScoreBadge score={grade.score} /></td>
<td>{#if grade.comment}{grade.comment}{:else}-{/if}</td>
<td>{grade.date}</td>
<td>

View File

@ -32,16 +32,21 @@
repositoriesP[userid] = getRepositories(wid, userid);
}
let gradation_preflight = false;
async function runGradations() {
gradation_preflight = true;
for (const user of await usersP) {
if (repositoriesP[user.id]) {
try {
for (const repo of await repositoriesP[user.id]) {
repo.runGradation();
}
} catch {}
} catch (err) {
ToastsStore.addToast({color: "danger", title: "Impossible de lancer la notation de " + user.login, msg: err})
}
}
}
gradation_preflight = false;
}
let nb_rendus = 0;
@ -66,8 +71,20 @@
class="btn btn-sm btn-success mr-1"
title="Relancer les tests"
on:click={runGradations}
disabled={gradation_preflight}
>
<i class="bi bi-play"></i>
{#if gradation_preflight}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:else}
<i class="bi bi-play"></i>
{/if}
</button>
<button
class="btn btn-sm btn-danger mr-1"
title="Arrêter tous les tests en cours"
on:click={() => w.stopTests()}
>
<i class="bi bi-stop"></i>
</button>
</div>
</div>

View File

@ -153,6 +153,18 @@ func declareAPIAdminWorksRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, nil)
})
worksRoutes.DELETE("/tests", func(c *gin.Context) {
w := c.MustGet("work").(*Work)
err := w.stopTests()
if err != nil {
log.Println("Unable to stop tests:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during test stop."})
return
}
c.JSON(http.StatusOK, true)
})
// Grades related to works
worksRoutes.GET("/grades", func(c *gin.Context) {