Compare commits
9 Commits
0620d9d834
...
1515140c09
Author | SHA1 | Date | |
---|---|---|---|
1515140c09 | |||
9a145965fb | |||
bcf76a2c86 | |||
8b8f3947f8 | |||
a78de73671 | |||
906501cc7b | |||
5020f378c8 | |||
ce0c8e0025 | |||
64eb8f2852 |
74
gradation.go
74
gradation.go
@ -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
|
||||
}
|
||||
|
121
repositories.go
121
repositories.go
@ -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
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const SharingTime = 15 * time.Minute
|
||||
const SharingTime = 10 * time.Minute
|
||||
|
||||
var (
|
||||
s3_endpoint string
|
||||
|
14
ui/src/lib/components/ScoreBadge.svelte
Normal file
14
ui/src/lib/components/ScoreBadge.svelte
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
12
works.go
12
works.go
@ -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) {
|
||||
|
Reference in New Issue
Block a user