Skip to content
Snippets Groups Projects
Commit 75fd6f1e authored by Matthieu BENOIST's avatar Matthieu BENOIST
Browse files

Merge branch 'livraison' into 'main'

Livraison

See merge request !2
parents b3adde20 c3d7efb7
Branches
Tags
1 merge request!2Livraison
Showing
with 322 additions and 725 deletions
name: Deploy Angular to GitHub Pages
on:
push:
branches:
- main
permissions:
contents: write
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm install
- name: Install Angular CLI
run: npm install -g @angular/cli
- name: Build Angular app
run: ng build --configuration production --base-href "/Auto-evaluation/"
- name: Add 404.html for GitHub Pages
run: |
echo '<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; URL='https://foucblg.github.io/Auto-evaluation/'" />
</head>
<body>
</body>
</html>' > dist/autoeval/browser/404.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: dist/autoeval/browser
#Yo # Auto-évaluation d'inclusif le jeu
\ No newline at end of file
# Documentation du l'auto-évaluation
Cette application se rapproche fortement du Quiz par sa construction. Les réponses sont en revanche les mêmes pour chaque question.
On retrouve la même architecture, les mêmes services et interfaces, pour des renseignements, se référer à la documentation du Quiz.
\ No newline at end of file
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^19.1.5", "@angular-devkit/build-angular": "^19.1.5",
"@angular/cli": "^19.1.5", "@angular/cli": "^19.1.4",
"@angular/compiler-cli": "^19.1.4", "@angular/compiler-cli": "^19.1.4",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"jasmine-core": "~5.2.0", "jasmine-core": "~5.2.0",
...@@ -38,4 +38,4 @@ ...@@ -38,4 +38,4 @@
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.5.2" "typescript": "~5.5.2"
} }
} }
\ No newline at end of file
public/images/Score_A.png

4.35 KiB

public/images/Score_B.png

4.32 KiB

public/images/Score_C.png

4.52 KiB

public/images/Score_D.png

4.26 KiB

public/images/Score_E.png

4.04 KiB

...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
height: 100%; height: 100%;
} }
/* Navbar */ /* Header */
.navbar-div { .header-div {
height: 61px; height: 66px;
} }
/* Contenu principal */ /* Contenu principal */
...@@ -19,11 +19,12 @@ main { ...@@ -19,11 +19,12 @@ main {
height: 100%; height: 100%;
} }
.quiz-div { .autoeval-div {
flex: 1; /* Prend tout l'espace disponible */ flex: 1; /* Prend tout l'espace disponible */
min-height: 50vh; /* Hauteur minimale pour le contenu */ min-height: 50vh; /* Hauteur minimale pour le contenu */
overflow: auto; /* Ajoute un défilement si le contenu est trop long */ overflow: auto; /* Ajoute un défilement si le contenu est trop long */
height: 100%; height: 100%;
padding: 16px;
} }
/* Footer */ /* Footer */
...@@ -37,7 +38,7 @@ footer { ...@@ -37,7 +38,7 @@ footer {
padding: 0 28px; padding: 0 28px;
font-size: 12px; font-size: 12px;
width: 100%; width: 100%;
margin-top: auto; /* Force le footer à rester en bas */ margin-top: auto;
} }
footer ul { footer ul {
...@@ -45,7 +46,7 @@ footer ul { ...@@ -45,7 +46,7 @@ footer ul {
padding: 0; padding: 0;
margin: 0; margin: 0;
display: flex; display: flex;
gap: 12px; /* Espace entre les éléments */ gap: 12px;
} }
footer li { footer li {
...@@ -60,7 +61,13 @@ footer a { ...@@ -60,7 +61,13 @@ footer a {
vertical-align: middle; vertical-align: middle;
} }
footer a:hover{
text-decoration: underline;
}
footer p {
color: #ffffff;
}
footer .right-section { footer .right-section {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -69,4 +76,4 @@ footer .right-section { ...@@ -69,4 +76,4 @@ footer .right-section {
footer .right-section img { footer .right-section img {
height: 20px; height: 20px;
margin-right: 5px; margin-right: 5px;
} }
\ No newline at end of file
<div class="app-container"> <div class="app-container">
<header> <header>
<div class="navbar-div"> <div class="header-div">
<app-navbar></app-navbar> <app-header></app-header>
</div> </div>
</header> </header>
<main> <main>
<div class="quiz-div"> <div class="autoeval-div">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</main> </main>
<!-- Mise en place du footer -->
<footer> <footer>
<ul> <ul>
<li><a href="https://resin.grandlyon.com/mentions-legales" target="_blank" rel="noopener noreferrer">Mentions légales &nbsp; &#9679;</a></li> <li><a href="https://resin.grandlyon.com/mentions-legales" target="_blank" rel="noopener noreferrer">Mentions légales</a></li>
<li><a href="https://resin.grandlyon.com/newsletter" target="_blank" rel="noopener noreferrer">Newsletter &nbsp; &#9679;</a></li> <p>&#9679;</p>
<li><a href="https://resin.grandlyon.com/contact" target="_blank" rel="noopener noreferrer">Contact &nbsp; &#9679;</a></li> <li><a href="https://resin.grandlyon.com/page/qui-sommes-nous" target="_blank" rel="noopener noreferrer">Qui sommes nous ?</a></li>
<li><a href="https://resin.grandlyon.com/page/qui-sommes-nous" target="_blank" rel="noopener noreferrer">Qui sommes-nous ? &nbsp; &#9679;</a></li> <p>&#9679;</p>
<li><a href="/accessibilite" target="_blank" rel="noopener noreferrer">Accessibilité : Pas Testé</a></li> <li>Accessibilité : Non conforme</li>
</ul> </ul>
<div class="right-section"> <div class="right-section">
<ul> <ul>
...@@ -25,7 +26,8 @@ ...@@ -25,7 +26,8 @@
<a href="https://www.grandlyon.com/" target="_blank" rel="noopener noreferrer"> <a href="https://www.grandlyon.com/" target="_blank" rel="noopener noreferrer">
<img src="https://inclusivite-resin.grandlyon.com/app/themes/ausy-modular-theme/public/images/picto-plus.0a1d2b.png" alt="Logo"> <img src="https://inclusivite-resin.grandlyon.com/app/themes/ausy-modular-theme/public/images/picto-plus.0a1d2b.png" alt="Logo">
<p>Un site de la Métropole de Lyon</p> <p>Un site de la Métropole de Lyon</p>
</a></li> </a>
</li>
</ul> </ul>
</div> </div>
</footer> </footer>
......
...@@ -2,18 +2,18 @@ import { CommonModule } from '@angular/common'; ...@@ -2,18 +2,18 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import * as data from './shared/assets/data/questions_final.json'; import * as data from './shared/assets/data/questions_final.json';
import { QuizData } from './shared/types/interfaces'; import { AutoevalData } from './shared/types/interfaces';
import { NavbarComponent } from './views/navbar/navbar.component'; import { HeaderComponent } from "./views/header/header.component";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [CommonModule, RouterOutlet, NavbarComponent], imports: [CommonModule, RouterOutlet, HeaderComponent],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
export class AppComponent { export class AppComponent {
title = 'INCLUSIF: Le jeu'; title = 'INCLUSIF: Le jeu';
quizData = quizData; autoevalData = autoevalData;
} }
export const quizData: QuizData = data; export const autoevalData: AutoevalData = data;
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { QuizComponent } from './views/quiz/quiz.component'; import { ContexteComponent } from './views/autoeval/autoeval-contexte/autoeval-contexte.component';
import { AutoevalComponent } from './views/autoeval/autoeval.component';
import { AutoevalHomepageComponent } from './views/autoeval/autoeval-homepage/autoeval-homepage.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
path: 'quiz', title: "Quiz Inclusif, le jeu", children: [ path: 'autoeval',
{ path: '**', component: QuizComponent }, title: "Auto-évaluation d'inclusivité",
children: [
{ path: '', redirectTo: 'accueil', pathMatch: 'full' },
{ path: 'accueil', component: AutoevalHomepageComponent },
{ path: 'contexte', component: ContexteComponent },
{ path: ':id', component: AutoevalComponent },
{ path: '**', redirectTo: 'accueil' },
], ],
}, },
{ path: '', redirectTo: 'quiz/accueil', pathMatch: 'full' }, { path: '', redirectTo: 'autoeval/accueil', pathMatch: 'full' },
]; ];
This diff is collapsed.
import { Injectable, signal } from "@angular/core";
import { autoevalData } from "../../app.component";
import { AutoevalSegment } from "../types/interfaces";
@Injectable({
providedIn: 'root'
})
export class DataService {
topics = autoevalData["topics"];
autoevalSegments = autoevalData["questions"];
questionNumber = signal(0);
questionNumberTopic = signal(0);
topicNumber = signal(0);
currentSegment = signal<AutoevalSegment | undefined>(undefined);
currentTopic = signal("");
possibleAnswers = autoevalData["possible_answers"];
numberOfQuestions = Object.fromEntries(this.topics.map(topic => [topic, this.autoevalSegments[topic].length]));
step = signal('');
totalQuestions:number = 0
startAutoeval() {
this.step.set('start');
if (this.topics.length != 0) {
this.currentTopic.set(this.topics[this.topicNumber()]);
}
else {
console.log("Aucun topic n'est défini");
}
this.currentSegment.set(this.autoevalSegments[this.currentTopic()][this.questionNumber()]);
}
getNewQuestion() {
this.step.set('ongoing');
if (this.questionNumberTopic() === this.numberOfQuestions[this.currentTopic()]-1) {
// Si on est à la dernière question du thème
if (this.currentTopic() != this.topics[this.topics.length-1]) {
// Si on est pas au dernier thème
this.questionNumberTopic.set(0);
this.topicNumber.update(n => n + 1);
this.currentTopic.set(this.topics[this.topicNumber()]);
this.currentSegment.set(this.autoevalSegments[this.currentTopic()][this.questionNumberTopic()]);
this.questionNumber.update(n => n + 1);
}
else {
this.step.set('end');
}
}
else {
this.questionNumberTopic.update(n => n + 1);
this.questionNumber.update(n => n + 1);
this.currentSegment.set(this.autoevalSegments[this.currentTopic()][this.questionNumberTopic()]);
}
}
getPreviousQuestion() {
if (this.questionNumberTopic() > 0) {
this.questionNumberTopic.update(n => n - 1);
this.currentSegment.set(this.autoevalSegments[this.currentTopic()][this.questionNumberTopic()]);
this.questionNumber.update(n => n - 1);
}
else if (this.topicNumber() > 0) {
this.topicNumber.update(n => n - 1);
this.currentTopic.set(this.topics[this.topicNumber()]);
this.questionNumberTopic.set(this.autoevalSegments[this.currentTopic()].length - 1);
this.currentSegment.set(this.autoevalSegments[this.currentTopic()][this.questionNumberTopic()]);
this.questionNumber.update(n => n - 1);
}
else{
this.step.set('start');
}
}
getTotalQuestions(){
for (let i =0; i<this.topics.length; i++){
this.totalQuestions = this.totalQuestions + this.numberOfQuestions[this.topics[i]]
}
return this.totalQuestions
};
}
import { computed, inject, Injectable, signal } from "@angular/core"; import {inject, Injectable, signal } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Answer } from "../types/enums";
import { DataService } from "./quiz-service"; import { DataService } from "./autoeval-service";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ProgressService { export class ProgressService {
/* Service qui gère la navigation dans l'application */
router = inject(Router); router = inject(Router);
route = inject(ActivatedRoute); route = inject(ActivatedRoute);
dataService = inject(DataService); dataService = inject(DataService);
score = signal(0); currentAnswer = signal<number>(0);
questionNumber = signal(0); score = signal(Object.fromEntries(this.dataService.topics.map(topic => [topic, [0, 0]])));
currentAnswer = signal<number[]>([]); answers = signal(<number[]>[]);
currentAnswerValidity = computed(() => this.verifyAnswer())
hasEnded = signal(false);
answered = signal(false);
goToBegining() { goToBegining() {
/* Revient à la page du début, sans oublienpmr les questions déjà posées */ this.router.navigate(["autoeval", "contexte"], { onSameUrlNavigation: 'ignore' });
this.score.set(0);
this.questionNumber.set(0);
this.hasEnded.set(false);
this.router.navigate(["quiz", "accueil"], { onSameUrlNavigation: 'ignore' });
} }
start(nQuestions: number) { start() {
/* Lance une session de quiz avec le nombre de questions sélectionnées sur la page d'accueil */ this.dataService.startAutoeval();
this.score.set(0); this.router.navigate(["autoeval", this.dataService.questionNumber().toString()], {
this.questionNumber.set(0); });
this.hasEnded.set(false);
this.dataService.startQuiz(nQuestions);
this.goToNext();
} }
goToEnd() { goToEnd() {
/* Envoie sur la page de fin */ this.router.navigate(["autoeval", "end"], { replaceUrl: true });
this.hasEnded.set(true);
this.router.navigate(["quiz", "end"], { replaceUrl: true });
} }
goToNext() { goToNext() {
/* Si la session de quiz n'est pas finie, envoie vers la question suivante */ this.dataService.getNewQuestion();
if (!this.dataService.isFinished()) { if (this.dataService.step() !== 'end') {
this.dataService.getNewQuestion(); this.router.navigate(["autoeval", this.dataService.questionNumber().toString()], {
this.questionNumber.update(n => n + 1);
this.answered.set(false);
this.router.navigate(["quiz", this.questionNumber().toString()], {
queryParams: { theme: this.dataService.currentTopic(), theme_id: this.dataService.currentQuestionId(), answered: false },
replaceUrl: this.questionNumber() > 0,
}); });
} else { } else {
this.goToEnd(); this.goToEnd();
} }
} }
answer() { goToPrevious() {
if (this.currentAnswerValidity() === Answer.Empty) { this.dataService.getPreviousQuestion();
// Ne passe pas à la question suivante si aucune réponse n'a été sélectionnée if (this.dataService.step() !== 'start') {
return; this.router.navigate(["autoeval", this.dataService.questionNumber().toString()], {
} });
if (this.currentAnswerValidity() === Answer.True) { } else {
this.score.update(s => s + 1); this.goToBegining();
} }
this.answered.set(true);
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams: { answered: true },
queryParamsHandling: 'merge', // remove to replace all query params by provided
replaceUrl: true,
}
);
} }
private verifyAnswer(): Answer { answer() {
const realAnswers = this.dataService.currentSegment()?.true_answers as number[]; if (this.currentAnswer() in [0, 1, 2]) {
if (this.currentAnswer().length === 0) { this.score.update(scores => {
return Answer.Empty; scores[this.dataService.currentTopic()][0] += this.dataService.currentSegment()!.outOf*(1-this.currentAnswer()/2);
scores[this.dataService.currentTopic()][1] += this.dataService.currentSegment()!.outOf;
return scores;
});
} }
if (this.currentAnswer().sort().toString() == realAnswers.sort().toString()) { if (this.answers().length === this.dataService.questionNumber()) {
return Answer.True; this.answers.update(answers => {
} else { answers.push(this.currentAnswer());
return Answer.False; return answers;
});
} }
else{
this.answers.update(answers => {
answers[this.dataService.questionNumber()] = this.currentAnswer();
return answers;
});
}
this.goToNext();
} }
} }
import { Injectable, signal } from "@angular/core";
import { quizData } from "../../app.component";
import { QuizSegment } from "../types/interfaces";
class RandomizedQuestionIndexQueue {
/* Implémente un tirage aléatoire sans remise des numéros de questions dans un thème
* https://gist.github.com/4skinSkywalker/f10939e0b070fe1815933730670177df
*/
private remainingIndices;
private intialSize;
private currentSize;
constructor(size: number) {
this.intialSize = size;
this.currentSize = size;
this.remainingIndices = [...Array(size).keys()];
}
private randomId() {
// conversion en entier avec la comparaison BitWise OR
return Math.random() * (this.currentSize - 1) | 0
}
isEmpty() {
return (this.currentSize < 1)
}
replenish() {
this.remainingIndices = [...Array(this.intialSize).keys()];
this.currentSize = this.intialSize;
}
dequeueIndex() {
if (this.isEmpty()) {
this.replenish()
}
const id = this.randomId();
const index = this.remainingIndices[id];
this.remainingIndices.splice(id, 1);
this.currentSize--;
return index;
}
}
class TopicsQueue {
/* Implémente la queue des thèmes à effectuer selon le nombre de questions et le cycle de thème à aborder */
private possibleTopics;
private topicsCycle;
private numberOfQuestionsPerCycle = 1; // valeur par défaut
private topics: string[] = []; // queue qui va permettre de tirer les topics
constructor(possibleTopics: string[] = [], topicsCycle: number[] = []) {
this.possibleTopics = possibleTopics;
this.topicsCycle = topicsCycle;
}
initialize(nQuestions: number) {
/* Initialise la queue pour une session de quiz */
this.numberOfQuestionsPerCycle = nQuestions;
this.topics = [];
this.topicsCycle.forEach((n, i) => { // n = occurrence, i = index
this.topics.push(...Array(n * this.numberOfQuestionsPerCycle).fill(this.possibleTopics[i]));
});
}
isEmpty() {
return this.topics.length < 1;
}
deqeue() {
return this.topics.shift();
}
getPossibleTopics() {
return this.possibleTopics;
}
getNumberOfQuestions() {
return this.topicsCycle.length * this.numberOfQuestionsPerCycle;
}
}
@Injectable({
providedIn: 'root'
})
export class DataService {
/* Service qui fournit les données relatives à la question en cours de la session de quiz
* Les questions sont tirées aléatoirement sans remise
* Lorsqu'il n'y a plus de questions non faites, la liste de questions est régénèrée
*/
quizSegmentTopicsQueue;
quizSegments = quizData["questions"]; // tous les segments de quiz possibles, groupés par thème
randomizedQuestionIndexQueuePool: Record<string, RandomizedQuestionIndexQueue> = {}
questionNumber = signal(0);
hasEnded = signal(false);
numberOfQuestions = signal(0);
currentSegment = signal<QuizSegment | undefined>(undefined);
currentTopic = signal("");
currentQuestionId = signal(-1);
constructor() {
this.quizSegmentTopicsQueue = new TopicsQueue(quizData["question_topics"], quizData["question_cycle"]);
for (const questionTopic of this.quizSegmentTopicsQueue.getPossibleTopics()) {
const rq = new RandomizedQuestionIndexQueue(this.quizSegments[questionTopic].length);
this.randomizedQuestionIndexQueuePool[questionTopic] = rq;
}
}
startQuiz(nQuestions: number) {
this.questionNumber.set(0);
this.hasEnded.set(false);
this.quizSegmentTopicsQueue.initialize(nQuestions);
this.numberOfQuestions.set(this.quizSegmentTopicsQueue.getNumberOfQuestions());
}
getNewQuestion() {
const questionTopic = this.quizSegmentTopicsQueue.deqeue();
const questionId = this.randomizedQuestionIndexQueuePool[questionTopic!].dequeueIndex();
this.currentTopic.set(questionTopic!);
this.currentQuestionId.set(questionId);
this.currentSegment.set(this.quizSegments[questionTopic!][questionId]);
}
isFinished() {
return this.quizSegmentTopicsQueue.isEmpty();
}
getNumberOfTopics() {
return this.quizSegmentTopicsQueue.getPossibleTopics().length;
}
}
export interface QuizData { export interface AutoevalData {
question_topics: string[], topics: string[],
question_cycle: number[], possible_answers: string[],
questions: Record<string, QuizSegment[]>, questions: Record<string, AutoevalSegment[]>,
} }
export interface QuizSegment { export interface AutoevalSegment {
question_type: string,
question: string, question: string,
possible_answers: string[] | never[], outOf: number,
true_answers: number[],
explanation: string,
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
h1 { h1 {
color: #333333; color: #333333;
} }
.theme { .theme {
justify-self: start; justify-self: start;
background-color: var(--p-primary-100); background-color: var(--p-primary-100);
...@@ -20,6 +21,6 @@ h1 { ...@@ -20,6 +21,6 @@ h1 {
} }
.question { .question {
text-align: center; text-align: left;
padding: 8px; font-size: 24px;
} }
<div class="theme">
<span> {{ dataService.currentTopic() }}</span>
</div>
<h1 class="question">{{ autoeval_segment()?.question }}</h1>
<app-choice-box></app-choice-box>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment