Skip to content
25 changes: 0 additions & 25 deletions assets/css/_exercise-gestion.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,31 +72,6 @@ form {
font-weight: bold;
}

label[for=Archive] {
align-self: flex-start;
}

.message {
margin-top: 10px;
font-style: italic;
font-size: .75em;

&.message--primary-color {
color: $PRIMARY_COLOR
}

&.message--red {
color: $RED
}
}

.error-message {
margin-top: 10px;

&.error-message--red {
color: $RED;
}
}
}

.validation__tag {
Expand Down
24 changes: 24 additions & 0 deletions assets/css/form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,30 @@ input[type=file] {
font-style: italic;
}


form {
.message {
margin-top: 10px;
font-style: italic;
font-size: .75em;

&.message--primary-color {
color: $PRIMARY_COLOR
}

&.message--red {
color: $RED
}
}

.error-message {
margin-top: 10px;

&.error-message--red {
color: $RED;
}
}
}
/**
Custom theme of textarea
*/
Expand Down
141 changes: 141 additions & 0 deletions components/Gestion/ImportForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<template>
<section class="content">

<h1>{{title}}</h1>

<p>
Vous pouvez importer des exercices depuis cette interface en tenant compte de ce
<a href="https://sourcecodeoer.github.io/sourcecode_api/#operation/createMultipleExercises" target="_blank">
format</a>. Votre fichier doit être en UTF-8 !
</p>
<ValidationObserver ref="observer"
tag="form"
@submit.prevent="validateBeforeSubmit">

<FileInput
ref="fileObserver"
rules="required|mimes:application/json"
name="fichier json"
@input="updateFile">
<span class="label__name">
Uploadez votre fichier (json)
</span>
</FileInput>

</ValidationObserver>

<p class="disclaimer">* champs obligatoires</p>
<div class="cta__validate--wrapper">
<button @click="validateBeforeSubmit"
class="button--ternary-color-reverse cta__validate">
Importer les exercices
</button>
</div>
</section>

</template>

<script lang="ts">

import {Component, Vue, Prop, Ref} from "vue-property-decorator";
import {ValidationObserver} from 'vee-validate';
import {AxiosError} from "axios";
import Icon from "~/components/Symbols/Icon.vue";
import FileInput from "~/components/Input/FileInput.vue";

@Component({
components: {FileInput, ValidationObserver, Icon}
})
export default class ExerciseForm extends Vue {
/**
* Validation Observer for the zip archive and the url
*/
@Ref() observer!: InstanceType<typeof ValidationObserver>;

@Ref() fileObserver!: FileInput;

@Prop({type: String, required: true}) title!: string;

form: {file: File | null} = {
file: null
};

updateFile(file: File | null) {
this.form.file = file;
}

/**
* Validate the entire page and send the new exercise
*/
async validateBeforeSubmit() {

// Basic validation form
const isValid = await this.observer.validate();

let reader = new FileReader();

if (isValid) {

const file: File | null = this.form.file;

if (file !== null) {
reader.onloadend = async () => {
if (reader.result !== null) {
const buffer = reader.result as ArrayBuffer;
const string: string = new TextDecoder().decode(buffer);

try {
this.$nuxt.$loading.start();
await this.$axios.$post("/api/bulk/create_exercises", JSON.parse(string));
this.$displaySuccess("L'importation s'est correctement déroulée.");

this.$nextTick(() => {
this.form.file;
// @ts-ignore
this.fileObserver.deleteFile();
this.observer.reset();
})
} catch (e) {
const error = e as AxiosError;

if (error.response) {
const status = error.response.status;

if (status === 400) {
this.$displayWarning("Votre fichier ne possède pas le bon format.")
} else if (status === 401) {
this.$displayWarning("Vous n'êtes pas autorisé à effectuer cette action.")
} else if (status === 500) {
this.$displayError("Une erreur est survenue depuis nos serveurs.")
} else {
this.$displayError("Une erreur est survenue.")
}
} else {
this.$displayError("Le contenu de votre fichier n'est pas correct.")
}
} finally {
this.$nuxt.$loading.finish();
}

}
};

reader.readAsArrayBuffer(file);

}

requestAnimationFrame(() => {
this.observer.reset();
});

} else {
this.$displayWarning("Votre fichier ne possède pas le bon format.", {time: 5000})
}
}
}

</script>

<style lang="scss" scoped>
@import "../../assets/css/exercise-gestion";
</style>
123 changes: 123 additions & 0 deletions components/Input/FileInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<template>
<ValidationProvider ref="fileObserver" :tag="tag" :rules="rules" :name="name" :vid="vid" v-slot="{ errors }">
<slot></slot>
<input :id="name" name="archive" ref="inputFile" @change="selectedFile" class="input--secondary-color input__file"
type="file">
<label :for="name">
<Icon type="archive" theme="theme--white"/>
{{labelFileText}}</label>
<span class="error-message">{{errors[0]}}</span>
<slot name="footer"></slot>
<span class="message message--red" v-if="filename"
style="text-decoration: underline; cursor: pointer;"
@click="deleteFile">Supprimer le fichier</span>
</ValidationProvider>
</template>

<script lang="ts">
import {ValidationProvider} from 'vee-validate';
import {Component, Emit, Prop, Ref, Vue} from "vue-property-decorator";
import Icon from "~/components/Symbols/Icon.vue";

@Component({
components: {
Icon,
ValidationProvider
}
})
export default class TextInput extends Vue {

@Prop({type: [String, Object], default: ''}) rules!: string | object;
@Prop({type: String, default: ''}) name!: string;
@Prop({type: String, default: undefined}) vid!: string | undefined;
@Prop({type: String, default: 'div'}) tag!: string;
@Prop({type:String, default: ''}) defaultFilename!:string;

/**
* Observer for the input file element
*/
@Ref() inputFile!: HTMLInputElement;
/**
* Validation Observer for the zip archive and the url
*/
@Ref() fileObserver!: InstanceType<typeof ValidationProvider>;

/**
* The name of the uploaded file
* Default is null
*/
filename: string | null = null;

/**
* Returns the name of the uploaded file or a default message instead
*/
protected get labelFileText() {
if (this.filename !== null) {
if (this.filename.length > 18) {
return this.filename.slice(0, 18) + '...'
}

return this.filename
}

return 'Choisir un fichier...'
}


@Emit()
input(file: File | null) {
return file;
}

/**
* Get the file from the input file element
*/
file(): File | null {
const inputFile: any = this.fileObserver.value;
if (!inputFile) {
return null;
}
return inputFile
}

/**
* Event for the changed state of the input file
*/
async selectedFile() {
const inputElement: HTMLInputElement = this.inputFile;
const files = inputElement.files;
if (files !== null) {
const file: File | null = files.item(0);
this.filename = file !== null ? file.name : null;
await this.fileObserver.validate(file);
this.input(file);
}
}

/**
* Delete file from input and reset the filename
*/
deleteFile() {
this.$nextTick(async () => {
this.filename = null;
this.inputFile.files = null;
this.inputFile.value = '';
this.fileObserver.value = undefined;
this.fileObserver.reset();
this.input(null);
});
}

mounted() {
this.filename = this.defaultFilename === '' ? null : this.defaultFilename;
}

}
</script>


<style lang="scss" scoped>
label {
align-self: flex-start;
}
</style>
45 changes: 45 additions & 0 deletions components/Input/TextInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<ValidationProvider :tag="tag" :rules="rules" :name="name" :vid="vid" v-slot="{ errors }">
<slot></slot>
<input class="input--grey" :type="type" :placeholder="placeholder" v-model="currentValue">
<span class="error-message">{{ errors[0] }}</span>
</ValidationProvider>
</template>

<script lang="ts">
import {ValidationProvider} from 'vee-validate';
import {Component, Emit, Prop, Vue, Watch} from "vue-property-decorator";

@Component({
components: {
ValidationProvider
}
})
export default class TextInput extends Vue {

@Prop({type: [String, Object], default: ''}) rules!: string | object;
@Prop({type: String, default: ''}) name!: string;
@Prop({type: String, default: undefined}) vid!: string | undefined;
@Prop({type: String, default: 'text'}) type!: string;
@Prop({type: String, default: 'div'}) tag!: string;
@Prop({type:String, default: ''}) defaultValue!:string;
@Prop({type:String, default: ''}) placeholder!:string;

currentValue: string = '';

@Watch('currentValue')
OnCurrentValueChange(val: string) {
this.input(val);
}

@Emit()
input(value: string) {
return value;
}

mounted() {
this.currentValue = this.defaultValue;
}

}
</script>
6 changes: 6 additions & 0 deletions components/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
</div>
Exercices
</nuxt-link>
<nuxt-link class="cta-link cta-link-with-arrow" tag="li" to="/administration/importer-des-exercices">
<div class="logo-link-wrapper">
<Icon type="upload" theme="theme--white"/>
</div>
Importer
</nuxt-link>
<nuxt-link class="cta-link cta-link-with-arrow" tag="li" :class="isAdministrationCategoryLink" to="/administration/categories">
<div class="logo-link-wrapper">
<Icon type="bookmark" theme="theme--white"/>
Expand Down
1 change: 1 addition & 0 deletions components/Symbols/Icon.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
mail: () => import('./library/MailSymbol.vue'),
clock: () => import('./library/ClockSymbol.vue'),
info: () => import('./library/InfoSymbol.vue'),
upload: () => import('./library/UploadSymbol.vue'),
book: () => import('./library/BookSymbol.vue')
}
})
Expand Down
Loading