diff --git a/core/migrations/0015_feedbacklabel_colour.py b/core/migrations/0015_feedbacklabel_colour.py new file mode 100644 index 0000000000000000000000000000000000000000..2d4715b56e28b3bed41bc4f322f4c06656a79d33 --- /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 3ee53eb0ef3610b97cbefe70d10acd8ca9b28ec4..4893fb9fabb626746e026df35816a8920e1a4ab5 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 3d358c8a6f4ba37d0c315fc48b59a65c1d256e19..9aad4762e61e86f641e3a7eac3cdbc0ab94b1e39 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 ff27c51addf78f4f50d1c550ff46d5cc19101b84..22263bfd91698bba027e9e8ece295627f8e1d58d 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 0000000000000000000000000000000000000000..bf14cbe05e6bc29f1cf5880318d4d11cb1e2299b --- /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 0000000000000000000000000000000000000000..be2413e2731f3f1e910469e9cd5210a7de6ba547 --- /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 0000000000000000000000000000000000000000..0bc6e3548fb173944bb59dd81437bb4a1c751bcd --- /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 5f0a7e370851826577ff780c97720c553feb6de3..d84324ebd76a165270a23a2c14f0534910d307db 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 051427ea2f5c46eb58b8e27bf7e7b20db4273166..267fe57b34ca721b04983c3c585e328379b1b2b0 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 1e25cbd8d61604da53ac4c8fd62c4ef9dc5c4cb6..33628127237ca3aadd9e0af88de75dc48e4b61aa 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 0000000000000000000000000000000000000000..57c9647c11bd7c245dd9ca71c4695f23adbc6f10 --- /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 a1d33255b5c32a53d57e9f0ebaa7be55573f5fd6..ae98cc96b3077d6c105d3f7c0f78151c6d8ddcc4 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 19ddf7b2e55fb306b68f7ee41b00ca8cfec342d1..5f4899ffab4b2f98314c7ac5cd69133ba446d5eb 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 0000000000000000000000000000000000000000..2056979cdba61f38b0115869455f35efdd2f130c --- /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 926cac6d1d1df23aa61ea282908a2f47fe8303b1..e7cd1e7089538282a0c68b4e09f37316dd0f5bd2 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 e8f940afa6a20214facb6f482d1292478d903752..401e1801a943fde1aee77ae4d10034660c020267 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 {