From 5982fa92fd0d34ac23cf813807945cb16ed271cf Mon Sep 17 00:00:00 2001 From: Dominik Seeger <dominik.seeger@gmx.net> Date: Thu, 25 Apr 2019 18:00:03 +0200 Subject: [PATCH] added labelling system components and vuex modules --- core/migrations/0015_feedbacklabel_colour.py | 19 +++ core/models/label.py | 7 ++ core/serializers/label.py | 3 +- frontend/src/api.ts | 6 +- .../feedback_labels/FeedbackLabel.vue | 25 ++++ .../feedback_labels/FeedbackLabelsList.vue | 24 ++++ .../feedback_labels/LabelSelector.vue | 46 ++++++++ .../submission_notes/SubmissionCorrection.vue | 6 + .../base/BaseAnnotatedSubmission.vue | 1 + .../submission_notes/base/CommentForm.vue | 109 ++++++++++-------- .../toolbars/AnnotatedSubmissionLabels.vue | 48 ++++++++ frontend/src/models.ts | 16 ++- .../pages/base/TutorReviewerBaseLayout.vue | 3 + frontend/src/store/modules/feedback-labels.ts | 44 +++++++ .../src/store/modules/submission-notes.ts | 13 ++- frontend/src/store/store.ts | 8 +- 16 files changed, 316 insertions(+), 62 deletions(-) create mode 100644 core/migrations/0015_feedbacklabel_colour.py create mode 100644 frontend/src/components/feedback_labels/FeedbackLabel.vue create mode 100644 frontend/src/components/feedback_labels/FeedbackLabelsList.vue create mode 100644 frontend/src/components/feedback_labels/LabelSelector.vue create mode 100644 frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue create mode 100644 frontend/src/store/modules/feedback-labels.ts diff --git a/core/migrations/0015_feedbacklabel_colour.py b/core/migrations/0015_feedbacklabel_colour.py new file mode 100644 index 00000000..2d4715b5 --- /dev/null +++ b/core/migrations/0015_feedbacklabel_colour.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.4 on 2019-04-30 17:01 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_feedbacklabel'), + ] + + operations = [ + migrations.AddField( + model_name='feedbacklabel', + name='colour', + field=models.CharField(default='#b0b0b0', max_length=7, validators=[django.core.validators.RegexValidator(code='nomatch', message='Colour must be in format: #[0-9A-F]{7}', regex='^#[0-9A-F]{6}$')]), + ), + ] diff --git a/core/models/label.py b/core/models/label.py index 3ee53eb0..4893fb9f 100644 --- a/core/models/label.py +++ b/core/models/label.py @@ -1,14 +1,21 @@ import logging +from django.core.validators import RegexValidator from django.db import models from core.models.feedback import Feedback, FeedbackComment log = logging.getLogger(__name__) +HexColourValidator = RegexValidator( + regex='^#[0-9A-F]{6}$', + message='Colour must be in format: #[0-9A-F]{7}', + code='nomatch') + class FeedbackLabel(models.Model): name = models.CharField(max_length=50) description = models.TextField() + colour = models.CharField(validators=[HexColourValidator], max_length=7, default='#b0b0b0') feedback = models.ManyToManyField(Feedback, related_name='labels') feedback_comments = models.ManyToManyField(FeedbackComment, related_name='labels') diff --git a/core/serializers/label.py b/core/serializers/label.py index 3d358c8a..9aad4762 100644 --- a/core/serializers/label.py +++ b/core/serializers/label.py @@ -9,5 +9,6 @@ class LabelSerializer(serializers.ModelSerializer): fields = ( 'pk', 'name', - 'description' + 'description', + 'colour' ) diff --git a/frontend/src/api.ts b/frontend/src/api.ts index ff27c51a..22263bfd 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -11,7 +11,7 @@ import { Submission, SubmissionNoType, SubmissionType, Subscription, - Tutor, UserAccount + Tutor, UserAccount, FeedbackLabel } from '@/models' function getInstanceBaseUrl (): string { @@ -208,6 +208,10 @@ export async function changeActiveForUser (userPk: string, active: boolean): Pro return (await ax.patch(`/api/user/${userPk}/change_active/`, { 'is_active': active })).data } +export async function getLabels () { + return (await ax.get('/api/label')).data +} + export interface StudentExportOptions { setPasswords?: boolean } export interface StudentExportItem { Matrikel: string, diff --git a/frontend/src/components/feedback_labels/FeedbackLabel.vue b/frontend/src/components/feedback_labels/FeedbackLabel.vue new file mode 100644 index 00000000..bf14cbe0 --- /dev/null +++ b/frontend/src/components/feedback_labels/FeedbackLabel.vue @@ -0,0 +1,25 @@ +<template> + <v-tooltip top> + <v-chip + close + slot="activator" + > {{ name }} </v-chip> + <span> {{ description }} </span> + </v-tooltip> +</template> + +<script lang="ts"> +import Vue from "vue" +import Component from "vue-class-component" +import { Prop } from "vue-property-decorator" + +@Component +export default class FeedbackLabel extends Vue { + @Prop({ type: String, required: true }) readonly name!: string + @Prop({ type: String, required: true }) readonly description!: string +} +</script> + +<style> + +</style> diff --git a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue new file mode 100644 index 00000000..be2413e2 --- /dev/null +++ b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue @@ -0,0 +1,24 @@ +<template> + <div> + test test test + </div> +</template> + +<script lang="ts"> +import Vue from 'vue' +import Component from 'vue-class-component' +import { getLabels } from '@/api' +import { FeedbackLabels } from '@/store/modules/feedback-labels' + +@Component +export default class FeedbackLabelsList extends Vue { + async created() { + const labels = await getLabels() + FeedbackLabels.SET_LABELS(labels) + } +} +</script> + +<style> + +</style> diff --git a/frontend/src/components/feedback_labels/LabelSelector.vue b/frontend/src/components/feedback_labels/LabelSelector.vue new file mode 100644 index 00000000..0bc6e354 --- /dev/null +++ b/frontend/src/components/feedback_labels/LabelSelector.vue @@ -0,0 +1,46 @@ +<template> + <v-card> + <v-card-title>Assign labels</v-card-title> + <v-divider/> + <v-layout> + <v-flex ml-2 lg5> + <v-autocomplete + :items="feedbackLabels" + item-text="name" + item-value="pk" + append-icon="search" + placeholder="search for keywords" + @input="onChange" + /> + </v-flex> + <v-flex lg8> + + </v-flex> + </v-layout> + </v-card> +</template> + +<script lang="ts"> +import Vue from "vue"; +import Component from "vue-class-component"; +import { FeedbackLabels } from '@/store/modules/feedback-labels' +import { FeedbackLabel } from '@/models'; + +@Component +export default class LabelSelector extends Vue { + + get feedbackLabels() { + return FeedbackLabels.availableLabels + } + + onChange(val: string) { + const selectedLabel = this.feedbackLabels.find((label) => { + return label.pk === val + }); + console.log(selectedLabel) + } +} +</script> + +<style> +</style> diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index 5f0a7e37..d84324eb 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -42,6 +42,10 @@ </submission-line> </tr> </template> + <annotated-submission-labels + class="elevation-1" + slot="labels" + /> <annotated-submission-bottom-toolbar class="mt-1 elevation-1" slot="footer" @@ -61,6 +65,7 @@ import CommentForm from '@/components/submission_notes/base/CommentForm.vue' import FeedbackComment from '@/components/submission_notes/base/FeedbackComment.vue' import AnnotatedSubmissionTopToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionTopToolbar' import AnnotatedSubmissionBottomToolbar from '@/components/submission_notes/toolbars/AnnotatedSubmissionBottomToolbar' +import AnnotatedSubmissionLabels from '@/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue' import BaseAnnotatedSubmission from '@/components/submission_notes/base/BaseAnnotatedSubmission' import SubmissionLine from '@/components/submission_notes/base/SubmissionLine' import { SubmissionNotes } from '@/store/modules/submission-notes' @@ -74,6 +79,7 @@ export default { BaseAnnotatedSubmission, AnnotatedSubmissionBottomToolbar, AnnotatedSubmissionTopToolbar, + AnnotatedSubmissionLabels, FeedbackComment, CommentForm }, name: 'submission-correction', diff --git a/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue b/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue index 051427ea..267fe57b 100644 --- a/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue +++ b/frontend/src/components/submission_notes/base/BaseAnnotatedSubmission.vue @@ -4,6 +4,7 @@ <table class="submission-table elevation-1"> <slot name="table-content"/> </table> + <slot name="labels"/> <slot name="footer"/> </div> </template> diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue index 1e25cbd8..33628127 100644 --- a/frontend/src/components/submission_notes/base/CommentForm.vue +++ b/frontend/src/components/submission_notes/base/CommentForm.vue @@ -1,63 +1,70 @@ <template> - <div> - <v-textarea - name="feedback-input" - label="Please provide your feedback here" - v-model="currentFeedback" - @keyup.enter.ctrl.exact="submitFeedback" - @keyup.esc="collapseTextField" - @focus="selectInput($event)" - rows="2" - outline - autofocus - auto-grow - hide-details - /> - <v-btn id="submit-comment" color="success" @click="submitFeedback"><v-icon>check</v-icon>Submit</v-btn> - <v-btn id="cancel-comment" @click="collapseTextField"><v-icon>cancel</v-icon>cancel</v-btn> - </div> + <v-layout wrap> + <v-flex lg12 md12 sm12 mr-1> + <v-textarea + name="feedback-input" + label="Please provide your feedback here" + v-model="currentFeedback" + @keyup.enter.ctrl.exact="submitFeedback" + @keyup.esc="collapseTextField" + @focus="selectInput($event)" + rows="2" + outline + autofocus + auto-grow + hide-details + /> + </v-flex> + <v-flex lg10 md8 sm8 my-2> + <label-selector/> + </v-flex> + <v-flex lg2 ma-0> + <v-btn id="submit-comment" color="success" @click="submitFeedback"><v-icon>check</v-icon>Submit</v-btn> + <v-btn id="cancel-comment" @click="collapseTextField"><v-icon>cancel</v-icon>cancel</v-btn> + </v-flex> + </v-layout> </template> -<script> +<script lang="ts"> +import Vue from 'vue' +import Component from 'vue-class-component' +import { Prop, Watch } from 'vue-property-decorator' import { SubmissionNotes } from '@/store/modules/submission-notes' +import LabelSelector from "@/components/feedback_labels/LabelSelector.vue" -export default { - name: 'comment-form', - props: { - feedback: { - type: String, - default: '' - }, - lineNo: { - type: String, - required: true - } - }, - data () { - return { - currentFeedback: this.feedback +@Component({ + components: { + LabelSelector + } +}) +export default class CommentForm extends Vue { + @Prop({ type: String, default: '' }) readonly feedback!: string + @Prop({ type: String, required: true }) readonly lineNo!: string + + currentFeedback = this.feedback + + selectInput (event: Event) { + if (event !== null) { + const target = event.target as HTMLTextAreaElement + target.select() } - }, - methods: { - selectInput (event) { - if (event) { - event.target.select() + } + + collapseTextField () { + this.$emit('collapseFeedbackForm') + } + + submitFeedback () { + SubmissionNotes.UPDATE_FEEDBACK_LINE({ + lineNo: Number(this.lineNo), + comment: { + text: this.currentFeedback } - }, - collapseTextField () { - this.$emit('collapseFeedbackForm') - }, - submitFeedback () { - SubmissionNotes.UPDATE_FEEDBACK_LINE({ - lineNo: this.lineNo, - comment: { - text: this.currentFeedback - } - }) - this.collapseTextField() - } + }) + this.collapseTextField() } } + </script> <style scoped> diff --git a/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue new file mode 100644 index 00000000..57c9647c --- /dev/null +++ b/frontend/src/components/submission_notes/toolbars/AnnotatedSubmissionLabels.vue @@ -0,0 +1,48 @@ +<template> + <v-container> + <v-layout wrap> + <v-flex> + <feedback-label + v-for="(label, index) in labels" + v-bind="label" + :key="index" + /> + </v-flex> + </v-layout> + </v-container> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import Component from 'vue-class-component' +import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue" +import { SubmissionNotes } from '@/store/modules/submission-notes' +import { FeedbackLabels as Labels } from '@/store/modules/feedback-labels' + +@Component({ + components: { + FeedbackLabel + } +}) +export default class AnnotatedSubmissionLabels extends Vue { + // maps origFeedback's label pk's to the ones stored in vuex + get labels() { + const labels = SubmissionNotes.state.origFeedback.labels + if (labels) { + const mappedLabels = labels.map((val) => { + const label = Labels.availableLabels.find((label) => { + return Number(label.pk) === val + }) + + if (!label) return // TODO: throw error + return { pk: val, name: label.name, description: label.description } + }) + + return mappedLabels + } + + return {} + } +} +</script> + diff --git a/frontend/src/models.ts b/frontend/src/models.ts index a1d33255..ae98cc96 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -138,7 +138,8 @@ export interface Feedback { * @type {string} * @memberof Feedback */ - feedbackStageForUser?: string + feedbackStageForUser?: string, + labels?: number[], } /** @@ -183,6 +184,19 @@ export interface FeedbackComment { * @memberof FeedbackComment */ visibleToStudent?: boolean + labels?: FeedbackLabel[] +} + +/** + * + * @export + * @interface FeedbackLabel + */ +export interface FeedbackLabel { + pk: string + name: string + description: string + color: string } /** diff --git a/frontend/src/pages/base/TutorReviewerBaseLayout.vue b/frontend/src/pages/base/TutorReviewerBaseLayout.vue index 19ddf7b2..5f4899ff 100644 --- a/frontend/src/pages/base/TutorReviewerBaseLayout.vue +++ b/frontend/src/pages/base/TutorReviewerBaseLayout.vue @@ -21,6 +21,7 @@ <v-divider></v-divider> <slot name="above-subscriptions"></slot> <subscription-list :sidebar="true"/> + <feedback-labels-list/> <slot name="below-subscriptions"></slot> </template> <template slot="toolbar-right"><slot name="toolbar-right"></slot></template> @@ -30,10 +31,12 @@ <script> import BaseLayout from '@/components/BaseLayout' import SubscriptionList from '@/components/subscriptions/SubscriptionList' +import FeedbackLabelsList from '@/components/feedback_labels/FeedbackLabelsList.vue' export default { components: { SubscriptionList, + FeedbackLabelsList, BaseLayout }, name: 'tutor-reviewer-base-layout', data () { diff --git a/frontend/src/store/modules/feedback-labels.ts b/frontend/src/store/modules/feedback-labels.ts new file mode 100644 index 00000000..2056979c --- /dev/null +++ b/frontend/src/store/modules/feedback-labels.ts @@ -0,0 +1,44 @@ +import { FeedbackLabel } from '@/models'; +import { getStoreBuilder } from 'vuex-typex'; +import { RootState } from '../store'; +import Vue from 'vue'; + +export interface FeedbackLabelsState { + labels: FeedbackLabel[] +} + +function initialState(): FeedbackLabelsState { + return { + labels: [] + } +} + +const mb = getStoreBuilder<RootState>().module('FeedbackLabels', initialState()) + +const stateGetter = mb.state() + +const availableLabelsGetter = mb.read(function labels(state) { + return state.labels +}) + +function SET_LABELS(state: FeedbackLabelsState, labels: FeedbackLabel[]) { + state.labels = labels +} + +function ADD_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) { + state.labels.push(label) +} + +function REMOVE_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) { + +} + +export const FeedbackLabels = { + get state() { return stateGetter() }, + get availableLabels() { return availableLabelsGetter() }, + + SET_LABELS: mb.commit(SET_LABELS), + ADD_LABEL: mb.commit(ADD_LABEL), + REMOVE_LABEL: mb.commit(REMOVE_LABEL) +} + diff --git a/frontend/src/store/modules/submission-notes.ts b/frontend/src/store/modules/submission-notes.ts index 926cac6d..e7cd1e70 100644 --- a/frontend/src/store/modules/submission-notes.ts +++ b/frontend/src/store/modules/submission-notes.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import * as hljs from 'highlight.js' import * as api from '@/api' -import { Feedback, FeedbackComment, SubmissionNoType } from '@/models' +import { Feedback, FeedbackComment, SubmissionNoType, FeedbackLabel } from '@/models' import { RootState } from '@/store/store' import { getStoreBuilder, BareActionContext } from 'vuex-typex' import { syntaxPostProcess } from '@/util/helpers'; @@ -38,12 +38,14 @@ function initialState(): SubmissionNotesState { pk: 0, score: undefined, isFinal: false, - feedbackLines: {} + feedbackLines: {}, + labels: [], }, updatedFeedback: { pk: 0, score: undefined, - feedbackLines: {} + feedbackLines: {}, + labels: [], }, commentsMarkedForDeletion: {} } @@ -141,12 +143,13 @@ async function deleteComments({ state }: BareActionContext<SubmissionNotesState, } async function submitFeedback( { state }: BareActionContext<SubmissionNotesState, RootState>, -{ isFinal = false }): +{ isFinal = false, labels = [] }): Promise<AxiosResponse<void>[]> { let feedback: Partial<Feedback> = { isFinal: isFinal, - ofSubmission: state.submission.pk + ofSubmission: state.submission.pk, + labels: labels } if (state.origFeedback.score === undefined && state.updatedFeedback.score === undefined) { throw new Error('You need to give a score.') diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index e8f940af..401e1801 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -39,8 +39,9 @@ import { Statistics, StudentInfoForListView, SubmissionNoType, - SubmissionType, Tutor + SubmissionType, Tutor, FeedbackLabel } from '@/models' +import { FeedbackLabelsState } from './modules/feedback-labels'; Vue.use(Vuex) @@ -65,8 +66,9 @@ export interface RootState extends RootInitialState{ FeedbackTable: FeedbackTableState, Subscriptions: SubscriptionsState, SubmissionNotes: SubmissionNotesState, - StudentPage: StudentPageState - TutorOverview: TutorOverviewState + StudentPage: StudentPageState, + TutorOverview: TutorOverviewState, + FeedbackLabels: FeedbackLabelsState } export function initialState (): RootInitialState { -- GitLab