qa-svelte: initial commit
This commit is contained in:
parent
abdf146fea
commit
0fe037d7f5
|
@ -5,6 +5,7 @@ evdist/evdist
|
|||
frontend/frontend
|
||||
repochecker/repochecker
|
||||
frontend/ui/node_modules
|
||||
qa/ui/node_modules
|
||||
fickit-backend-initrd.img
|
||||
fickit-backend-kernel
|
||||
fickit-backend-squashfs.img
|
||||
|
|
25
.drone.yml
25
.drone.yml
|
@ -29,6 +29,15 @@ steps:
|
|||
- go get -v -d srs.epita.fr/fic-server/qa
|
||||
- mkdir deploy
|
||||
|
||||
- name: build qa ui
|
||||
image: node:19-alpine
|
||||
commands:
|
||||
- cd qa/ui
|
||||
- npm install --network-timeout=100000
|
||||
- sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte
|
||||
- npm run build
|
||||
- tar chjf ../../deploy/htdocs-qa.tar.bz2 build
|
||||
|
||||
- name: vet
|
||||
image: golang:alpine
|
||||
commands:
|
||||
|
@ -81,9 +90,8 @@ steps:
|
|||
CGO_ENABLED: 0
|
||||
|
||||
- name: build frontend ui
|
||||
image: node:19-alpine3.15
|
||||
image: node:19-alpine
|
||||
commands:
|
||||
- apk --no-cache add python2 build-base
|
||||
- cd frontend/ui
|
||||
- npm install --network-timeout=100000
|
||||
- sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte
|
||||
|
@ -124,7 +132,6 @@ steps:
|
|||
image: golang:alpine
|
||||
commands:
|
||||
- go build -v -buildvcs=false -o deploy/qa-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} srs.epita.fr/fic-server/qa
|
||||
- tar chjf deploy/htdocs-qa.tar.bz2 htdocs-qa
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
|
@ -372,9 +379,8 @@ steps:
|
|||
CGO_ENABLED: 0
|
||||
|
||||
- name: build frontend ui
|
||||
image: node:19-alpine3.15
|
||||
image: node:19-alpine
|
||||
commands:
|
||||
- apk --no-cache add python2 build-base
|
||||
- cd frontend/ui
|
||||
- npm install --network-timeout=100000
|
||||
- sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte
|
||||
|
@ -405,6 +411,15 @@ steps:
|
|||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
|
||||
- name: build qa ui
|
||||
image: node:19-alpine
|
||||
commands:
|
||||
- cd qa/ui
|
||||
- npm install --network-timeout=100000
|
||||
- sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte
|
||||
- npm run build
|
||||
- tar chjf ../../deploy/htdocs-qa.tar.bz2 build
|
||||
|
||||
- name: build qa
|
||||
image: golang:alpine
|
||||
commands:
|
||||
|
|
|
@ -25,7 +25,7 @@ VOLUME /srv/htdocs-dashboard/
|
|||
|
||||
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/dashboard/dashboard /srv/dashboard
|
||||
COPY dashboard/static/index.html /srv/htdocs-dashboard/
|
||||
COPY admin/static/css/bootstrap.min.css qa/static/css/fic.css admin/static/css/glyphicon.css /srv/htdocs-dashboard/css/
|
||||
COPY admin/static/css/bootstrap.min.css dashboard/static/css/fic.css admin/static/css/glyphicon.css /srv/htdocs-dashboard/css/
|
||||
COPY admin/static/fonts /srv/htdocs-dashboard/fonts
|
||||
COPY frontend/ui/static/img/ dashboard/static/img/logo-epita-bw.png dashboard/static/img/sii.png /srv/htdocs-dashboard/img/
|
||||
COPY dashboard/static/js/dashboard.js admin/static/js/angular.min.js dashboard/static/js/angular-animate.min.js admin/static/js/angular-route.min.js admin/static/js/angular-sanitize.min.js admin/static/js/bootstrap.min.js admin/static/js/common.js admin/static/js/d3.v3.min.js admin/static/js/jquery.min.js /srv/htdocs-dashboard/js/
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
FROM node:19-alpine as nodebuild
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
COPY qa/ui/ .
|
||||
|
||||
RUN npm install --network-timeout=100000 && \
|
||||
sed -i 's!@popperjs/core/dist/esm/popper!@popperjs/core!' node_modules/sveltestrap/src/*.js node_modules/sveltestrap/src/*.svelte && \
|
||||
npm run build
|
||||
|
||||
|
||||
FROM golang:1-alpine as gobuild
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
@ -7,6 +18,7 @@ WORKDIR /go/src/srs.epita.fr/fic-server/
|
|||
COPY go.mod go.sum ./
|
||||
COPY settings settings/
|
||||
COPY libfic ./libfic/
|
||||
COPY --from=nodebuild /ui ./qa/ui
|
||||
COPY qa ./qa/
|
||||
|
||||
RUN go get -d -v ./qa && \
|
||||
|
@ -24,9 +36,3 @@ ENTRYPOINT ["/srv/qa", "--bind=:8083"]
|
|||
VOLUME /srv/htdocs-qa/
|
||||
|
||||
COPY --from=gobuild /go/src/srs.epita.fr/fic-server/qa/qa /srv/qa
|
||||
COPY qa/static/index.html /srv/htdocs-qa/
|
||||
COPY admin/static/css/bootstrap.min.css qa/static/css/fic.css admin/static/css/glyphicon.css /srv/htdocs-qa/css/
|
||||
COPY admin/static/fonts /srv/htdocs-qa/fonts
|
||||
COPY frontend/ui/static/img/ /srv/htdocs-qa/img/
|
||||
COPY qa/static/js/qa.js admin/static/js/angular.min.js qa/static/js/angular-resource.min.js admin/static/js/angular-route.min.js admin/static/js/angular-sanitize.min.js admin/static/js/bootstrap.min.js admin/static/js/common.js admin/static/js/i18n admin/static/js/jquery.min.js /srv/htdocs-qa/js/
|
||||
COPY qa/static/views/ /srv/htdocs-qa/views/
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../../qa/static/css/fic.css
|
|
@ -0,0 +1,387 @@
|
|||
@font-face {
|
||||
font-family: "Linux Biolinum";
|
||||
src: url('../fonts/LinBiolinum_R.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Linux Biolinum";
|
||||
src: url('../fonts/LinBiolinum_RB.woff') format('woff');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Linux Biolinum";
|
||||
src: url('../fonts/LinBiolinum_RI.woff') format('woff');
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'FantasqueSansMonoRegular';
|
||||
src: url('../fonts/FantasqueSansMono-Regular.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[ng-cloak] {
|
||||
display:none !important;
|
||||
}
|
||||
|
||||
.popover.bs-popover-left .arrow::after {
|
||||
border-left-color: #7A8288;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.bg-public {
|
||||
background-image: url('../img/logo-epita-bw.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.bg-public .carousel h3 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.1rem;
|
||||
}
|
||||
|
||||
.flag {
|
||||
font-family: 'FantasqueSansMonoRegular', monospace;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.theme-card {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.beautiful {
|
||||
font-family: "Linux Biolinum",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
.beautiful ol {
|
||||
font-size: 133%;
|
||||
}
|
||||
.beautiful ol ol {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bolder;
|
||||
}
|
||||
.text-indent p {
|
||||
text-indent: 1em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.niceborder {
|
||||
border-bottom: 5px #4eaee6 solid;
|
||||
}
|
||||
.navbar img {
|
||||
margin: 3px auto;
|
||||
height: 100px;
|
||||
}
|
||||
.navbar .clock {
|
||||
font-size: 70px;
|
||||
}
|
||||
.clock:not(.expired):not(.wait) .point, .clock.expired {
|
||||
transition: color text-shadow 1s;
|
||||
position: relative;
|
||||
animation: clockanim 1s ease infinite;
|
||||
-moz-animation: clockanim 1s ease infinite;
|
||||
-webkit-animation: clockanim 1s ease infinite;
|
||||
}
|
||||
.clock.wait .point {
|
||||
transition: color text-shadow 1s;
|
||||
position: relative;
|
||||
animation: clockwait 1s ease infinite;
|
||||
-moz-animation: clockwait 1s ease infinite;
|
||||
-webkit-animation: clockwait 1s ease infinite;
|
||||
}
|
||||
.end {
|
||||
color: #e64143;
|
||||
}
|
||||
.point {
|
||||
text-shadow: 0 0 20px #4eaee6;
|
||||
}
|
||||
.end .point {
|
||||
text-shadow: 0 0 20px #e64143;
|
||||
}
|
||||
@-webkit-keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; };
|
||||
}
|
||||
@-moz-keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; };
|
||||
}
|
||||
keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; };
|
||||
}
|
||||
@-webkit-keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
@-moz-keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
|
||||
samp.cksum {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 16vw;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
h1 small.authors {
|
||||
float: right;
|
||||
font-style: italic;
|
||||
font-size: 42%;
|
||||
}
|
||||
.lead small.authors {
|
||||
color: #7a8288;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a.badge:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.teamname {
|
||||
-webkit-filter: invert(100%);
|
||||
filter: invert(100%);
|
||||
}
|
||||
a:hover .teamname {
|
||||
text-shadow: 0px 0px 10px #888888;
|
||||
}
|
||||
|
||||
.authors a {
|
||||
color: #3A3F44;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-style: italic;
|
||||
margin-top: -7px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#eventsList {
|
||||
overflow:hidden;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.swap-animation .alert {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.swap-animation {
|
||||
margin-bottom: 0.5rem;
|
||||
max-height: 30vh;
|
||||
transition: max-height 1.0s linear,opacity 1.0s linear,transform 0.5s linear;
|
||||
}
|
||||
.swap-animation.ng-enter {
|
||||
transform: translateY(-25vh);
|
||||
max-height: 0vh;
|
||||
}
|
||||
.swap-animation.ng-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
max-height: 30vh;
|
||||
}
|
||||
.swap-animation.ng-leave {
|
||||
opacity: 1;
|
||||
max-height: 30vh;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
.swap-animation.ng-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(120vw);
|
||||
max-height: 0vh;
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
bottom: -10px;
|
||||
}
|
||||
.carousel-caption {
|
||||
padding: 0;
|
||||
position: static;
|
||||
}
|
||||
.carousel .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.carousel .table-sm td {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.table th.frotated {
|
||||
border: 0;
|
||||
}
|
||||
.table th.rotated {
|
||||
height: 100px;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
line-height: 0.9;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
th.rotated > div {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
left: -50px;
|
||||
height: 100%;
|
||||
transform: skew(45deg,0deg);
|
||||
overflow: hidden;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
th.rotated div span {
|
||||
transform: skew(-45deg,0deg) rotate(45deg);
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: -35px;
|
||||
display: inline-block;
|
||||
width: 110px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
ul.list-inline li {
|
||||
display: inline;
|
||||
}
|
||||
ul.list-inline li:not(:last-child)::after {
|
||||
content: " ● "
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: ">"
|
||||
}
|
||||
|
||||
.excard {
|
||||
transition: transform 250ms;
|
||||
}
|
||||
.excard:hover {
|
||||
transform: scale(1.07);
|
||||
}
|
||||
|
||||
#tagsMenu + .dropdown-menu div {
|
||||
overflow-y: auto;
|
||||
max-height: calc(66vh - 100px);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: solid 2px;
|
||||
margin-left: 1em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.jumbotron img {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#eventsList .card {
|
||||
border-left-color: rgba(0,0,0,.125) !important;
|
||||
border-right-color: rgba(0,0,0,.125) !important;
|
||||
border-top-color: rgba(0,0,0,.125) !important;
|
||||
}
|
||||
|
||||
.bg-public .card-body {
|
||||
padding:1rem;
|
||||
padding-bottom:0;
|
||||
}
|
||||
|
||||
#themesSummary .card-body {
|
||||
padding:0;
|
||||
}
|
||||
#themesSummary h3 {
|
||||
background: rgba(64,64,64,0.66);
|
||||
border-radius: 2px;
|
||||
padding: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-top: -40px;
|
||||
}
|
||||
#themesSummary p {
|
||||
font-size: 90%;
|
||||
margin: 0.2rem;
|
||||
text-indent: 0.6em;
|
||||
}
|
||||
|
||||
.card-sm .card-header, .card-sm .card-footer {
|
||||
padding: 0.2rem 0.75rem;
|
||||
}
|
||||
.card-sm .card-body {
|
||||
padding: 0.4rem 0.75rem;
|
||||
}
|
||||
.card-sm .card-body.text-indent p {
|
||||
text-indent: 0.4rem;
|
||||
}
|
||||
|
||||
.carousel-item, .carousel-caption {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-bottom: -15rem;
|
||||
}
|
||||
.page-header h1 {
|
||||
text-shadow: 0 0 15px rgba(255,255,255,0.95), 0 0 5px rgb(255,255,255)
|
||||
}
|
||||
.page-header h1, .page-header h1 a {
|
||||
color: black;
|
||||
}
|
||||
.page-header h1 a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.page-header h2 {
|
||||
font-size: 100%;
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.9)
|
||||
}
|
||||
.page-header h2, .page-header h2 a {
|
||||
color: #4eaee6;
|
||||
}
|
||||
.page-header h1 {
|
||||
padding-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.page-header h2 {
|
||||
padding-bottom: 14rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header .headerfade {
|
||||
background: linear-gradient(transparent 0%, rgb(233,236,239) 100%);
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
a.list-group-item:hover {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
Assets http.FileSystem
|
||||
StaticDir string = "ui/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&StaticDir, "static", StaticDir, "Directory containing static files")
|
||||
}
|
||||
|
||||
func sanitizeStaticOptions() error {
|
||||
StaticDir, _ = filepath.Abs(StaticDir)
|
||||
if _, err := os.Stat(StaticDir); os.IsNotExist(err) {
|
||||
StaticDir, _ = filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "ui"))
|
||||
if _, err := os.Stat(StaticDir); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Assets = http.Dir(StaticDir)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//go:build !dev
|
||||
// +build !dev
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed ui/build/* ui/build/_app/assets/pages/* ui/build/_app/pages/*
|
||||
var _assets embed.FS
|
||||
|
||||
var Assets http.FileSystem
|
||||
|
||||
func init() {
|
||||
sub, err := fs.Sub(_assets, "ui/build")
|
||||
if err != nil {
|
||||
log.Fatal("Unable to cd to ui/build directory:", err)
|
||||
}
|
||||
Assets = http.FS(sub)
|
||||
}
|
||||
|
||||
func sanitizeStaticOptions() error {
|
||||
return nil
|
||||
}
|
21
qa/main.go
21
qa/main.go
|
@ -2,14 +2,12 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
|
@ -17,8 +15,6 @@ import (
|
|||
"srs.epita.fr/fic-server/qa/api"
|
||||
)
|
||||
|
||||
var StaticDir string
|
||||
|
||||
type ResponseWriterPrefix struct {
|
||||
real http.ResponseWriter
|
||||
prefix string
|
||||
|
@ -69,7 +65,7 @@ func main() {
|
|||
var bind = flag.String("bind", "127.0.0.1:8083", "Bind port/socket")
|
||||
var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
|
||||
flag.StringVar(&BaseURL, "baseurl", BaseURL, "URL prepended to each URL")
|
||||
flag.StringVar(&StaticDir, "static", "./htdocs-qa/", "Directory containing static files")
|
||||
flag.StringVar(&DevProxy, "dev", DevProxy, "Proxify traffic to this host for static assets")
|
||||
flag.StringVar(&api.TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
|
||||
flag.StringVar(&api.Simulator, "simulator", "", "Auth string to simulate (for development only)")
|
||||
flag.Parse()
|
||||
|
@ -79,21 +75,6 @@ func main() {
|
|||
// Sanitize options
|
||||
var err error
|
||||
log.Println("Checking paths...")
|
||||
if StaticDir != "" {
|
||||
if sDir, err := filepath.Abs(StaticDir); err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
log.Println("Serving pages from", sDir)
|
||||
staticFS = http.Dir(sDir)
|
||||
}
|
||||
} else {
|
||||
sub, err := fs.Sub(assets, "static")
|
||||
if err != nil {
|
||||
log.Fatal("Unable to cd to static/ directory:", err)
|
||||
}
|
||||
log.Println("Serving pages from memory.")
|
||||
staticFS = http.FS(sub)
|
||||
}
|
||||
if BaseURL != "/" {
|
||||
BaseURL = path.Clean(BaseURL)
|
||||
} else {
|
||||
|
|
124
qa/static.go
124
qa/static.go
|
@ -2,74 +2,96 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
var assets embed.FS
|
||||
var (
|
||||
BaseURL = "/"
|
||||
DevProxy string
|
||||
indexTmpl []byte
|
||||
)
|
||||
|
||||
var BaseURL = "/"
|
||||
func getIndexHtml(w io.Writer, file io.Reader) {
|
||||
var err error
|
||||
if indexTmpl, err = ioutil.ReadAll(file); err != nil {
|
||||
log.Println("Cannot read whole index.html: ", err)
|
||||
} else {
|
||||
indexTmpl = bytes.Replace(indexTmpl, []byte("{{.urlbase}}"), []byte(path.Clean(path.Join(BaseURL+"/", "nuke"))[:len(path.Clean(path.Join(BaseURL+"/", "nuke")))-4]), -1)
|
||||
}
|
||||
|
||||
var indexTmpl []byte
|
||||
w.Write(indexTmpl)
|
||||
}
|
||||
|
||||
func getIndexHtml(c *gin.Context) {
|
||||
if len(indexTmpl) == 0 {
|
||||
if file, err := os.Open(path.Join(StaticDir, "index.html")); err != nil {
|
||||
log.Println("Unable to open index.html: ", err)
|
||||
} else {
|
||||
defer file.Close()
|
||||
|
||||
if indexTmpl, err = ioutil.ReadAll(file); err != nil {
|
||||
log.Println("Cannot read whole index.html: ", err)
|
||||
func serveOrReverse(forced_url string, baseURL string) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
if DevProxy != "" {
|
||||
if u, err := url.Parse(DevProxy); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
indexTmpl = bytes.Replace(indexTmpl, []byte("{{.urlbase}}"), []byte(path.Clean(path.Join(BaseURL+"/", "nuke"))[:len(path.Clean(path.Join(BaseURL+"/", "nuke")))-4]), -1)
|
||||
if forced_url != "" {
|
||||
u.Path = path.Join(u.Path, forced_url)
|
||||
} else {
|
||||
u.Path = path.Join(u.Path, strings.TrimPrefix(c.Request.URL.Path, baseURL))
|
||||
}
|
||||
|
||||
if r, err := http.NewRequest(c.Request.Method, u.String(), c.Request.Body); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
} else if resp, err := http.DefaultClient.Do(r); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusBadGateway)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
|
||||
for key := range resp.Header {
|
||||
c.Writer.Header().Add(key, resp.Header.Get(key))
|
||||
}
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
|
||||
if r.URL.Path == path.Join(u.Path, "/") {
|
||||
getIndexHtml(c.Writer, resp.Body)
|
||||
} else {
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if forced_url != "" {
|
||||
c.Request.URL.Path = forced_url
|
||||
} else {
|
||||
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, baseURL)
|
||||
}
|
||||
|
||||
if c.Request.URL.Path == "/" {
|
||||
if len(indexTmpl) == 0 {
|
||||
if file, err := Assets.Open("index.html"); err != nil {
|
||||
log.Println("Unable to open index.html: ", err)
|
||||
} else {
|
||||
defer file.Close()
|
||||
|
||||
getIndexHtml(c.Writer, file)
|
||||
}
|
||||
} else {
|
||||
c.Writer.Write(indexTmpl)
|
||||
}
|
||||
} else {
|
||||
http.FileServer(Assets).ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Writer.Write(indexTmpl)
|
||||
}
|
||||
|
||||
var staticFS http.FileSystem
|
||||
|
||||
func serveFile(c *gin.Context, url string) {
|
||||
c.Request.URL.Path = url
|
||||
http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
func declareStaticRoutes(router *gin.RouterGroup, baseURL string) {
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
getIndexHtml(c)
|
||||
})
|
||||
|
||||
router.GET("/exercices/*_", func(c *gin.Context) {
|
||||
getIndexHtml(c)
|
||||
})
|
||||
router.GET("/themes/*_", func(c *gin.Context) {
|
||||
getIndexHtml(c)
|
||||
})
|
||||
|
||||
router.GET("/css/*_", func(c *gin.Context) {
|
||||
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
|
||||
})
|
||||
router.GET("/fonts/*_", func(c *gin.Context) {
|
||||
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
|
||||
})
|
||||
router.GET("/img/*_", func(c *gin.Context) {
|
||||
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
|
||||
})
|
||||
router.GET("/js/*_", func(c *gin.Context) {
|
||||
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
|
||||
})
|
||||
router.GET("/views/*_", func(c *gin.Context) {
|
||||
serveFile(c, strings.TrimPrefix(c.Request.URL.Path, baseURL))
|
||||
})
|
||||
router.GET("/", serveOrReverse("", baseURL))
|
||||
router.GET("/exercices", serveOrReverse("/", baseURL))
|
||||
router.GET("/exercices/*_", serveOrReverse("/", baseURL))
|
||||
router.GET("/themes", serveOrReverse("/", baseURL))
|
||||
router.GET("/themes/*_", serveOrReverse("/", baseURL))
|
||||
router.GET("/_app/*_", serveOrReverse("", baseURL))
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/css/bootstrap.min.css
|
|
@ -1,387 +0,0 @@
|
|||
@font-face {
|
||||
font-family: "Linux Biolinum";
|
||||
src: url('../fonts/LinBiolinum_R.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Linux Biolinum";
|
||||
src: url('../fonts/LinBiolinum_RB.woff') format('woff');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Linux Biolinum";
|
||||
src: url('../fonts/LinBiolinum_RI.woff') format('woff');
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'FantasqueSansMonoRegular';
|
||||
src: url('../fonts/FantasqueSansMono-Regular.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[ng-cloak] {
|
||||
display:none !important;
|
||||
}
|
||||
|
||||
.popover.bs-popover-left .arrow::after {
|
||||
border-left-color: #7A8288;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.bg-public {
|
||||
background-image: url('../img/logo-epita-bw.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.bg-public .carousel h3 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.1rem;
|
||||
}
|
||||
|
||||
.flag {
|
||||
font-family: 'FantasqueSansMonoRegular', monospace;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.theme-card {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.beautiful {
|
||||
font-family: "Linux Biolinum",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
.beautiful ol {
|
||||
font-size: 133%;
|
||||
}
|
||||
.beautiful ol ol {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bolder;
|
||||
}
|
||||
.text-indent p {
|
||||
text-indent: 1em;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.niceborder {
|
||||
border-bottom: 5px #4eaee6 solid;
|
||||
}
|
||||
.navbar img {
|
||||
margin: 3px auto;
|
||||
height: 100px;
|
||||
}
|
||||
.navbar .clock {
|
||||
font-size: 70px;
|
||||
}
|
||||
.clock:not(.expired):not(.wait) .point, .clock.expired {
|
||||
transition: color text-shadow 1s;
|
||||
position: relative;
|
||||
animation: clockanim 1s ease infinite;
|
||||
-moz-animation: clockanim 1s ease infinite;
|
||||
-webkit-animation: clockanim 1s ease infinite;
|
||||
}
|
||||
.clock.wait .point {
|
||||
transition: color text-shadow 1s;
|
||||
position: relative;
|
||||
animation: clockwait 1s ease infinite;
|
||||
-moz-animation: clockwait 1s ease infinite;
|
||||
-webkit-animation: clockwait 1s ease infinite;
|
||||
}
|
||||
.end {
|
||||
color: #e64143;
|
||||
}
|
||||
.point {
|
||||
text-shadow: 0 0 20px #4eaee6;
|
||||
}
|
||||
.end .point {
|
||||
text-shadow: 0 0 20px #e64143;
|
||||
}
|
||||
@-webkit-keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; };
|
||||
}
|
||||
@-moz-keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; };
|
||||
}
|
||||
keyframes clockanim {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0; }
|
||||
100% { opacity: 1.0; };
|
||||
}
|
||||
@-webkit-keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
@-moz-keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
keyframes clockwait {
|
||||
0% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
50% { text-shadow: 0 0 2px #A6D6F2; }
|
||||
100% { text-shadow: 0 0 20px #A6D6F2; }
|
||||
}
|
||||
|
||||
samp.cksum {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 16vw;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
||||
h1 small.authors {
|
||||
float: right;
|
||||
font-style: italic;
|
||||
font-size: 42%;
|
||||
}
|
||||
.lead small.authors {
|
||||
color: #7a8288;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
a.badge:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.teamname {
|
||||
-webkit-filter: invert(100%);
|
||||
filter: invert(100%);
|
||||
}
|
||||
a:hover .teamname {
|
||||
text-shadow: 0px 0px 10px #888888;
|
||||
}
|
||||
|
||||
.authors a {
|
||||
color: #3A3F44;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-style: italic;
|
||||
margin-top: -7px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#eventsList {
|
||||
overflow:hidden;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.swap-animation .alert {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.swap-animation {
|
||||
margin-bottom: 0.5rem;
|
||||
max-height: 30vh;
|
||||
transition: max-height 1.0s linear,opacity 1.0s linear,transform 0.5s linear;
|
||||
}
|
||||
.swap-animation.ng-enter {
|
||||
transform: translateY(-25vh);
|
||||
max-height: 0vh;
|
||||
}
|
||||
.swap-animation.ng-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
max-height: 30vh;
|
||||
}
|
||||
.swap-animation.ng-leave {
|
||||
opacity: 1;
|
||||
max-height: 30vh;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
.swap-animation.ng-leave-active {
|
||||
opacity: 0;
|
||||
transform: translateX(120vw);
|
||||
max-height: 0vh;
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
bottom: -10px;
|
||||
}
|
||||
.carousel-caption {
|
||||
padding: 0;
|
||||
position: static;
|
||||
}
|
||||
.carousel .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.carousel .table-sm td {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.table th.frotated {
|
||||
border: 0;
|
||||
}
|
||||
.table th.rotated {
|
||||
height: 100px;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
line-height: 0.9;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
th.rotated > div {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
left: -50px;
|
||||
height: 100%;
|
||||
transform: skew(45deg,0deg);
|
||||
overflow: hidden;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
th.rotated div span {
|
||||
transform: skew(-45deg,0deg) rotate(45deg);
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: -35px;
|
||||
display: inline-block;
|
||||
width: 110px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
ul.list-inline li {
|
||||
display: inline;
|
||||
}
|
||||
ul.list-inline li:not(:last-child)::after {
|
||||
content: " ● "
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: ">"
|
||||
}
|
||||
|
||||
.excard {
|
||||
transition: transform 250ms;
|
||||
}
|
||||
.excard:hover {
|
||||
transform: scale(1.07);
|
||||
}
|
||||
|
||||
#tagsMenu + .dropdown-menu div {
|
||||
overflow-y: auto;
|
||||
max-height: calc(66vh - 100px);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: solid 2px;
|
||||
margin-left: 1em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.jumbotron img {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#eventsList .card {
|
||||
border-left-color: rgba(0,0,0,.125) !important;
|
||||
border-right-color: rgba(0,0,0,.125) !important;
|
||||
border-top-color: rgba(0,0,0,.125) !important;
|
||||
}
|
||||
|
||||
.bg-public .card-body {
|
||||
padding:1rem;
|
||||
padding-bottom:0;
|
||||
}
|
||||
|
||||
#themesSummary .card-body {
|
||||
padding:0;
|
||||
}
|
||||
#themesSummary h3 {
|
||||
background: rgba(64,64,64,0.66);
|
||||
border-radius: 2px;
|
||||
padding: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-top: -40px;
|
||||
}
|
||||
#themesSummary p {
|
||||
font-size: 90%;
|
||||
margin: 0.2rem;
|
||||
text-indent: 0.6em;
|
||||
}
|
||||
|
||||
.card-sm .card-header, .card-sm .card-footer {
|
||||
padding: 0.2rem 0.75rem;
|
||||
}
|
||||
.card-sm .card-body {
|
||||
padding: 0.4rem 0.75rem;
|
||||
}
|
||||
.card-sm .card-body.text-indent p {
|
||||
text-indent: 0.4rem;
|
||||
}
|
||||
|
||||
.carousel-item, .carousel-caption {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-bottom: -15rem;
|
||||
}
|
||||
.page-header h1 {
|
||||
text-shadow: 0 0 15px rgba(255,255,255,0.95), 0 0 5px rgb(255,255,255)
|
||||
}
|
||||
.page-header h1, .page-header h1 a {
|
||||
color: black;
|
||||
}
|
||||
.page-header h1 a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.page-header h2 {
|
||||
font-size: 100%;
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.9)
|
||||
}
|
||||
.page-header h2, .page-header h2 a {
|
||||
color: #4eaee6;
|
||||
}
|
||||
.page-header h1 {
|
||||
padding-top: 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.page-header h2 {
|
||||
padding-bottom: 14rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header .headerfade {
|
||||
background: linear-gradient(transparent 0%, rgb(233,236,239) 100%);
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
a.list-group-item:hover {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/css/glyphicon.css
|
|
@ -1 +0,0 @@
|
|||
../../admin/static/fonts/
|
|
@ -1 +0,0 @@
|
|||
../../../frontend/ui/static/img/fic.png
|
|
@ -1,70 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html ng-app="FICApp">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Challenge Forensic - QA</title>
|
||||
<link href="{{.urlbase}}css/bootstrap.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="{{.urlbase}}css/glyphicon.css" type="text/css" rel="stylesheet" media="screen">
|
||||
<style>
|
||||
.cksum {
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bg-mfound {
|
||||
background-color: #7bcfd0 !important;
|
||||
}
|
||||
.bg-ffound {
|
||||
background-color: #7bdfc0 !important;
|
||||
}
|
||||
.bg-wchoices {
|
||||
background-color: #c07bdf !important;
|
||||
}
|
||||
</style>
|
||||
<base href="{{.urlbase}}">
|
||||
</head>
|
||||
<body class="bg-light text-dark">
|
||||
<nav class="navbar sticky-top navbar-expand-md navbar-dark bg-dark text-light">
|
||||
<a class="navbar-brand" href=".">
|
||||
<img alt="FIC" src="img/fic.png" style="height: 30px">
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#qaMenu" aria-controls="qaMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="qaMenu">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="themes">Scénarios</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="exercices">Défis</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<span class="navbar-text" ng-controller="VersionController" ng-cloak>
|
||||
v{{ v.version }} – Logged as {{ v.auth.name }} (team #{{ v.auth.id_team }})
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<div class="progress" style="background-color: #4eaee6; height: 3px; border-radius: 0;">
|
||||
<div class="progress-bar bg-secondary" role="progressbar" style="width: {{timeProgression * 100}}%"></div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-1" ng-view></div>
|
||||
|
||||
<div style="position: fixed; top: 60px; right: 0; z-index: 10; min-width: 30vw;">
|
||||
<toast ng-repeat="toast in toasts" yes-no="toast.yesFunc || toast.noFunc" onyes="toast.yesFunc" onno="toast.noFunc" date="toast.date" msg="toast.msg" timeout="toast.timeout" title="toast.title" variant="toast.variant"></toast>
|
||||
</div>
|
||||
|
||||
<script src="{{.urlbase}}js/jquery.min.js"></script>
|
||||
<script src="{{.urlbase}}js/bootstrap.min.js"></script>
|
||||
<script src="{{.urlbase}}js/angular.min.js"></script>
|
||||
<script src="{{.urlbase}}js/angular-resource.min.js"></script>
|
||||
<script src="{{.urlbase}}js/angular-route.min.js"></script>
|
||||
<script src="{{.urlbase}}js/angular-sanitize.min.js"></script>
|
||||
<script src="{{.urlbase}}js/qa.js"></script>
|
||||
<script src="{{.urlbase}}js/common.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
AngularJS v1.7.9
|
||||
(c) 2010-2018 Google, Inc. http://angularjs.org
|
||||
License: MIT
|
||||
*/
|
||||
(function(T,a){'use strict';function M(m,f){f=f||{};a.forEach(f,function(a,d){delete f[d]});for(var d in m)!m.hasOwnProperty(d)||"$"===d.charAt(0)&&"$"===d.charAt(1)||(f[d]=m[d]);return f}var B=a.$$minErr("$resource"),H=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;a.module("ngResource",["ng"]).info({angularVersion:"1.7.9"}).provider("$resource",function(){var m=/^https?:\/\/\[[^\]]*][^/]*/,f=this;this.defaults={stripTrailingSlashes:!0,cancellable:!1,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",
|
||||
isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}};this.$get=["$http","$log","$q","$timeout",function(d,F,G,N){function C(a,d){this.template=a;this.defaults=n({},f.defaults,d);this.urlParams={}}var O=a.noop,r=a.forEach,n=a.extend,R=a.copy,P=a.isArray,D=a.isDefined,x=a.isFunction,I=a.isNumber,y=a.$$encodeUriQuery,S=a.$$encodeUriSegment;C.prototype={setUrlParams:function(a,d,f){var g=this,c=f||g.template,s,h,n="",b=g.urlParams=Object.create(null);r(c.split(/\W/),function(a){if("hasOwnProperty"===
|
||||
a)throw B("badname");!/^\d+$/.test(a)&&a&&(new RegExp("(^|[^\\\\]):"+a+"(\\W|$)")).test(c)&&(b[a]={isQueryParamValue:(new RegExp("\\?.*=:"+a+"(?:\\W|$)")).test(c)})});c=c.replace(/\\:/g,":");c=c.replace(m,function(b){n=b;return""});d=d||{};r(g.urlParams,function(b,a){s=d.hasOwnProperty(a)?d[a]:g.defaults[a];D(s)&&null!==s?(h=b.isQueryParamValue?y(s,!0):S(s),c=c.replace(new RegExp(":"+a+"(\\W|$)","g"),function(b,a){return h+a})):c=c.replace(new RegExp("(/?):"+a+"(\\W|$)","g"),function(b,a,e){return"/"===
|
||||
e.charAt(0)?e:a+e})});g.defaults.stripTrailingSlashes&&(c=c.replace(/\/+$/,"")||"/");c=c.replace(/\/\.(?=\w+($|\?))/,".");a.url=n+c.replace(/\/(\\|%5C)\./,"/.");r(d,function(b,c){g.urlParams[c]||(a.params=a.params||{},a.params[c]=b)})}};return function(m,y,z,g){function c(b,c){var d={};c=n({},y,c);r(c,function(c,f){x(c)&&(c=c(b));var e;if(c&&c.charAt&&"@"===c.charAt(0)){e=b;var k=c.substr(1);if(null==k||""===k||"hasOwnProperty"===k||!H.test("."+k))throw B("badmember",k);for(var k=k.split("."),h=0,
|
||||
n=k.length;h<n&&a.isDefined(e);h++){var g=k[h];e=null!==e?e[g]:void 0}}else e=c;d[f]=e});return d}function s(b){return b.resource}function h(b){M(b||{},this)}var Q=new C(m,g);z=n({},f.defaults.actions,z);h.prototype.toJSON=function(){var b=n({},this);delete b.$promise;delete b.$resolved;delete b.$cancelRequest;return b};r(z,function(b,a){var f=!0===b.hasBody||!1!==b.hasBody&&/^(POST|PUT|PATCH)$/i.test(b.method),g=b.timeout,m=D(b.cancellable)?b.cancellable:Q.defaults.cancellable;g&&!I(g)&&(F.debug("ngResource:\n Only numeric values are allowed as `timeout`.\n Promises are not supported in $resource, because the same value would be used for multiple requests. If you are looking for a way to cancel requests, you should use the `cancellable` option."),
|
||||
delete b.timeout,g=null);h[a]=function(e,k,J,y){function z(a){p.catch(O);null!==u&&u.resolve(a)}var K={},v,t,w;switch(arguments.length){case 4:w=y,t=J;case 3:case 2:if(x(k)){if(x(e)){t=e;w=k;break}t=k;w=J}else{K=e;v=k;t=J;break}case 1:x(e)?t=e:f?v=e:K=e;break;case 0:break;default:throw B("badargs",arguments.length);}var E=this instanceof h,l=E?v:b.isArray?[]:new h(v),q={},C=b.interceptor&&b.interceptor.request||void 0,D=b.interceptor&&b.interceptor.requestError||void 0,F=b.interceptor&&b.interceptor.response||
|
||||
s,H=b.interceptor&&b.interceptor.responseError||G.reject,I=t?function(a){t(a,A.headers,A.status,A.statusText)}:void 0;w=w||void 0;var u,L,A;r(b,function(a,b){switch(b){default:q[b]=R(a);case "params":case "isArray":case "interceptor":case "cancellable":}});!E&&m&&(u=G.defer(),q.timeout=u.promise,g&&(L=N(u.resolve,g)));f&&(q.data=v);Q.setUrlParams(q,n({},c(v,b.params||{}),K),b.url);var p=G.resolve(q).then(C).catch(D).then(d),p=p.then(function(c){var e=c.data;if(e){if(P(e)!==!!b.isArray)throw B("badcfg",
|
||||
a,b.isArray?"array":"object",P(e)?"array":"object",q.method,q.url);if(b.isArray)l.length=0,r(e,function(a){"object"===typeof a?l.push(new h(a)):l.push(a)});else{var d=l.$promise;M(e,l);l.$promise=d}}c.resource=l;A=c;return F(c)},function(a){a.resource=l;A=a;return H(a)}),p=p["finally"](function(){l.$resolved=!0;!E&&m&&(l.$cancelRequest=O,N.cancel(L),u=L=q.timeout=null)});p.then(I,w);return E?p:(l.$promise=p,l.$resolved=!1,m&&(l.$cancelRequest=z),l)};h.prototype["$"+a]=function(b,c,d){x(b)&&(d=c,c=
|
||||
b,b={});b=h[a].call(this,b,this,c,d);return b.$promise||b}});return h}}]})})(window,window.angular);
|
||||
//# sourceMappingURL=angular-resource.min.js.map
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/angular-route.min.js
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/angular-sanitize.min.js
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/angular.min.js
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/bootstrap.min.js
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/common.js
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/d3.v3.min.js
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/i18n/
|
|
@ -1 +0,0 @@
|
|||
../../../admin/static/js/jquery.min.js
|
|
@ -1,492 +0,0 @@
|
|||
angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
|
||||
.config(function($routeProvider, $locationProvider) {
|
||||
$routeProvider
|
||||
.when("/themes", {
|
||||
controller: "ThemesListController",
|
||||
templateUrl: "views/theme-list.html"
|
||||
})
|
||||
.when("/themes/:themeId", {
|
||||
controller: "ThemeController",
|
||||
templateUrl: "views/theme.html"
|
||||
})
|
||||
.when("/themes/:themeId/exercices/:exerciceId", {
|
||||
controller: "ExerciceController",
|
||||
templateUrl: "views/exercice.html"
|
||||
})
|
||||
.when("/exercices", {
|
||||
controller: "AllExercicesListController",
|
||||
templateUrl: "views/exercice-list.html"
|
||||
})
|
||||
.when("/exercices/:exerciceId", {
|
||||
controller: "ExerciceController",
|
||||
templateUrl: "views/exercice.html"
|
||||
})
|
||||
.when("/", {
|
||||
templateUrl: "views/home.html"
|
||||
});
|
||||
$locationProvider.html5Mode(true);
|
||||
});
|
||||
|
||||
angular.module("FICApp")
|
||||
.directive('autofocus', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link : function($scope, $element) {
|
||||
$timeout(function() {
|
||||
$element[0].focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}])
|
||||
|
||||
.component('toast', {
|
||||
bindings: {
|
||||
date: '=',
|
||||
msg: '=',
|
||||
timeout: '=',
|
||||
title: '=',
|
||||
variant: '=',
|
||||
yesNo: '=',
|
||||
onyes: '=',
|
||||
onno: '=',
|
||||
},
|
||||
controller: function($element) {
|
||||
if (this.timeout === 0)
|
||||
$element.children(0).toast({autohide: false});
|
||||
else if (!this.timeout && this.timeout !== 0)
|
||||
$element.children(0).toast({delay: 7000});
|
||||
else
|
||||
$element.children(0).toast({delay: this.timeout});
|
||||
$element.children(0).toast('show');
|
||||
this.yesFunc = function() {
|
||||
$element.children(0).toast('dispose');
|
||||
if (this.onyes)
|
||||
this.onyes();
|
||||
}
|
||||
this.noFunc = function() {
|
||||
$element.children(0).toast('dispose');
|
||||
if (this.onno)
|
||||
this.onno();
|
||||
}
|
||||
},
|
||||
template: `<div class="toast mb-2" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<span ng-if="$ctrl.variant" class="badge badge-pill badge-{{ $ctrl.variant }}" style="padding: .25em .66em"> </span>
|
||||
<strong class="mr-auto" ng-bind="$ctrl.title"></strong>
|
||||
<small class="text-muted" ng-bind="$ctrl.date">just now</small>
|
||||
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="toast-body" ng-bind-html="$ctrl.msg" ng-if="$ctrl.msg"></div>
|
||||
<div class="d-flex justify-content-around mb-1" ng-if="$ctrl.yesNo">
|
||||
<button type="button" class="ml-2 btn btn-sm btn-success" ng-click="$ctrl.yesFunc()">Yes</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.noFunc()">No</button>
|
||||
</div>
|
||||
</div>`
|
||||
});
|
||||
|
||||
angular.module("FICApp")
|
||||
.factory("Version", function($resource) {
|
||||
return $resource("api/version")
|
||||
})
|
||||
.factory("Todo", function($resource) {
|
||||
return $resource("api/qa_work.json")
|
||||
})
|
||||
.factory("TodoWorked", function($resource) {
|
||||
return $resource("api/qa_mywork.json")
|
||||
})
|
||||
.factory("MyExercices", function($resource) {
|
||||
return $resource("api/qa_myexercices.json")
|
||||
})
|
||||
.factory("ExercicesTested", function($resource) {
|
||||
return $resource("api/qa_exercices.json")
|
||||
})
|
||||
.factory("Team", function($resource) {
|
||||
return $resource("api/teams/:teamId", { teamId: '@id' }, {
|
||||
'update': {method: 'PUT'},
|
||||
})
|
||||
})
|
||||
.factory("Teams", function($resource) {
|
||||
return $resource("api/teams.json")
|
||||
})
|
||||
.factory("Theme", function($resource) {
|
||||
return $resource("api/themes/:themeId", { themeId: '@id' }, {
|
||||
update: {method: 'PUT'}
|
||||
});
|
||||
})
|
||||
.factory("Themes", function($resource) {
|
||||
return $resource("api/themes.json", null, {
|
||||
'get': {method: 'GET'},
|
||||
})
|
||||
})
|
||||
.factory("ThemedExercice", function($resource) {
|
||||
return $resource("api/themes/:themeId/exercices/:exerciceId", { themeId: '@id', exerciceId: '@idExercice' }, {
|
||||
update: {method: 'PUT'}
|
||||
})
|
||||
})
|
||||
.factory("Exercice", function($resource) {
|
||||
return $resource("api/exercices/:exerciceId", { exerciceId: '@id' }, {
|
||||
update: {method: 'PUT'},
|
||||
patch: {method: 'PATCH'}
|
||||
})
|
||||
})
|
||||
.factory("ExerciceQA", function($resource) {
|
||||
return $resource("api/qa/:exerciceId/:qaId", { exerciceId: '@idExercice', qaId: '@id' }, {
|
||||
update: {method: 'PUT'},
|
||||
patch: {method: 'PATCH'}
|
||||
})
|
||||
});
|
||||
|
||||
angular.module("FICApp")
|
||||
.filter("toColor", function() {
|
||||
return function(num) {
|
||||
num >>>= 0;
|
||||
var b = num & 0xFF,
|
||||
g = (num & 0xFF00) >>> 8,
|
||||
r = (num & 0xFF0000) >>> 16,
|
||||
a = ( (num & 0xFF000000) >>> 24 ) / 255 ;
|
||||
return "#" + r.toString(16) + g.toString(16) + b.toString(16);
|
||||
}
|
||||
})
|
||||
.filter("cksum", function() {
|
||||
return function(input) {
|
||||
if (input == undefined)
|
||||
return input;
|
||||
var raw = atob(input).toString(16);
|
||||
var hex = '';
|
||||
for (var i = 0; i < raw.length; i++ ) {
|
||||
var _hex = raw.charCodeAt(i).toString(16)
|
||||
hex += (_hex.length == 2 ? _hex : '0' + _hex);
|
||||
}
|
||||
return hex
|
||||
}
|
||||
})
|
||||
|
||||
.directive('color', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, ele, attr, ctrl){
|
||||
ctrl.$formatters.unshift(function(num){
|
||||
num >>>= 0;
|
||||
var b = num & 0xFF,
|
||||
g = (num & 0xFF00) >>> 8,
|
||||
r = (num & 0xFF0000) >>> 16,
|
||||
a = ( (num & 0xFF000000) >>> 24 ) / 255 ;
|
||||
return "#" + r.toString(16) + g.toString(16) + b.toString(16);
|
||||
});
|
||||
ctrl.$parsers.unshift(function(viewValue){
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(viewValue);
|
||||
return result ? (
|
||||
parseInt(result[1], 16) * 256 * 256 +
|
||||
parseInt(result[2], 16) * 256 +
|
||||
parseInt(result[3], 16)
|
||||
|
||||
) : 0;
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.directive('integer', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, ele, attr, ctrl){
|
||||
ctrl.$parsers.unshift(function(viewValue){
|
||||
return parseInt(viewValue, 10);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.directive('float', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, ele, attr, ctrl){
|
||||
ctrl.$parsers.unshift(function(viewValue){
|
||||
return parseFloat(viewValue, 10);
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
.run(function($rootScope, $http, $interval) {
|
||||
$rootScope.toasts = [];
|
||||
$rootScope.addToast = function(kind, title, msg, yesFunc, noFunc, tmout) {
|
||||
$rootScope.toasts.unshift({
|
||||
variant: kind,
|
||||
title: title,
|
||||
msg: msg,
|
||||
timeout: tmout,
|
||||
yesFunc: yesFunc,
|
||||
noFunc: noFunc,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
.controller("VersionController", function($scope, Version) {
|
||||
$scope.v = Version.get();
|
||||
})
|
||||
|
||||
.controller("ToDoController", function($scope, Todo, TodoWorked, ExercicesTested, $location) {
|
||||
$scope.todos = Todo.query();
|
||||
$scope.exo_done = ExercicesTested.get();
|
||||
$scope.tododone = {}
|
||||
$scope.work = TodoWorked.query(function(tw) {
|
||||
tw.forEach(function(t) {
|
||||
$scope.tododone[t.id_exercice] = t
|
||||
})
|
||||
});
|
||||
|
||||
$scope.show = function(id) {
|
||||
$location.url("/exercices/" + id);
|
||||
};
|
||||
})
|
||||
|
||||
.controller("MyExercicesController", function($scope, MyExercices, $location) {
|
||||
$scope.my_exercices = MyExercices.query();
|
||||
|
||||
$scope.show = function(id) {
|
||||
$location.url("/exercices/" + id);
|
||||
};
|
||||
})
|
||||
|
||||
.controller("ThemesListController", function($scope, Theme, $location, $rootScope, $http) {
|
||||
$scope.themes = Theme.query();
|
||||
$scope.fields = ["name", "authors", "headline"];
|
||||
|
||||
$scope.validateSearch = function(keyEvent) {
|
||||
if (keyEvent.which === 13) {
|
||||
var myTheme = null;
|
||||
$scope.themes.forEach(function(theme) {
|
||||
if (String(theme.name.toLowerCase()).indexOf($scope.query.toLowerCase()) >= 0) {
|
||||
if (myTheme === null)
|
||||
myTheme = theme;
|
||||
else
|
||||
myTheme = false;
|
||||
}
|
||||
});
|
||||
if (myTheme)
|
||||
$location.url("themes/" + myTheme.id);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.show = function(id) {
|
||||
$location.url("/themes/" + id);
|
||||
};
|
||||
})
|
||||
.controller("ThemeController", function($scope, Theme, $routeParams, $location, $rootScope, $http) {
|
||||
$scope.theme = Theme.get({ themeId: $routeParams.themeId });
|
||||
$scope.fields = ["name", "urlid", "authors", "headline", "intro", "image"];
|
||||
})
|
||||
|
||||
.controller("AllExercicesListController", function($scope, Exercice, Theme, $routeParams, $location, $rootScope, $http, $filter) {
|
||||
$http({
|
||||
url: "api/themes.json",
|
||||
method: "GET"
|
||||
}).then(function(response) {
|
||||
$scope.themes = response.data
|
||||
});
|
||||
|
||||
$scope.exercices = Exercice.query();
|
||||
$scope.exercice = {}; // Array used to save fields to updates in selected exercices
|
||||
$scope.fields = ["title", "headline"];
|
||||
|
||||
$scope.validateSearch = function(keyEvent) {
|
||||
if (keyEvent.which === 13) {
|
||||
var myExercice = null;
|
||||
$scope.exercices.forEach(function(exercice) {
|
||||
if (String(exercice.title.toLowerCase()).indexOf($scope.query.toLowerCase()) >= 0) {
|
||||
if (myExercice === null)
|
||||
myExercice = exercice;
|
||||
else
|
||||
myExercice = false;
|
||||
}
|
||||
});
|
||||
if (myExercice)
|
||||
$location.url("exercices/" + myExercice.id);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.show = function(id) {
|
||||
$location.url("/exercices/" + id);
|
||||
};
|
||||
})
|
||||
.controller("ExercicesListController", function($scope, ThemedExercice, $location, $rootScope, $http) {
|
||||
$scope.exercices = ThemedExercice.query({ themeId: $scope.theme.id });
|
||||
$scope.fields = ["title", "headline"];
|
||||
|
||||
$scope.show = function(id) {
|
||||
$location.url("/themes/" + $scope.theme.id + "/exercices/" + id);
|
||||
};
|
||||
})
|
||||
|
||||
.controller("MyTodoExerciceController", function($scope, Exercice, ExerciceQA, Theme) {
|
||||
$scope.mytheme = null
|
||||
$scope.myexercice = Exercice.get({ exerciceId: $scope.todo.id_exercice }, function(e) {
|
||||
$scope.mytheme = Theme.get({ themeId: e.id_theme })
|
||||
});
|
||||
})
|
||||
|
||||
.controller("ExerciceController", function($scope, $rootScope, Exercice, ThemedExercice, $routeParams, $location, $http) {
|
||||
if ($routeParams.themeId && $routeParams.exerciceId == "new") {
|
||||
$scope.exercice = new ThemedExercice();
|
||||
} else {
|
||||
$scope.exercice = Exercice.get({ exerciceId: $routeParams.exerciceId });
|
||||
}
|
||||
$http({
|
||||
url: "api/themes.json",
|
||||
method: "GET"
|
||||
}).then(function(response) {
|
||||
$scope.themes = response.data
|
||||
var last_exercice = null;
|
||||
angular.forEach($scope.themes[$scope.exercice.id_theme].exercices, function(exercice, k) {
|
||||
if (last_exercice != null) {
|
||||
$scope.themes[$scope.exercice.id_theme].exercices[last_exercice].next = k;
|
||||
exercice.previous = last_exercice;
|
||||
}
|
||||
last_exercice = k;
|
||||
exercice.id = k;
|
||||
});
|
||||
});
|
||||
$scope.exercices = Exercice.query();
|
||||
})
|
||||
|
||||
.controller("ExerciceQAController", function($scope, $rootScope, ExerciceQA, $routeParams, $location, $http) {
|
||||
if ($routeParams.exerciceId) {
|
||||
$scope.queries = ExerciceQA.query({ exerciceId: $routeParams.exerciceId });
|
||||
} else {
|
||||
$scope.queries = ExerciceQA.query({ exerciceId: $scope.todo.id_exercice });
|
||||
}
|
||||
$scope.queriesNSolved = "N/A"
|
||||
$scope.queriesNClosed = "N/A"
|
||||
$scope.queries.$promise.then(function (queries) {
|
||||
var nbResolved = 0;
|
||||
var nbClosed = 0;
|
||||
queries.forEach(function(q) {
|
||||
if (q.solved) {
|
||||
nbResolved++;
|
||||
}
|
||||
if (q.closed) {
|
||||
nbClosed++;
|
||||
}
|
||||
})
|
||||
$scope.queriesNSolved = queries.length - nbResolved
|
||||
$scope.queriesNClosed = queries.length - nbClosed
|
||||
})
|
||||
$scope.fields = ["state", "subject", "user", "creation"];
|
||||
$scope.namedFields = {
|
||||
"state": "État",
|
||||
"subject": "Sujet",
|
||||
"content": "Description",
|
||||
};
|
||||
$scope.states = {
|
||||
"ok": "OK",
|
||||
"orthograph": "Orthographe et grammaire",
|
||||
"issue-statement": "Pas compris",
|
||||
"issue-flag": "Problème de flag",
|
||||
"issue-mcq": "Problème de QCM/QCU",
|
||||
"issue-hint": "Problème d'indice",
|
||||
"issue-file": "Problème de fichier",
|
||||
"issue": "Problème autre",
|
||||
"suggest": "Suggestion",
|
||||
"too-hard": "Trop dur",
|
||||
"too-easy": "Trop facile",
|
||||
};
|
||||
|
||||
$scope.newQuery = new ExerciceQA();
|
||||
|
||||
$scope.query_comments = null
|
||||
$scope.query_selected = null
|
||||
$scope.showComments = function(qid) {
|
||||
if ($scope.query_selected == qid) {
|
||||
$scope.query_selected = null
|
||||
$scope.queries_comments = null
|
||||
} else {
|
||||
$scope.query_selected = qid
|
||||
$http({
|
||||
url: "api/qa/" + $routeParams.exerciceId + "/" + $scope.queries[$scope.query_selected].id + "/comments"
|
||||
}).then(function(response) {
|
||||
$scope.queries_comments = response.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$scope.newComment = {content: ""}
|
||||
$scope.addComment = function() {
|
||||
$http({
|
||||
url: "api/qa/" + $routeParams.exerciceId + "/" + $scope.queries[$scope.query_selected].id + "/comments",
|
||||
method: "POST",
|
||||
data: $scope.newComment,
|
||||
}).then(function(response) {
|
||||
$scope.newComment = {content: ""}
|
||||
$http({
|
||||
url: "api/qa/" + $routeParams.exerciceId + "/" + $scope.queries[$scope.query_selected].id + "/comments"
|
||||
}).then(function(response) {
|
||||
$scope.queries_comments = response.data
|
||||
})
|
||||
}, function(response) {
|
||||
$scope.addToast('danger', 'An error occurs when trying to respond to QA entry:', response.data.errmsg);
|
||||
})
|
||||
}
|
||||
|
||||
$scope.thumbUp = function(qid) {
|
||||
$http({
|
||||
url: "api/qa/" + $routeParams.exerciceId + "/" + $scope.queries[qid].id + "/comments",
|
||||
method: "POST",
|
||||
data: { "content": "+1" },
|
||||
}).then(function(response) {
|
||||
$http({
|
||||
url: "api/qa/" + $routeParams.exerciceId + "/" + $scope.queries[$scope.query_selected].id + "/comments"
|
||||
}).then(function(response) {
|
||||
$scope.queries_comments = response.data
|
||||
})
|
||||
}, function(response) {
|
||||
$scope.addToast('danger', 'An error occurs when trying to respond to QA entry:', response.data.errmsg);
|
||||
})
|
||||
}
|
||||
|
||||
$scope.updateQA = function(qid) {
|
||||
$scope.newQuery = $scope.queries[$scope.query_selected]
|
||||
}
|
||||
|
||||
$scope.deleteQA = function(qid) {
|
||||
var myq = $scope.queries[$scope.query_selected]
|
||||
myq.$delete(
|
||||
{ exerciceId: $routeParams.exerciceId, qaId: qid },
|
||||
function() {
|
||||
$scope.queries = ExerciceQA.query({ exerciceId: $routeParams.exerciceId });
|
||||
$scope.query_selected = null
|
||||
}, function(response) {
|
||||
$scope.addToast('danger', 'An error occurs when trying to delete QA query:', response.data.errmsg);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$scope.solveQA = function(qid) {
|
||||
var myq = $scope.queries[$scope.query_selected]
|
||||
myq.solved = (new Date()).toISOString()
|
||||
myq.$update({ exerciceId: $routeParams.exerciceId, qaId: qid })
|
||||
}
|
||||
|
||||
$scope.closeQA = function(qid) {
|
||||
var myq = $scope.queries[$scope.query_selected]
|
||||
myq.closed = (new Date()).toISOString()
|
||||
myq.$update({ exerciceId: $routeParams.exerciceId, qaId: qid })
|
||||
}
|
||||
|
||||
$scope.saveQuery = function() {
|
||||
if (this.newQuery.id) {
|
||||
this.newQuery.$update({ exerciceId: $routeParams.exerciceId, qaId: this.newQuery.id });
|
||||
} else {
|
||||
this.newQuery.$save({ exerciceId: $routeParams.exerciceId }, function() {
|
||||
//$scope.saveComment();
|
||||
$scope.addToast('success', 'QA query created!');
|
||||
$scope.queries = ExerciceQA.query({ exerciceId: $routeParams.exerciceId });
|
||||
$scope.newQuery = new ExerciceQA();
|
||||
}, function(response) {
|
||||
$scope.addToast('danger', 'An error occurs when trying to create QA query:', response.data.errmsg);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,27 +0,0 @@
|
|||
<h2>
|
||||
Défis
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<p><input type="search" class="form-control" placeholder="Filtrer" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>
|
||||
<table class="table table-hover table-bordered table-striped table-sm">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th ng-repeat="field in fields">
|
||||
{{ field }}
|
||||
</th>
|
||||
<th>
|
||||
Scénario
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="exercice in exercices | filter: query">
|
||||
<td ng-repeat="field in fields" ng-click="show(exercice.id)" ng-bind-html="exercice[field]"></td>
|
||||
<td>
|
||||
<a ng-href="themes/{{ exercice.id_theme }}">{{ themes[exercice.id_theme].name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -1,110 +0,0 @@
|
|||
<h2>
|
||||
{{exercice.title}}
|
||||
<small ng-if="themes && themes[exercice.id_theme]"><a href="themes/{{ exercice.id_theme }}" title="{{themes[exercice.id_theme].authors | stripHTML}}">{{themes[exercice.id_theme].name}}</a></small>
|
||||
<div class="btn-group" role="group" ng-if="themes[exercice.id_theme].exercices[exercice.id]">
|
||||
<a href="exercices/{{ themes[exercice.id_theme].exercices[exercice.id].previous }}" title="Exercice précédent" ng-class="{'disabled': !themes[exercice.id_theme].exercices[exercice.id].previous}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span></a>
|
||||
<a href="exercices/{{ themes[exercice.id_theme].exercices[exercice.id].next }}" title="Exercice suivant" ng-class="{'disabled': !themes[exercice.id_theme].exercices[exercice.id].next}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
<a href="../{{themes[exercice.id_theme].urlid}}/{{exercice.urlid}}" target="_self" class="float-right ml-2 btn btn-sm btn-info"><span class="glyphicon glyphicon-road" aria-hidden="true"></span> Site du challenge</a>
|
||||
</h2>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6" ng-bind-html="exercice.statement"></div>
|
||||
<div class="col-md-6" ng-bind-html="exercice.overview"></div>
|
||||
</div>
|
||||
|
||||
<div ng-controller="ExerciceQAController" class="mb-5">
|
||||
<form ng-submit="saveQuery()" class="card mb-3">
|
||||
<div class="card-header">
|
||||
Qu'avez-vous pensé de ce défi ?
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group row" ng-repeat="(field, namedField) in namedFields">
|
||||
<label for="{{ field }}" class="col-2 col-form-label col-form-label-sm">{{ namedField }}</label>
|
||||
<div class="col-10">
|
||||
<input type="text" class="form-control form-control-sm" id="{{ field }}" ng-model="newQuery[field]" ng-if="field != 'state' && field != 'content'">
|
||||
<select class="custom-select custom-select-sm" id="{{ field }}" ng-model="newQuery[field]" ng-options="k as v for (k, v) in states" ng-if="field == 'state'"></select>
|
||||
<textarea class="form-control form-control-sm" placeholder="Ajouter un commentaire" rows="2" id="{{ field }}" ng-model="newQuery[field]" ng-if="field == 'content' && !newQuery.id"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary float-right">
|
||||
Soumettre
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table table-bordered table-striped" ng-class="{'table-hover': queries.length, 'table-sm': queries.length}">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th ng-repeat="field in fields">
|
||||
{{ field }}
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-if="queries.length">
|
||||
<tr ng-repeat="(qid, q) in queries" ng-click="showComments(qid)" ng-class="{'bg-warning': qid == query_selected}">
|
||||
<td ng-repeat="field in fields" ng-bind-html="q[field]"></td>
|
||||
<td>
|
||||
<button type="button" ng-click="thumbUp(qid)" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody ng-if="!queries.length">
|
||||
<tr>
|
||||
<td colspan="{{ fields.length }}" class="font-weight-bold text-info text-center">Aucun requête enregistrée</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div ng-if="query_selected !== null" class="card">
|
||||
<div class="card-header">
|
||||
<h4>{{ queries[query_selected].subject }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<dl class="col-9 row">
|
||||
<dt class="col-3">Qui ?</dt>
|
||||
<dd class="col-9">{{ queries[query_selected].user }} (team #{{ queries[query_selected].id_team}})</dd>
|
||||
|
||||
<dt class="col-3">État</dt>
|
||||
<dd class="col-9">{{ queries[query_selected].state }}</dd>
|
||||
|
||||
<dt class="col-3">Date de création</dt>
|
||||
<dd class="col-9">{{ queries[query_selected].creation }}</dd>
|
||||
|
||||
<dt class="col-3">Date de résolution</dt>
|
||||
<dd class="col-9">{{ queries[query_selected].solved }}</dd>
|
||||
|
||||
<dt class="col-3">Date de clôture</dt>
|
||||
<dd class="col-9">{{ queries[query_selected].closed }}</dd>
|
||||
</dl>
|
||||
<div class="col-3">
|
||||
<button ng-click="updateQA(queries[query_selected].id)" class="btn btn-secondary">
|
||||
Mettre à jour
|
||||
</button>
|
||||
<button ng-click="solveQA(queries[query_selected].id)" class="btn btn-info">
|
||||
Marqué comme résolu
|
||||
</button>
|
||||
<button ng-click="deleteQA(queries[query_selected].id)" class="btn btn-danger">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<tr ng-repeat="comment in queries_comments">
|
||||
<td style="white-space: pre-line">
|
||||
Le {{ comment.date }}, <strong>{{ comment.user }}</strong> a écrit : {{ comment.content }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<form ng-submit="addComment()">
|
||||
<labe for="newComment">Répondre :</label>
|
||||
<textarea class="form-control" placeholder="Ajouter un commentaire" rows="2" id="newComment" ng-model="newComment.content"></textarea>
|
||||
<button type="submit" class="btn btn-primary mt-1 float-right">
|
||||
Ajouter le commentaire
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,57 +0,0 @@
|
|||
<div class="jumbotron">
|
||||
<div class="row">
|
||||
<div class="col-6" ng-controller="ToDoController">
|
||||
<h3>Challenges à valider</h3>
|
||||
<table class="table table-stripped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Avancement</th>
|
||||
<th>Scénario</th>
|
||||
<th>Défi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="todo in todos" ng-controller="MyTodoExerciceController" ng-class="{'text-light': !tododone[todo.id_exercice] && !exo_done[todo.id_exercice], 'table-dark': !tododone[todo.id_exercice] && !exo_done[todo.id_exercice], 'table-warning': !tododone[todo.id_exercice] && exo_done[todo.id_exercice] == 'access', 'table-info': !tododone[todo.id_exercice] && (exo_done[todo.id_exercice] == 'tried' || exo_done[todo.id_exercice] == 'solved'), 'table-success': tododone[todo.id_exercice]}" ng-click="show(todo.id_exercice)">
|
||||
<td ng-if="!tododone[todo.id_exercice] && (!exo_done[todo.id_exercice] || exo_done[todo.id_exercice] == 'access')">
|
||||
À tester
|
||||
</td>
|
||||
<td ng-if="!tododone[todo.id_exercice] && exo_done[todo.id_exercice] && exo_done[todo.id_exercice] != 'access'">
|
||||
À commenter
|
||||
</td>
|
||||
<td ng-if="tododone[todo.id_exercice]">
|
||||
Commenté <span ng-if="!exo_done[todo.id_exercice] || exo_done[todo.id_exercice] != 'solved'">mais pas testé</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ mytheme.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ myexercice.title }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-6" ng-controller="MyExercicesController">
|
||||
<h3>Vos challenges</h3>
|
||||
<table class="table table-stripped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Défi</th>
|
||||
<th>Requêtes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="todo in my_exercices" ng-controller="ExerciceQAController" ng-class="{'table-success': queries.length > 0, 'table-warning': queriesNSolved > 0}" ng-click="show(todo.id_exercice)">
|
||||
<td ng-controller="MyTodoExerciceController">
|
||||
<span ng-if="mytheme.name">{{ mytheme.name }} –</span>
|
||||
{{ myexercice.title }}
|
||||
</td>
|
||||
<td>
|
||||
{{ queriesNSolved }} / {{ queriesNClosed }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,19 +0,0 @@
|
|||
<h2>
|
||||
Scénarios
|
||||
</h2>
|
||||
|
||||
<p><input type="search" class="form-control" placeholder="Filtrer" ng-model="query" ng-keypress="validateSearch($event)" autofocus></p>
|
||||
<table class="table table-hover table-bordered table-striped">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th ng-repeat="field in fields">
|
||||
{{ field }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="theme in themes | filter: query" ng-click="show(theme.id)">
|
||||
<td ng-repeat="field in fields" ng-bind-html="theme[field]"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
|
@ -1,25 +0,0 @@
|
|||
<h2>{{theme.name}} <small class="text-muted">{{theme.authors | stripHTML}}</small></h2>
|
||||
|
||||
<div class="container" ng-bind-html="theme.intro"></div>
|
||||
|
||||
<div ng-if="theme.id" ng-controller="ExercicesListController">
|
||||
<h3>
|
||||
Défis ({{ exercices.length }})
|
||||
</h3>
|
||||
|
||||
<p><input type="search" class="form-control form-control-sm" placeholder="Search" ng-model="query" autofocus></p>
|
||||
<table class="table table-hover table-bordered table-striped table-sm">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th ng-repeat="field in fields">
|
||||
{{ field }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="exercice in exercices | filter: query" ng-click="show(exercice.id)">
|
||||
<td ng-repeat="field in fields" ng-bind-html="exercice[field]"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2019
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init svelte@next
|
||||
|
||||
# create a new project in my-app
|
||||
npm init svelte@next my-app
|
||||
```
|
||||
|
||||
> Note: the `@next` is temporary
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"$lib": ["src/lib"],
|
||||
"$lib/*": ["src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "qa",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "svelte-kit dev",
|
||||
"build": "svelte-kit build",
|
||||
"package": "svelte-kit package",
|
||||
"preview": "svelte-kit preview",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "next",
|
||||
"@sveltejs/adapter-static": "^1.0.0-next.21",
|
||||
"@sveltejs/kit": "next",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-svelte3": "^3.2.1",
|
||||
"prettier": "^2.4.1",
|
||||
"prettier-plugin-svelte": "^2.4.0",
|
||||
"svelte": "^3.44.0"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
|
||||
<meta name="author" content="EPITA Laboratoire SRS">
|
||||
<meta name="robots" content="none">
|
||||
<base href="{{.urlbase}}">
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id="svelte">%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
|
@ -0,0 +1,2 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,18 @@
|
|||
import adapt from '@sveltejs/adapter-static';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapt({
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
paths: {
|
||||
// base: '{{.urlbase}}',
|
||||
},
|
||||
ssr: false,
|
||||
// hydrate the <div id="svelte"> element in src/app.html
|
||||
target: '#svelte'
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
Loading…
Reference in New Issue