Skip to content
Snippets Groups Projects
Commit cf75b332 authored by robinwilliam.hundt's avatar robinwilliam.hundt
Browse files

fixed #149

also some quality of life ui improvements
parent 5fe644f4
Branches
Tags
1 merge request!163fixed #149
Pipeline #95128 failed
......@@ -502,7 +502,7 @@ class FeedbackCommentApiEndpointTest(APITestCase):
response = self.client.delete(self.url % comment.pk)
self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code)
def test_reviewer_can_delete_everything_they_want(self):
def test_reviewer_can_delete_other_users_comments(self):
reviewer = self.data['reviewers'][0]
self.client.force_authenticate(user=reviewer)
comment01 = FeedbackComment.objects.get(of_tutor=self.tutor01)
......@@ -512,10 +512,11 @@ class FeedbackCommentApiEndpointTest(APITestCase):
self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
response = self.client.delete(self.url % comment02.pk)
self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code)
self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code)
self.assertTrue(FeedbackComment.objects.filter(of_tutor=self.tutor02).exists(),
msg='Second comment should not be deleted for feedback with not full score')
try:
FeedbackComment.objects.get(of_tutor=self.tutor01)
FeedbackComment.objects.get(of_tutor=self.tutor02)
except FeedbackComment.DoesNotExist:
pass
else:
......
import logging
from multiprocessing import Lock
from rest_framework import mixins, status, viewsets
from rest_framework.exceptions import PermissionDenied
......@@ -143,3 +144,14 @@ class FeedbackCommentApiView(
serializer.is_valid()
serializer.save()
return Response(serializer.data)
def destroy(self, request, *args, **kwargs):
with Lock():
instance = self.get_object()
if instance.of_feedback.feedback_lines.count() == 1 and \
not instance.of_feedback.is_full_score():
raise PermissionDenied(detail="Last comment can not be deleted for submissions "
"with non full score")
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
<template>
<v-layout justify-center class="mg-bottom">
<v-layout justify-center>
<v-card class="mt-5">
<v-card-title class="title">
This is the student overview page!
......@@ -25,7 +25,7 @@ export default {
</script>
<style scoped>
.mg-bottom {
/* .mg-bottom {
margin-bottom: 25px;
}
} */
</style>
......@@ -146,6 +146,8 @@ export default {
duration: -1
})
}).finally(() => {
this.$emit('feedbackChanged')
SubmissionNotes.RESET_MARKED_COMMENTS_FOR_DELETE()
this.loading = false
})
},
......
......@@ -5,6 +5,7 @@
:submission-without-assignment="submission"
:feedback="submission.feedback"
@feedbackCreated="refresh"
@feedbackChanged="refresh"
:ignoreHiddenState="true"
/>
<submission-tests
......
......@@ -3,7 +3,7 @@
<v-flex xs6>
<student-list class="ma-1"></student-list>
</v-flex>
<v-flex xs6>
<v-flex xs6 class="right-view">
<router-view></router-view>
</v-flex>
</v-layout>
......@@ -51,5 +51,10 @@ export default {
</script>
<style scoped>
.right-view {
position: sticky;
top: 80px;
overflow-y: scroll;
height: 90vh;
}
</style>
......@@ -5,21 +5,22 @@ import { Feedback, FeedbackComment, SubmissionNoType } from '@/models'
import { RootState } from '@/store/store'
import { getStoreBuilder, BareActionContext } from 'vuex-typex'
import { syntaxPostProcess } from '@/util/helpers';
import { AxiosResponse } from 'axios';
export interface SubmissionNotesState {
submission: SubmissionNoType
ui: {
showEditorOnLine: {[lineNo: number]: boolean}
selectedCommentOnLine: {[lineNo: number]: FeedbackComment}
showFeedback: boolean
showEditorOnLine: { [lineNo: number]: boolean }
selectedCommentOnLine: { [lineNo: number]: FeedbackComment }
showFeedback: boolean
},
hasOrigFeedback: boolean
origFeedback: Feedback
updatedFeedback: Feedback
commentsMarkedForDeletion: {[pk: string]: FeedbackComment}
commentsMarkedForDeletion: { [pk: string]: FeedbackComment }
}
function initialState (): SubmissionNotesState {
function initialState(): SubmissionNotesState {
return {
submission: {
text: '',
......@@ -52,90 +53,97 @@ const mb = getStoreBuilder<RootState>().module('SubmissionNotes', initialState()
const stateGetter = mb.state()
const submissionTypeGetter = mb.read(function submissionType (state, getters, rootState) {
const submissionTypeGetter = mb.read(function submissionType(state, getters, rootState) {
return rootState.submissionTypes[state.submission.type]
})
// highlight the submission the reduce the string
// submission.text into an object where the keys are the
// line indexes starting at one and the values the corresponding submission line
// this makes iterating over the submission much more pleasant
const submissionGetter = mb.read(function submission (state, getters) {
const submissionGetter = mb.read(function submission(state, getters) {
const language = getters.submissionType
? getters.submissionType.programmingLanguage
: 'c'
const highlighted = hljs.highlight(language, state.submission.text || '', true).value
const postProcessed = syntaxPostProcess(highlighted);
const splitted = postProcessed.split('\n').reduce((acc: {[k: number]: string}, cur, index) => {
const splitted = postProcessed.split('\n').reduce((acc: { [k: number]: string }, cur, index) => {
acc[index + 1] = cur
return acc
}, {})
return splitted;
})
const scoreGetter = mb.read(function score (state) {
const scoreGetter = mb.read(function score(state) {
return state.updatedFeedback.score !== undefined ? state.updatedFeedback.score : state.origFeedback.score
})
const workInProgressGetter = mb.read(function workInProgress (state) {
const workInProgressGetter = mb.read(function workInProgress(state) {
const openEditor = Object.values(state.ui.showEditorOnLine).reduce((acc, curr) => acc || curr, false)
const feedbackWritten = Object.entries(state.updatedFeedback.feedbackLines || {}).length > 0
return openEditor || feedbackWritten
})
const isFeedbackCreationGetter = mb.read(function isFeedbackCreation (state) {
const isFeedbackCreationGetter = mb.read(function isFeedbackCreation(state) {
return !state.origFeedback['feedbackStageForUser'] ||
state.origFeedback['feedbackStageForUser'] === 'feedback-creation'
})
function SET_SUBMISSION (state: SubmissionNotesState, submission: SubmissionNoType) {
function SET_SUBMISSION(state: SubmissionNotesState, submission: SubmissionNoType) {
state.submission = submission
}
function SET_ORIG_FEEDBACK (state: SubmissionNotesState, feedback: Feedback) {
function SET_ORIG_FEEDBACK(state: SubmissionNotesState, feedback: Feedback) {
if (feedback) {
state.origFeedback = feedback
state.hasOrigFeedback = true
}
}
function SET_SHOW_FEEDBACK (state: SubmissionNotesState, val: boolean) {
function SET_SHOW_FEEDBACK(state: SubmissionNotesState, val: boolean) {
state.ui.showFeedback = val
}
function UPDATE_FEEDBACK_LINE (state: SubmissionNotesState, feedback: {lineNo: number, comment: Partial<FeedbackComment>}) {
function UPDATE_FEEDBACK_LINE(state: SubmissionNotesState, feedback: { lineNo: number, comment: Partial<FeedbackComment> }) {
// explicit .toString() on lineNo is necessary because of Vue.set typings
if (state.updatedFeedback.feedbackLines) {
Vue.set(state.updatedFeedback.feedbackLines, feedback.lineNo.toString(), feedback.comment)
}
}
function UPDATE_FEEDBACK_SCORE (state: SubmissionNotesState, score: number) {
function UPDATE_FEEDBACK_SCORE(state: SubmissionNotesState, score: number) {
state.updatedFeedback.score = score
}
function DELETE_FEEDBACK_LINE (state: SubmissionNotesState, lineNo: number) {
function DELETE_FEEDBACK_LINE(state: SubmissionNotesState, lineNo: number) {
if (state.updatedFeedback.feedbackLines) {
// Vue dosn't like objects that are indexed with numbers...
Vue.delete(state.updatedFeedback.feedbackLines, <string><any>lineNo)
}
}
function TOGGLE_EDITOR_ON_LINE (state: SubmissionNotesState, { lineNo, comment }: {lineNo: number, comment: FeedbackComment}) {
function TOGGLE_EDITOR_ON_LINE(state: SubmissionNotesState, { lineNo, comment }: { lineNo: number, comment: FeedbackComment }) {
Vue.set(state.ui.selectedCommentOnLine, <string><any>lineNo, comment)
Vue.set(state.ui.showEditorOnLine, <string><any> lineNo, !state.ui.showEditorOnLine[lineNo])
Vue.set(state.ui.showEditorOnLine, <string><any>lineNo, !state.ui.showEditorOnLine[lineNo])
}
function MARK_COMMENT_FOR_DELETION (state: SubmissionNotesState, comment: FeedbackComment) {
function MARK_COMMENT_FOR_DELETION(state: SubmissionNotesState, comment: FeedbackComment) {
Vue.set(state.commentsMarkedForDeletion, comment.pk, comment)
}
function UN_MARK_COMMENT_FOR_DELETION (state: SubmissionNotesState, comment: FeedbackComment) {
function UN_MARK_COMMENT_FOR_DELETION(state: SubmissionNotesState, comment: FeedbackComment) {
Vue.delete(state.commentsMarkedForDeletion, comment.pk)
}
function RESET_UPDATED_FEEDBACK (state: SubmissionNotesState) {
function RESET_MARKED_COMMENTS_FOR_DELETE(state: SubmissionNotesState) {
state.commentsMarkedForDeletion = {}
}
function RESET_UPDATED_FEEDBACK(state: SubmissionNotesState) {
state.updatedFeedback = initialState().updatedFeedback
}
function RESET_STATE (state: SubmissionNotesState) {
function RESET_STATE(state: SubmissionNotesState) {
Object.assign(state, initialState())
}
async function deleteComments ({ state }: BareActionContext<SubmissionNotesState, RootState>) {
async function deleteComments({ state }: BareActionContext<SubmissionNotesState, RootState>) {
return Promise.all(
Object.values(state.commentsMarkedForDeletion).map(comment => {
return api.deleteComment(comment)
})
)
}
async function submitFeedback ({ state }: BareActionContext<SubmissionNotesState, RootState>, { isFinal = false }) {
async function submitFeedback(
{ state }: BareActionContext<SubmissionNotesState, RootState>,
{ isFinal = false }):
Promise<AxiosResponse<void>[]> {
let feedback: Partial<Feedback> = {
isFinal: isFinal,
ofSubmission: state.submission.pk
......@@ -153,23 +161,23 @@ async function submitFeedback ({ state }: BareActionContext<SubmissionNotesState
} else if (feedback.score! < SubmissionNotes.submissionType.fullScore! && !state.hasOrigFeedback) {
throw new Error('You need to add or change a comment when setting a non full score.')
}
// delete those comments that have been marked for deletion
await SubmissionNotes.deleteComments()
if (!state.hasOrigFeedback) {
return api.submitFeedbackForAssignment({ feedback })
await api.submitFeedbackForAssignment({ feedback })
} else {
feedback.pk = state.origFeedback.pk
return api.submitUpdatedFeedback(<{feedback: Feedback}> { feedback })
await api.submitUpdatedFeedback(<{ feedback: Feedback }>{ feedback })
}
// delete those comments that have been marked for deletion
return SubmissionNotes.deleteComments()
}
export const SubmissionNotes = {
get state () { return stateGetter() },
get submissionType () { return submissionTypeGetter() },
get submission () { return submissionGetter() },
get score () { return scoreGetter() },
get workInProgress () { return workInProgressGetter() },
get isFeedbackCreation () { return isFeedbackCreationGetter() },
get state() { return stateGetter() },
get submissionType() { return submissionTypeGetter() },
get submission() { return submissionGetter() },
get score() { return scoreGetter() },
get workInProgress() { return workInProgressGetter() },
get isFeedbackCreation() { return isFeedbackCreationGetter() },
SET_SUBMISSION: mb.commit(SET_SUBMISSION),
SET_ORIG_FEEDBACK: mb.commit(SET_ORIG_FEEDBACK),
......@@ -180,6 +188,7 @@ export const SubmissionNotes = {
TOGGLE_EDITOR_ON_LINE: mb.commit(TOGGLE_EDITOR_ON_LINE),
MARK_COMMENT_FOR_DELETION: mb.commit(MARK_COMMENT_FOR_DELETION),
UN_MARK_COMMENT_FOR_DELETION: mb.commit(UN_MARK_COMMENT_FOR_DELETION),
RESET_MARKED_COMMENTS_FOR_DELETE: mb.commit(RESET_MARKED_COMMENTS_FOR_DELETE),
RESET_UPDATED_FEEDBACK: mb.commit(RESET_UPDATED_FEEDBACK),
RESET_STATE: mb.commit(RESET_STATE),
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment