Skip to content

Commit 55dc9da

Browse files
authored
Merge pull request #59 from SourceCodeOER/import_exercises_feature
feat : Import exercises (new page)
2 parents 8fce270 + 42d159c commit 55dc9da

File tree

10 files changed

+387
-27
lines changed

10 files changed

+387
-27
lines changed

assets/css/_exercise-gestion.scss

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -72,31 +72,6 @@ form {
7272
font-weight: bold;
7373
}
7474

75-
label[for=Archive] {
76-
align-self: flex-start;
77-
}
78-
79-
.message {
80-
margin-top: 10px;
81-
font-style: italic;
82-
font-size: .75em;
83-
84-
&.message--primary-color {
85-
color: $PRIMARY_COLOR
86-
}
87-
88-
&.message--red {
89-
color: $RED
90-
}
91-
}
92-
93-
.error-message {
94-
margin-top: 10px;
95-
96-
&.error-message--red {
97-
color: $RED;
98-
}
99-
}
10075
}
10176

10277
.validation__tag {

assets/css/form.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,30 @@ input[type=file] {
110110
font-style: italic;
111111
}
112112

113+
114+
form {
115+
.message {
116+
margin-top: 10px;
117+
font-style: italic;
118+
font-size: .75em;
119+
120+
&.message--primary-color {
121+
color: $PRIMARY_COLOR
122+
}
123+
124+
&.message--red {
125+
color: $RED
126+
}
127+
}
128+
129+
.error-message {
130+
margin-top: 10px;
131+
132+
&.error-message--red {
133+
color: $RED;
134+
}
135+
}
136+
}
113137
/**
114138
Custom theme of textarea
115139
*/

components/Gestion/ImportForm.vue

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<template>
2+
<section class="content">
3+
4+
<h1>{{title}}</h1>
5+
6+
<p>
7+
Vous pouvez importer des exercices depuis cette interface en tenant compte de ce
8+
<a href="https://sourcecodeoer.github.io/sourcecode_api/#operation/createMultipleExercises" target="_blank">
9+
format</a>. Votre fichier doit être en UTF-8 !
10+
</p>
11+
<ValidationObserver ref="observer"
12+
tag="form"
13+
@submit.prevent="validateBeforeSubmit">
14+
15+
<FileInput
16+
ref="fileObserver"
17+
rules="required|mimes:application/json"
18+
name="fichier json"
19+
@input="updateFile">
20+
<span class="label__name">
21+
Uploadez votre fichier (json)
22+
</span>
23+
</FileInput>
24+
25+
</ValidationObserver>
26+
27+
<p class="disclaimer">* champs obligatoires</p>
28+
<div class="cta__validate--wrapper">
29+
<button @click="validateBeforeSubmit"
30+
class="button--ternary-color-reverse cta__validate">
31+
Importer les exercices
32+
</button>
33+
</div>
34+
</section>
35+
36+
</template>
37+
38+
<script lang="ts">
39+
40+
import {Component, Vue, Prop, Ref} from "vue-property-decorator";
41+
import {ValidationObserver} from 'vee-validate';
42+
import {AxiosError} from "axios";
43+
import Icon from "~/components/Symbols/Icon.vue";
44+
import FileInput from "~/components/Input/FileInput.vue";
45+
46+
@Component({
47+
components: {FileInput, ValidationObserver, Icon}
48+
})
49+
export default class ExerciseForm extends Vue {
50+
/**
51+
* Validation Observer for the zip archive and the url
52+
*/
53+
@Ref() observer!: InstanceType<typeof ValidationObserver>;
54+
55+
@Ref() fileObserver!: FileInput;
56+
57+
@Prop({type: String, required: true}) title!: string;
58+
59+
form: {file: File | null} = {
60+
file: null
61+
};
62+
63+
updateFile(file: File | null) {
64+
this.form.file = file;
65+
}
66+
67+
/**
68+
* Validate the entire page and send the new exercise
69+
*/
70+
async validateBeforeSubmit() {
71+
72+
// Basic validation form
73+
const isValid = await this.observer.validate();
74+
75+
let reader = new FileReader();
76+
77+
if (isValid) {
78+
79+
const file: File | null = this.form.file;
80+
81+
if (file !== null) {
82+
reader.onloadend = async () => {
83+
if (reader.result !== null) {
84+
const buffer = reader.result as ArrayBuffer;
85+
const string: string = new TextDecoder().decode(buffer);
86+
87+
try {
88+
this.$nuxt.$loading.start();
89+
await this.$axios.$post("/api/bulk/create_exercises", JSON.parse(string));
90+
this.$displaySuccess("L'importation s'est correctement déroulée.");
91+
92+
this.$nextTick(() => {
93+
this.form.file;
94+
// @ts-ignore
95+
this.fileObserver.deleteFile();
96+
this.observer.reset();
97+
})
98+
} catch (e) {
99+
const error = e as AxiosError;
100+
101+
if (error.response) {
102+
const status = error.response.status;
103+
104+
if (status === 400) {
105+
this.$displayWarning("Votre fichier ne possède pas le bon format.")
106+
} else if (status === 401) {
107+
this.$displayWarning("Vous n'êtes pas autorisé à effectuer cette action.")
108+
} else if (status === 500) {
109+
this.$displayError("Une erreur est survenue depuis nos serveurs.")
110+
} else {
111+
this.$displayError("Une erreur est survenue.")
112+
}
113+
} else {
114+
this.$displayError("Le contenu de votre fichier n'est pas correct.")
115+
}
116+
} finally {
117+
this.$nuxt.$loading.finish();
118+
}
119+
120+
}
121+
};
122+
123+
reader.readAsArrayBuffer(file);
124+
125+
}
126+
127+
requestAnimationFrame(() => {
128+
this.observer.reset();
129+
});
130+
131+
} else {
132+
this.$displayWarning("Votre fichier ne possède pas le bon format.", {time: 5000})
133+
}
134+
}
135+
}
136+
137+
</script>
138+
139+
<style lang="scss" scoped>
140+
@import "../../assets/css/exercise-gestion";
141+
</style>

components/Input/FileInput.vue

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<template>
2+
<ValidationProvider ref="fileObserver" :tag="tag" :rules="rules" :name="name" :vid="vid" v-slot="{ errors }">
3+
<slot></slot>
4+
<input :id="name" name="archive" ref="inputFile" @change="selectedFile" class="input--secondary-color input__file"
5+
type="file">
6+
<label :for="name">
7+
<Icon type="archive" theme="theme--white"/>
8+
{{labelFileText}}</label>
9+
<span class="error-message">{{errors[0]}}</span>
10+
<slot name="footer"></slot>
11+
<span class="message message--red" v-if="filename"
12+
style="text-decoration: underline; cursor: pointer;"
13+
@click="deleteFile">Supprimer le fichier</span>
14+
</ValidationProvider>
15+
</template>
16+
17+
<script lang="ts">
18+
import {ValidationProvider} from 'vee-validate';
19+
import {Component, Emit, Prop, Ref, Vue} from "vue-property-decorator";
20+
import Icon from "~/components/Symbols/Icon.vue";
21+
22+
@Component({
23+
components: {
24+
Icon,
25+
ValidationProvider
26+
}
27+
})
28+
export default class TextInput extends Vue {
29+
30+
@Prop({type: [String, Object], default: ''}) rules!: string | object;
31+
@Prop({type: String, default: ''}) name!: string;
32+
@Prop({type: String, default: undefined}) vid!: string | undefined;
33+
@Prop({type: String, default: 'div'}) tag!: string;
34+
@Prop({type:String, default: ''}) defaultFilename!:string;
35+
36+
/**
37+
* Observer for the input file element
38+
*/
39+
@Ref() inputFile!: HTMLInputElement;
40+
/**
41+
* Validation Observer for the zip archive and the url
42+
*/
43+
@Ref() fileObserver!: InstanceType<typeof ValidationProvider>;
44+
45+
/**
46+
* The name of the uploaded file
47+
* Default is null
48+
*/
49+
filename: string | null = null;
50+
51+
/**
52+
* Returns the name of the uploaded file or a default message instead
53+
*/
54+
protected get labelFileText() {
55+
if (this.filename !== null) {
56+
if (this.filename.length > 18) {
57+
return this.filename.slice(0, 18) + '...'
58+
}
59+
60+
return this.filename
61+
}
62+
63+
return 'Choisir un fichier...'
64+
}
65+
66+
67+
@Emit()
68+
input(file: File | null) {
69+
return file;
70+
}
71+
72+
/**
73+
* Get the file from the input file element
74+
*/
75+
file(): File | null {
76+
const inputFile: any = this.fileObserver.value;
77+
if (!inputFile) {
78+
return null;
79+
}
80+
return inputFile
81+
}
82+
83+
/**
84+
* Event for the changed state of the input file
85+
*/
86+
async selectedFile() {
87+
const inputElement: HTMLInputElement = this.inputFile;
88+
const files = inputElement.files;
89+
if (files !== null) {
90+
const file: File | null = files.item(0);
91+
this.filename = file !== null ? file.name : null;
92+
await this.fileObserver.validate(file);
93+
this.input(file);
94+
}
95+
}
96+
97+
/**
98+
* Delete file from input and reset the filename
99+
*/
100+
deleteFile() {
101+
this.$nextTick(async () => {
102+
this.filename = null;
103+
this.inputFile.files = null;
104+
this.inputFile.value = '';
105+
this.fileObserver.value = undefined;
106+
this.fileObserver.reset();
107+
this.input(null);
108+
});
109+
}
110+
111+
mounted() {
112+
this.filename = this.defaultFilename === '' ? null : this.defaultFilename;
113+
}
114+
115+
}
116+
</script>
117+
118+
119+
<style lang="scss" scoped>
120+
label {
121+
align-self: flex-start;
122+
}
123+
</style>

components/Input/TextInput.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<template>
2+
<ValidationProvider :tag="tag" :rules="rules" :name="name" :vid="vid" v-slot="{ errors }">
3+
<slot></slot>
4+
<input class="input--grey" :type="type" :placeholder="placeholder" v-model="currentValue">
5+
<span class="error-message">{{ errors[0] }}</span>
6+
</ValidationProvider>
7+
</template>
8+
9+
<script lang="ts">
10+
import {ValidationProvider} from 'vee-validate';
11+
import {Component, Emit, Prop, Vue, Watch} from "vue-property-decorator";
12+
13+
@Component({
14+
components: {
15+
ValidationProvider
16+
}
17+
})
18+
export default class TextInput extends Vue {
19+
20+
@Prop({type: [String, Object], default: ''}) rules!: string | object;
21+
@Prop({type: String, default: ''}) name!: string;
22+
@Prop({type: String, default: undefined}) vid!: string | undefined;
23+
@Prop({type: String, default: 'text'}) type!: string;
24+
@Prop({type: String, default: 'div'}) tag!: string;
25+
@Prop({type:String, default: ''}) defaultValue!:string;
26+
@Prop({type:String, default: ''}) placeholder!:string;
27+
28+
currentValue: string = '';
29+
30+
@Watch('currentValue')
31+
OnCurrentValueChange(val: string) {
32+
this.input(val);
33+
}
34+
35+
@Emit()
36+
input(value: string) {
37+
return value;
38+
}
39+
40+
mounted() {
41+
this.currentValue = this.defaultValue;
42+
}
43+
44+
}
45+
</script>

components/Menu.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@
5353
</div>
5454
Exercices
5555
</nuxt-link>
56+
<nuxt-link class="cta-link cta-link-with-arrow" tag="li" to="/administration/importer-des-exercices">
57+
<div class="logo-link-wrapper">
58+
<Icon type="upload" theme="theme--white"/>
59+
</div>
60+
Importer
61+
</nuxt-link>
5662
<nuxt-link class="cta-link cta-link-with-arrow" tag="li" :class="isAdministrationCategoryLink" to="/administration/categories">
5763
<div class="logo-link-wrapper">
5864
<Icon type="bookmark" theme="theme--white"/>

components/Symbols/Icon.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
mail: () => import('./library/MailSymbol.vue'),
4444
clock: () => import('./library/ClockSymbol.vue'),
4545
info: () => import('./library/InfoSymbol.vue'),
46+
upload: () => import('./library/UploadSymbol.vue'),
4647
book: () => import('./library/BookSymbol.vue')
4748
}
4849
})

0 commit comments

Comments
 (0)