Commit f57fc659 authored by GuiΩ's avatar GuiΩ
Browse files

feat(evolution): add an EvolutionDialog

to show all questions and their results for given theme and classe
parent d2f9f5b7
......@@ -16,7 +16,7 @@ from .forms import UtilisateurCreationForm, UtilisateurChangeForm
from .models import Question, Classe, Feuille, Theme, QuestionPosee, Seance, LatexModel, QuestionOrder, \
LatexModelPart, Utilisateur
from .serializers import SeanceSerializerR, ThemeSerializer, LatexModelSerializer, \
LatexModelPartSerializer
LatexModelPartSerializer, QuestionPoseeSerializer
def index(_):
......@@ -348,7 +348,6 @@ class SeanceViewSetR(ModelViewSet):
queryset = queryset.filter(feuille__id=params['feuille_id'])
return queryset
class ThemeViewSet(ModelViewSet):
serializer_class = ThemeSerializer
queryset = Theme.objects.all()
......@@ -357,7 +356,28 @@ class ThemeViewSet(ModelViewSet):
@action(methods=['get'], detail=True)
def question_list(self, request, pk):
questions = Question.objects.filter(themes__id=pk)
return Response(questions.values_list('id', flat=True))
return Response(questions.values_list('id', flat=True))\
@action(methods=['get'], detail=True)
def get_evolution(self, request, pk):
queue = [pk]
themeIds = []
while len(queue):
theme = Theme.objects.get(pk=queue.pop())
if len(theme.enfants.all()):
for enfant in theme.enfants.all():
queue.append(enfant.id)
else:
themeIds.append(theme.id)
qpIds = []
for feuille in Classe.objects.get(pk=request.query_params['classe_id']).feuilles.all():
for seance in feuille.seances.all():
for qp in seance.questionsPosees.filter(question__themes__id__in=themeIds):
qpIds.append(qp.id)
return Response(
QuestionPoseeSerializer(QuestionPosee.objects.filter(id__in=qpIds)
.order_by('seance__feuille__position', 'seance__position', 'position'),
many=True).data)
@action(detail=True, methods=['get'])
def get_next_position(self, request, pk):
......
......@@ -20,10 +20,13 @@
async function show(event: MouseEvent, options: IContextMenuOption[]) {
state.options = options
root.value!.style.left = (event.pageX || event.clientX) + 'px'
root.value!.style.top = ((event.pageY || event.clientY) - window.scrollY) + 'px'
let left = event.pageX || event.clientX
let top = (event.pageY || event.clientY) - window.scrollY
state.visible = true
await nextTick()
const bb = root.value!.getBoundingClientRect()
root.value!.style.left = (left - Math.max(0, left + bb.width - window.innerWidth)) + 'px'
root.value!.style.top = (top - Math.max(0, top + bb.height-window.innerHeight)) + 'px'
root.value!.focus()
}
......@@ -62,33 +65,30 @@
</template>
<style>
.ContextMenu {
position: fixed;
background: #eff0f1;
z-index: 999;
outline: none;
border-radius: 5px;
border: 1px solid gray;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.8);
cursor: pointer;
}
.hidden {
display: none;
}
.ContextMenu_item {
padding: 5px 15px;
}
.ContextMenu_item__disabled, .ContextMenu_item__disabled:hover {
background-color: #eff0f1;
color: gray;
cursor: default;
}
.ContextMenu_item:hover{
background-color: lightskyblue;
}
<style lang="sass">
.ContextMenu
position: fixed
background: #eff0f1
z-index: 999
outline: none
border-radius: 5px
border: 1px solid gray
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.8)
cursor: pointer
.hidden
display: none
.ContextMenu_item
padding: 5px 15px
width: max-content
&:hover
background-color: lightskyblue
.ContextMenu_item__disabled, .ContextMenu_item__disabled:hover
background-color: #eff0f1
color: gray
cursor: default
</style>
\ No newline at end of file
<!--
- Copyright (c) 2022.
- GuiΩ guillaume.claus@worldonline.fr
-->
<script setup lang="ts">
import {computed, reactive, ref} from "vue"
import Modal from '@/components/Modal.vue'
import type {ClasseID, ThemeID} from "../types"
import {useThemesStore} from "../store/themes";
import {useClassesStore} from "@/store/classes";
import EvolutionDialogQP from "@/components/EvolutionDialogQP.vue"
interface IEvolutionDialogState {
themeId?: ThemeID,
classeId?: ClasseID
}
const themes = useThemesStore()
const classes = useClassesStore()
const modal = ref<InstanceType<typeof Modal> | null>(null)
const body = ref<HTMLDivElement | null>(null)
const state: IEvolutionDialogState = reactive({
})
const questionPosees = computed(() => themes.getEvolution)
const themePath = computed(() => {
if (state.themeId) {
let theme = themes.get(state.themeId)
let themeStr = theme.label
while (theme.parent! > 1) {
theme = themes.get(theme.parent!)
themeStr = theme.label + ' > ' + themeStr
}
return themeStr
}
return undefined
})
const classe = computed(() => {
if (state.classeId)
return classes.get(state.classeId)
return undefined
})
function show(themeId: ThemeID, classeId: ClasseID) {
console.debug('ED.show', themeId, classeId)
state.themeId = themeId
state.classeId = classeId
themes.loadEvolution(themeId, classeId)
modal.value!.show()
}
function close() {
console.debug('ED.close')
modal.value!.close()
}
function onResize({width: width, height: height}: {width: number, height: number}) {
body.value!.style.height = (height - 130) + 'px'
}
defineExpose({ show, close })
</script>
<template lang="pug">
Modal(
ref="modal"
@resize="onResize"
:closeOnBlur="true")
template(v-slot:title) Évolution de la classe {{ classe?.nom }} pour le thème "{{ themePath }}"
template(v-slot:body)
.EvolutionDialog__body(ref="body")
EvolutionDialogQP(
v-for="questionPosee in questionPosees"
:key="questionPosee.id"
:questionPosee="questionPosee"
:close="close"
@show-question-tooltip="$emit('show-question-tooltip', $event)")
template(v-slot:footer)
.EvolutionDialog__footer
button(data-testid="cancel" type="button" @click="close") Fermer
</template>
<style lang="sass">
.EvolutionDialog__body
height: 500px
padding: 0
overflow: auto
display: flex
flex-direction: column
</style>
\ No newline at end of file
<!--
- Copyright (c) 2022.
- GuiΩ guillaume.claus@worldonline.fr
-->
<script setup lang="ts">
import {computed, defineProps} from "vue";
import {useQuestionsStore} from "@/store/questions";
import type {IQuestionPosee} from "@/types";
import Score from '@/components/Score.vue'
import {useFeuillesStore} from "@/store/feuilles";
const questions = useQuestionsStore()
const feuilles = useFeuillesStore()
const emit = defineEmits(['loaded', 'show-question-tooltip'])
const props = defineProps<{
questionPosee: IQuestionPosee,
close: () => void
}>()
const question = computed(() => questions.get(props.questionPosee?.question))
const date = computed(() => {
const dateSeance = feuilles.getSeance(props.questionPosee!.seance)?.date
if (dateSeance) {
const dateObj = new Date(dateSeance)
return 'Le ' + Intl.DateTimeFormat('fr-FR').format(dateObj)
}
return 'Date non renseignée'
})
function onClick(event: MouseEvent) {
console.debug('EDQP.onClick', event)
emit('show-question-tooltip', {event: event, questionId: props.questionPosee?.question, onClose: props.close})
}
</script>
<template lang="pug">
.EvolutionDialogQP
.EvolutionDialog__date {{ date ? (date) : 'Date non précisée' }}
Score.EvolutionDialog__score(:score="props.questionPosee.score")/
img.EvolutionDialog__questionThumb(
:alt="'question ' + question.id"
:src="'/static/thumbs/' + question.thumb_name"
@load="$emit('loaded', question.id)"
@click="onClick")/
</template>
<style lang="sass">
.EvolutionDialogQP
display: grid
grid-template-columns: 150px 160px 215px
align-items: center
justify-content: center
.EvolutionDialog__date
justify-self: center
.EvolutionDialog__questionThumb
margin: 5px 0 0 0
</style>
\ No newline at end of file
......@@ -35,6 +35,8 @@ Events:
const emit = defineEmits([
'show-question-tooltip',
'show-context-menu',
'question-drop',
'question-edit',
'loaded'
])
......
......@@ -26,6 +26,8 @@ Events:
'show-question-tooltip',
'show-context-menu',
'set-selection',
'question-drop',
'question-edit',
'copy',
'loaded'
])
......@@ -41,7 +43,6 @@ Events:
const feuille = computed(() => feuilles.get(props.feuilleId) )
const isDeletable = computed(() => {
console.log('isDeletable', feuille.value)
for (const seanceId of feuille.value.seances) {
const seance = feuilles.getSeance(seanceId)!
if (seance)
......
......@@ -53,6 +53,8 @@ Events:
'set-selection',
'show-context-menu',
'show-question-tooltip',
'question-drop',
'question-edit',
'copy'])
const props = defineProps<{
......@@ -258,15 +260,6 @@ Events:
else
undoables.addSeance(seances[0], 'Insérer une séance')
}
// // pour débugage
// function onShowContextMenu(event) {
// console.log('FLFS.onShowContextMenu', event);
// // emit('show-context-menu', event)
// }
</script>
<template lang="pug">
......@@ -397,11 +390,6 @@ Events:
line-height: 150%
.tools .button
//display: inline-block
//margin: auto
a
cursor: pointer
......
......@@ -248,29 +248,6 @@ Events:
.QuestionSlot_questionPosee
text-align: center
/* Affichage du score : */
/* si non évalué : gris */
.score
display: block
max-height: 5px
width: 60%
height: 3px
background: lightgray
border: 1px solid gray
margin: auto
margin-top: 4px
.evaluee
background: red
/* et score en vert */
.scorevalue
height: 100%
background: greenyellow
.QuestionSlot_questionVide
display: block
text-align: center
......
......@@ -15,7 +15,8 @@
import {useUndoablesStore} from "@/store/undoables";
import type {ClasseID, IContextMenuOption, IQuestionDropEvent, IQuestionPosee, QuestionID, SeanceID, ThemeID} from "@/types";
import ContextMenu from "@/components/ContextMenu.vue";
import MessageBox from "@/components/MessageBox.vue";
import MessageBox from "@/components/MessageBox.vue"
import EvolutionDialog from '@/components/EvolutionDialog.vue'
const classes = useClassesStore()
const themes = useThemesStore()
......@@ -25,6 +26,7 @@
const questionTooltip = ref<InstanceType<typeof QuestionTooltip> | null>(null)
const questionEditor = ref<InstanceType<typeof QuestionEditor> | null>(null)
const questionList = ref<InstanceType<typeof QuestionList> | null>(null)
const evolutionDialog = ref<InstanceType<typeof EvolutionDialog> | null>(null)
interface IMainPageState {
onLoaded?: () => void
......@@ -37,12 +39,13 @@
return [themes.getSelectedId]
})
function onShowQuestionTooltip({event: event, questionId: questionId}: {event: MouseEvent, questionId: QuestionID}) {
function onShowQuestionTooltip(options: {event: MouseEvent, questionId: QuestionID, onClose?: () => void}) {
console.debug('MP.onShowQuestionTooltip', options)
if (questionTooltip.value === null) {
console.error('MainPage.onShowQuestionTooltip: questionTooltip is null')
return
}
questionTooltip.value.show(event, questionId)
questionTooltip.value.show(options)
}
function onShowContextMenu({event: event, options: options}: {event: MouseEvent, options: IContextMenuOption[]}) {
......@@ -58,6 +61,10 @@
msgBox.value?.show(title, message);
}
function onShowEvolutionDialog({themeId: themeId, classeId: classeId}: {themeId: ThemeID, classeId: ClasseID}) {
console.debug('MP.onShowEvolutionDialog', themeId, classeId)
evolutionDialog.value!.show(themeId, classeId)
}
async function onClasseChanged(classeId: ClasseID) {
classes.setSelectedId(classeId)
......@@ -170,7 +177,14 @@ QuestionEditor.QuestionEditor(
QuestionTooltip(
ref="questionTooltip"
@set-selection="setQuestionSelection")/
@set-selection="setQuestionSelection"
@show-evolution-dialog="onShowEvolutionDialog"
z-index="1000")/
EvolutionDialog(
ref="evolutionDialog"
@show-question-tooltip="onShowQuestionTooltip")/
#main-page
Toolbar.Toolbar(
@classe-changed="onClasseChanged")/
......@@ -178,7 +192,9 @@ QuestionTooltip(
.Selector
ThemeTree.Selector_theme(
:selection-ids="selectionThemeId"
@selection-changed="themes.setSelectedTheme($event[0])")/
@selection-changed="themes.setSelectedTheme($event[0])"
@show-context-menu="onShowContextMenu"
@show-evolution-dialog="onShowEvolutionDialog")/
.Selector_question
button(data-testid="newQuestion" @click="$refs.questionEditor.edit()") Nouvelle question
QuestionList.Selector_questionList(
......
......@@ -52,7 +52,7 @@
}
function onMouseDown(event: MouseEvent) {
console.debug('Modal.onMouseDown', event, modal.value!.getBoundingClientRect())
console.debug('Modal.onMouseDown', event)
const bb = modal.value!.getBoundingClientRect()
const x = event.clientX
const y = event.clientY
......@@ -65,7 +65,6 @@
}
function onMouseMove(event:MouseEvent) {
console.debug('Modal.onMouseMove', event)
const bb = modal.value!.getBoundingClientRect()
const x = event.clientX
const y = event.clientY
......
......@@ -3,14 +3,17 @@ import {computed, defineEmits, defineProps, nextTick, reactive, ref, watch} from
import {useQuestionsStore} from "@/store/questions";
import {useThemesStore} from "@/store/themes";
import type {QuestionID, ThemeID} from "@/types";
import {useClassesStore} from "@/store/classes";
interface IQuestionTooltipState {
visible: boolean
questionId?: QuestionID,
onClose?: () => void,
}
const questions = useQuestionsStore()
const themes = useThemesStore()
const classes = useClassesStore()
const state: IQuestionTooltipState = reactive({
......@@ -20,7 +23,7 @@ import type {QuestionID, ThemeID} from "@/types";
const root = ref<HTMLElement | null>(null)
const emit = defineEmits(['set-selection', 'update:show'])
const emit = defineEmits(['set-selection', 'update:show', 'show-evolution-dialog'])
const question = computed(() => {
if (state.questionId)
......@@ -36,79 +39,94 @@ import type {QuestionID, ThemeID} from "@/types";
}
function close() {
state.visible = false;
console.debug('QT.close')
state.visible = false
if (state.onClose)
state.onClose()
}
async function show(event: MouseEvent, questionId: QuestionID) {
state.questionId = questionId
root.value!.style.left = (event.pageX || event.clientX) + 'px'
root.value!.style.top = ((event.pageY || event.clientY) - window.scrollY) + 'px'
async function show(options: {event: MouseEvent, questionId: QuestionID, onClose?: () => void}) {
console.debug('QT.show', options)
state.questionId = options.questionId
state.onClose = options.onClose
let left = options.event.pageX || options.event.clientX
let top = (options.event.pageY || options.event.clientY) - window.scrollY
state.visible = true
await nextTick()
const bb = root.value!.getBoundingClientRect()
root.value!.style.left = (left - Math.max(0, left + bb.width - window.innerWidth)) + 'px'
root.value!.style.top = (top - Math.max(0, top + bb.height-window.innerHeight)) + 'px'
root.value?.focus()
}
function themePath(themeId: ThemeID) {
var path = '';
var path = [];
while (themeId !== 1) {
path.unshift(themeId)
let currTheme = themes.get(themeId)
if (path !== '')
path = ' > ' + path
path = currTheme.label + path
themeId = currTheme.parent!
}
return path
}
defineExpose({show, close})
</script>
<template lang="pug">
#question-tooltip(
.QuestionTooltip(
ref="root"
v-show="state.visible"
tabindex="0"
@blur="close")
div(v-if="question")
.QuestionTooltip__question(v-if="question")
|Question {{ question.id }} (auteur&nbsp;: {{ question.auteur }},
|{{ question.validee ? 'validée' : (question.avalider ? 'en attente de validation' : 'en cours de rédaction') }})&nbsp;
.themes Themes&nbsp;:
div.link(
v-for="question_order in question.themes"
@click="setSelection(question_order.theme, state.questionId)")
| {{ themePath(question_order.theme) }}
.QuestionTooltip__themes Themes&nbsp;:
.QuestionTooltip__theme.QuestionTooltip__link(
v-for="question_order in question.themes"
:title="'Ouvrir le thème \"' + themes.get(question_order.theme).label + '\" dans la zone de sélection'"
@click="setSelection(question_order.theme, state.questionId)")
template(v-for="themeId in themePath(question_order.theme)")
.QuestionTooltip__themeLabel {{ themes.get(themeId).label }} &nbsp;
img.QuestionTooltip__link(
src="/static/evolution.png"
:title="'Voir l\\'évolution pour \"' + themes.get(themeId).label + '\"'"
@click="$emit('show-evolution-dialog', {themeId: themeId, classeId: classes.getSelectedId})")
span(v-if="themeId !== question_order.theme") &nbsp;>&nbsp
</template>
<style scoped>
#question-tooltip {
position: fixed;
background: #eff0f1;
z-index: 999;
outline: none;
border-radius: 5px;
border: 1px solid gray;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.8);
padding-left: 5px;
text-align: left;
}
#question-tooltip div {
padding: 5px 15px;
}
.link {
cursor: pointer;
border-radius: 3px;
}
.link:hover {
background-color: lightskyblue;
}
.atrier {
color: #b30000;