Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • j.michal/grady
1 result
Show changes
Commits on Source (2)
Showing
with 127 additions and 73 deletions
...@@ -36,6 +36,8 @@ build_test_env: ...@@ -36,6 +36,8 @@ build_test_env:
build_frontend: build_frontend:
image: node:carbon image: node:carbon
stage: build stage: build
variables:
VUE_APP_CI: 'true'
script: script:
- cd frontend - cd frontend
- yarn - yarn
......
...@@ -191,7 +191,7 @@ class VisibleCommentFeedbackSerializer(FeedbackSerializer): ...@@ -191,7 +191,7 @@ class VisibleCommentFeedbackSerializer(FeedbackSerializer):
serializer = FeedbackCommentSerializer( serializer = FeedbackCommentSerializer(
comments, comments,
many=True, many=True,
fields=('pk', 'text', 'created', 'modified', 'of_line',) fields=('pk', 'text', 'created', 'modified', 'of_line', 'labels')
) )
# this is a weird hack because, for some reason, serializer.data # this is a weird hack because, for some reason, serializer.data
# just won't contain the correct data. Instead .data returns a list # just won't contain the correct data. Instead .data returns a list
...@@ -203,4 +203,4 @@ class VisibleCommentFeedbackSerializer(FeedbackSerializer): ...@@ -203,4 +203,4 @@ class VisibleCommentFeedbackSerializer(FeedbackSerializer):
class Meta: class Meta:
model = Feedback model = Feedback
fields = ('pk', 'of_submission', 'is_final', 'score', 'feedback_lines', fields = ('pk', 'of_submission', 'is_final', 'score', 'feedback_lines',
'created', 'of_submission_type') 'created', 'of_submission_type', 'labels')
<!DOCTYPE html> {% load static %}
<html> <!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><title>Grady</title><link href={% static 'css/app.bb1d5faa.css' %} rel=preload as=style><link href={% static 'css/chunk-vendors.23c885e7.css' %} rel=preload as=style><link href={% static 'js/app.b896b8ee.js' %} rel=preload as=script><link href={% static 'js/chunk-vendors.5a1b8f1d.js' %} rel=preload as=script><link href={% static 'css/chunk-vendors.23c885e7.css' %} rel=stylesheet><link href={% static 'css/app.bb1d5faa.css' %} rel=stylesheet></head><body><noscript><strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src={% static 'js/chunk-vendors.5a1b8f1d.js' %}></script><script src={% static 'js/app.b896b8ee.js' %}></script></body></html>
<head> \ No newline at end of file
<title>Grady Frontend placeholder</title>
</head>
<body>
This will be replaced in production.
</body>
</html>
...@@ -23,10 +23,10 @@ class LabelsTestCases(APITestCase): ...@@ -23,10 +23,10 @@ class LabelsTestCases(APITestCase):
} }
cls.label_url = '/api/label/' cls.label_url = '/api/label/'
def test_student_can_not_read_labels(self): def test_student_can_read_labels(self):
self.client.force_authenticate(user=self.student) self.client.force_authenticate(user=self.student)
response = self.client.get(self.label_url) response = self.client.get(self.label_url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(FeedbackLabel.objects.count(), 0) self.assertEqual(FeedbackLabel.objects.count(), 0)
def test_student_can_not_write_labels(self): def test_student_can_not_write_labels(self):
......
...@@ -3,6 +3,7 @@ import logging ...@@ -3,6 +3,7 @@ import logging
from django.db.models import Case, When, IntegerField, Sum, Q from django.db.models import Case, When, IntegerField, Sum, Q
from rest_framework import mixins, viewsets from rest_framework import mixins, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from core import models, permissions, serializers from core import models, permissions, serializers
...@@ -19,6 +20,12 @@ class LabelApiViewSet(viewsets.GenericViewSet, ...@@ -19,6 +20,12 @@ class LabelApiViewSet(viewsets.GenericViewSet,
queryset = models.FeedbackLabel.objects.all() queryset = models.FeedbackLabel.objects.all()
serializer_class = serializers.LabelSerializer serializer_class = serializers.LabelSerializer
def get_permissions(self):
if self.action == 'list':
return [IsAuthenticated(), ]
else:
return super().get_permissions()
class LabelStatistics(viewsets.ViewSet): class LabelStatistics(viewsets.ViewSet):
......
...@@ -17,7 +17,7 @@ import { ...@@ -17,7 +17,7 @@ import {
} from '@/models' } from '@/models'
function getInstanceBaseUrl (): string { function getInstanceBaseUrl (): string {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`.replace(/\/+$/, '') return `${window.location.protocol}//${window.location.host}${window.location.pathname}`.replace(/\/+$/, '')
} else { } else {
return 'http://localhost:8000/' return 'http://localhost:8000/'
...@@ -230,8 +230,8 @@ export async function changeActiveForUser (userPk: string, active: boolean): Pro ...@@ -230,8 +230,8 @@ export async function changeActiveForUser (userPk: string, active: boolean): Pro
return (await ax.patch(`/api/user/${userPk}/change_active/`, { 'is_active': active })).data return (await ax.patch(`/api/user/${userPk}/change_active/`, { 'is_active': active })).data
} }
export async function getLabels () { export async function getLabels (): Promise<FeedbackLabel[]> {
return (await ax.get('/api/label')).data return (await ax.get('/api/label/')).data
} }
export async function createLabel (payload: Partial<FeedbackLabel>) { export async function createLabel (payload: Partial<FeedbackLabel>) {
......
...@@ -11,20 +11,18 @@ ...@@ -11,20 +11,18 @@
<v-card-title v-else> <v-card-title v-else>
No Tests available No Tests available
</v-card-title> </v-card-title>
<v-expansion-panel v-if="expanded"> <v-card-text v-if="expanded">
<v-expansion-panel-content v-for="item in tests" :key="item.pk" > <v-flex sm12 v-for="item in tests" :key="item.pk">
<div slot="header" name="test-name-label"> <div name="test-name-label">
<v-layout row class="pr-4" > <v-layout row class="pr-4">
{{item.name}} <h3>{{item.name}}</h3>
<v-spacer/> <v-spacer/>
{{item.label}} <h3>{{item.label}}</h3>
</v-layout> </v-layout>
</div> </div>
<v-card name="test-output"> <span class="test-output">{{item.annotation}}</span>
<v-card-text class="test-output">{{item.annotation}}</v-card-text> </v-flex>
</v-card> </v-card-text>
</v-expansion-panel-content>
</v-expansion-panel>
</v-card> </v-card>
</template> </template>
...@@ -43,7 +41,8 @@ export default { ...@@ -43,7 +41,8 @@ export default {
}, },
data () { data () {
return { return {
expanded: this.expand expanded: this.expand,
panels: this.tests.map(_ => true)
} }
} }
} }
......
...@@ -51,15 +51,15 @@ export default class FeedbackLabelsList extends Vue { ...@@ -51,15 +51,15 @@ export default class FeedbackLabelsList extends Vue {
return !this.sidebar || (this.sidebar && !UI.state.sideBarCollapsed) return !this.sidebar || (this.sidebar && !UI.state.sideBarCollapsed)
} }
async created() { created() {
await this.refreshLabels() this.refreshLabels()
} }
async refreshLabels() { refreshLabels() {
this.updating = true this.updating = true
const labels = await getLabels() FeedbackLabels.getLabels().finally(() => {
FeedbackLabels.SET_LABELS(labels) this.updating = false
this.updating = false })
} }
} }
</script> </script>
......
...@@ -17,12 +17,12 @@ export default class commentLabelSelector extends Vue { ...@@ -17,12 +17,12 @@ export default class commentLabelSelector extends Vue {
/** /**
* Returns array of label pk's where feedbackType is * Returns array of label pk's where feedbackType is
* either "origFeedback" or "updatedFeedback" * either "origFeedback" or "updatedFeedback"
* *
* Will return null when labels property does not exist on the requested state's comment * Will return null when labels property does not exist on the requested state's comment
* This is the case when the labels have not been updated, as we don't want to have * This is the case when the labels have not been updated, as we don't want to have
* the labels field in the object if the labels have not changed. * the labels field in the object if the labels have not changed.
*/ */
copyStateLabels(feedbackType: FeedbackType): number[] | null { copyStateLabels(feedbackType: FeedbackType): number[] | null {
const currentLine = this.getFeedbackLine(feedbackType) const currentLine = this.getFeedbackLine(feedbackType)
if (currentLine && currentLine.labels) { if (currentLine && currentLine.labels) {
return currentLine.labels return currentLine.labels
...@@ -62,7 +62,6 @@ export default class commentLabelSelector extends Vue { ...@@ -62,7 +62,6 @@ export default class commentLabelSelector extends Vue {
if (labelsOrig === null || labelsOrig.length === 0) { if (labelsOrig === null || labelsOrig.length === 0) {
return new Array () return new Array ()
} }
const removedLabels = this.getRemovedLabels() const removedLabels = this.getRemovedLabels()
const addedLabels = this.getAddedLabels() const addedLabels = this.getAddedLabels()
...@@ -114,7 +113,7 @@ export default class commentLabelSelector extends Vue { ...@@ -114,7 +113,7 @@ export default class commentLabelSelector extends Vue {
}) })
if (!label) return undefined if (!label) return undefined
return { return {
pk: val, pk: val,
name: label.name, name: label.name,
description: label.description, description: label.description,
...@@ -131,4 +130,4 @@ export default class commentLabelSelector extends Vue { ...@@ -131,4 +130,4 @@ export default class commentLabelSelector extends Vue {
return mappedLabels; return mappedLabels;
} }
} }
\ No newline at end of file
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
<v-icon v-else size="20px">restore</v-icon> <v-icon v-else size="20px">restore</v-icon>
</v-btn> </v-btn>
</div> </div>
<v-layout v-if="showLabels" ml-2> <v-layout v-if="showLabels && correctorView" ml-2>
<v-flex sm4> <v-flex sm4>
<v-flex sm12> <v-flex sm12>
UNCHANGED UNCHANGED
...@@ -70,6 +70,18 @@ ...@@ -70,6 +70,18 @@
/> />
</v-flex> </v-flex>
</v-layout> </v-layout>
<template row wrap v-if="!correctorView" align-center>
<v-layout row wrap align-center v-for="label in unchangedLabels" :key="label.pk">
<v-flex sm6>
<feedback-label
v-bind="label"
/>
</v-flex>
<v-flex sm6>
<span><b>Description: </b>{{label.description}}</span>
</v-flex>
</v-layout>
</template>
</div> </div>
</template> </template>
...@@ -121,12 +133,16 @@ export default { ...@@ -121,12 +133,16 @@ export default {
showVisibilityIcon: { showVisibilityIcon: {
type: Boolean, type: Boolean,
default: true default: true
},
correctorView: {
type: Boolean,
default: true
} }
}, },
computed: { computed: {
commentDisplayable () { return this.text !== "" }, commentDisplayable () { return this.text !== "" },
showLabels () { showLabels () {
return this.visibleToStudent && return this.visibleToStudent &&
(this.getUnchangedLabels().length > 0 || (this.getUnchangedLabels().length > 0 ||
this.getAddedLabels().length > 0 || this.getAddedLabels().length > 0 ||
this.getRemovedLabels().length > 0) this.getRemovedLabels().length > 0)
......
...@@ -87,7 +87,7 @@ export default class SubmissionType extends Vue { ...@@ -87,7 +87,7 @@ export default class SubmissionType extends Vue {
}) reverse!: boolean }) reverse!: boolean
@Prop({ @Prop({
type: Object, type: Object,
default: {}, default: () => {return {}},
}) solutionComments!: {[ofLine: number]: SolutionComment[]} }) solutionComments!: {[ofLine: number]: SolutionComment[]}
@Prop({ @Prop({
type: Object, type: Object,
......
...@@ -118,7 +118,6 @@ export default class SolutionComment extends Vue { ...@@ -118,7 +118,6 @@ export default class SolutionComment extends Vue {
} }
toggleEditing() { toggleEditing() {
console.log('adasd')
this.editing = !this.editing this.editing = !this.editing
this.editedText = this.text this.editedText = this.text
} }
......
...@@ -17,19 +17,26 @@ import 'highlight.js/styles/atom-one-light.css' ...@@ -17,19 +17,26 @@ import 'highlight.js/styles/atom-one-light.css'
import "@/util/shortkeys" import "@/util/shortkeys"
Vue.use(Vuetify) Vue.use(Vuetify)
Vue.use(Clipboard) Vue.use(Clipboard)
Vue.use(Notifications) Vue.use(Notifications)
const integrations = [
new Integrations.Vue({Vue, attachProps: true, logErrors: true}),
new Integrations.ExtraErrorData({depth: 10}),
new Integrations.CaptureConsole({levels: ['warn', 'error']}),
new Integrations.Debug()
]
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test' || process.env.VUE_APP_CI === 'true') {
Sentry.init({ Sentry.init({
dsn: 'https://c86a983153da412b8aec8b8df9ce51ba@robin-in.space/2', dsn: 'https://c86a983153da412b8aec8b8df9ce51ba@robin-in.space/2',
integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})], integrations
}) })
} else if (process.env.NODE_ENV == 'production') { } else if (process.env.NODE_ENV === 'production') {
Sentry.init({ Sentry.init({
dsn: 'https://874b896335564d8c9c49137391f8e3f1@robin-in.space/3', dsn: 'https://874b896335564d8c9c49137391f8e3f1@robin-in.space/3',
integrations: [new Integrations.Vue({Vue, attachProps: true, logErrors: true})], integrations
}) })
} }
......
...@@ -925,4 +925,6 @@ export interface VisibleCommentFeedback { ...@@ -925,4 +925,6 @@ export interface VisibleCommentFeedback {
* @memberof VisibleCommentFeedback * @memberof VisibleCommentFeedback
*/ */
ofSubmissionType?: string ofSubmissionType?: string
labels: number[]
} }
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<script> <script>
import SubmissionList from '@/components/student/SubmissionList.vue' import SubmissionList from '@/components/student/SubmissionList.vue'
import { StudentPage } from '@/store/modules/student-page' import { StudentPage } from '@/store/modules/student-page'
import { FeedbackLabels } from '@/store/modules/feedback-labels'
export default { export default {
components: { components: {
...@@ -23,6 +24,7 @@ export default { ...@@ -23,6 +24,7 @@ export default {
created: function () { created: function () {
if (!this.loaded) { if (!this.loaded) {
StudentPage.getStudentData().then(() => { StudentPage.getStudentData().then(() => {
FeedbackLabels.getLabels()
StudentPage.getStudentSubmissions() StudentPage.getStudentSubmissions()
}) })
} }
......
...@@ -25,11 +25,12 @@ ...@@ -25,11 +25,12 @@
<template v-if="feedback"> <template v-if="feedback">
<template v-for="(comment, index) in feedback.feedbackLines[lineNo]"> <template v-for="(comment, index) in feedback.feedbackLines[lineNo]">
<feedback-comment <feedback-comment
v-if="feedback.feedbackLines[lineNo] && showFeedback" v-if="showFeedback"
v-bind="comment" v-bind="comment"
:line-no="lineNo" :line-no="lineNo"
:key="index" :key="comment.pk + index"
:show_visibility_icon="false" :showVisibilityIcon="false"
:correctorView="false"
/> />
</template> </template>
</template> </template>
...@@ -37,6 +38,21 @@ ...@@ -37,6 +38,21 @@
</tr> </tr>
</template> </template>
</base-annotated-submission> </base-annotated-submission>
<v-card>
<v-card-title>Labels:</v-card-title>
<v-card-text>
<v-layout row wrap align-center v-for="label in mappedLabels" :key="'global' + label.pk">
<v-flex sm6>
<feedback-label
v-bind="label"
/>
</v-flex>
<v-flex sm6>
<span><b>Description: </b>{{label.description}}</span>
</v-flex>
</v-layout>
</v-card-text>
</v-card>
<submission-tests <submission-tests
:tests="tests" :tests="tests"
:expand="true" :expand="true"
...@@ -64,6 +80,9 @@ import { SubmissionNotes } from '@/store/modules/submission-notes' ...@@ -64,6 +80,9 @@ import { SubmissionNotes } from '@/store/modules/submission-notes'
import ToggleFeedbackVisibilityButton from '@/components/submission_notes/toolbars/ToggleFeedbackVisibilityButton' import ToggleFeedbackVisibilityButton from '@/components/submission_notes/toolbars/ToggleFeedbackVisibilityButton'
import SubmissionTests from '@/components/SubmissionTests' import SubmissionTests from '@/components/SubmissionTests'
import NonFinalFeedbackAlert from '@/components/student/NonFinalFeedbackAlert' import NonFinalFeedbackAlert from '@/components/student/NonFinalFeedbackAlert'
import { FeedbackLabels } from '../../store/modules/feedback-labels'
import FeedbackLabel from "@/components/feedback_labels/FeedbackLabel.vue"
export default { export default {
name: 'student-submission-page', name: 'student-submission-page',
...@@ -75,6 +94,7 @@ export default { ...@@ -75,6 +94,7 @@ export default {
SubmissionLine, SubmissionLine,
BaseAnnotatedSubmission, BaseAnnotatedSubmission,
AnnotatedSubmission, AnnotatedSubmission,
FeedbackLabel,
SubmissionType }, SubmissionType },
computed: { computed: {
id: function () { id: function () {
...@@ -84,7 +104,14 @@ export default { ...@@ -84,7 +104,14 @@ export default {
tests () { return SubmissionNotes.state.submission.tests }, tests () { return SubmissionNotes.state.submission.tests },
showFeedback: function (state) { return SubmissionNotes.state.ui.showFeedback }, showFeedback: function (state) { return SubmissionNotes.state.ui.showFeedback },
submissionType () { return StudentPage.state.submissionData[this.id].type }, submissionType () { return StudentPage.state.submissionData[this.id].type },
feedback () { return StudentPage.state.submissionData[this.$route.params.id].feedback } feedback () { return StudentPage.state.submissionData[this.$route.params.id].feedback },
mappedLabels () {
return this.feedback.labels.map(entry => {
return FeedbackLabels.state.labels.find(label => {
return label.pk === entry
})
})
}
}, },
methods: { methods: {
onRouteMountOrUpdate (routeId) { onRouteMountOrUpdate (routeId) {
......
import { FeedbackLabel } from '@/models'; import { FeedbackLabel } from '@/models';
import { getStoreBuilder } from 'vuex-typex'; import { getStoreBuilder } from 'vuex-typex';
import { RootState } from '../store'; import { RootState } from '../store';
import * as api from '@/api'
import Vue from 'vue'; import Vue from 'vue';
export interface FeedbackLabelsState { export interface FeedbackLabelsState {
...@@ -40,6 +41,11 @@ function UPDATE_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) { ...@@ -40,6 +41,11 @@ function UPDATE_LABEL(state: FeedbackLabelsState, label: FeedbackLabel) {
ADD_LABEL(state, label) ADD_LABEL(state, label)
} }
async function getLabels() {
const labels = await api.getLabels()
FeedbackLabels.SET_LABELS(labels)
}
export const FeedbackLabels = { export const FeedbackLabels = {
get state() { return stateGetter() }, get state() { return stateGetter() },
get availableLabels() { return availableLabelsGetter() }, get availableLabels() { return availableLabelsGetter() },
...@@ -48,5 +54,7 @@ export const FeedbackLabels = { ...@@ -48,5 +54,7 @@ export const FeedbackLabels = {
ADD_LABEL: mb.commit(ADD_LABEL), ADD_LABEL: mb.commit(ADD_LABEL),
REMOVE_LABEL: mb.commit(REMOVE_LABEL), REMOVE_LABEL: mb.commit(REMOVE_LABEL),
UPDATE_LABEL: mb.commit(UPDATE_LABEL), UPDATE_LABEL: mb.commit(UPDATE_LABEL),
getLabels: mb.dispatch(getLabels)
} }
...@@ -60,24 +60,16 @@ function RESET_STATE (state: StudentPageState) { ...@@ -60,24 +60,16 @@ function RESET_STATE (state: StudentPageState) {
} }
async function getStudentData () { async function getStudentData () {
try {
const studentData = await fetchStudentSelfData() const studentData = await fetchStudentSelfData()
StudentPage.SET_STUDENT_NAME(studentData.name || '') StudentPage.SET_STUDENT_NAME(studentData.name || '')
StudentPage.SET_EXAM(studentData.exam) StudentPage.SET_EXAM(studentData.exam)
StudentPage.SET_SUBMISSIONS_FOR_LIST(studentData.submissions) StudentPage.SET_SUBMISSIONS_FOR_LIST(studentData.submissions)
StudentPage.SET_LOADED(true) StudentPage.SET_LOADED(true)
} catch (e) {
console.log(e)
}
} }
async function getStudentSubmissions () { async function getStudentSubmissions () {
try {
const submissions = await fetchStudentSubmissions() const submissions = await fetchStudentSubmissions()
StudentPage.SET_FULL_SUBMISSION_DATA(submissions) StudentPage.SET_FULL_SUBMISSION_DATA(submissions)
} catch (e) {
console.log(e)
}
} }
export const StudentPage = { export const StudentPage = {
......
...@@ -92,6 +92,9 @@ const isFeedbackCreationGetter = mb.read(function isFeedbackCreation(state) { ...@@ -92,6 +92,9 @@ const isFeedbackCreationGetter = mb.read(function isFeedbackCreation(state) {
function SET_SUBMISSION(state: SubmissionNotesState, submission: SubmissionNoType) { function SET_SUBMISSION(state: SubmissionNotesState, submission: SubmissionNoType) {
state.submission = submission state.submission = submission
if (submission.feedback !== undefined) {
SET_ORIG_FEEDBACK(state, submission.feedback)
}
} }
function SET_ORIG_FEEDBACK(state: SubmissionNotesState, feedback: Feedback) { function SET_ORIG_FEEDBACK(state: SubmissionNotesState, feedback: Feedback) {
if (feedback) { if (feedback) {
......
import instance from '@/main' import instance from '@/main'
import { parseErrorNotification, parseBlacklist } from '@/util/helpers' import { parseErrorNotification, parseBlacklist } from '@/util/helpers'
import * as Sentry from '@sentry/browser'
const errorUrlBlacklist = [ const errorUrlBlacklist = [
"/api/get-token/", "/api/get-token/",
...@@ -7,18 +9,14 @@ const errorUrlBlacklist = [ ...@@ -7,18 +9,14 @@ const errorUrlBlacklist = [
const blackListRegExp = new RegExp(parseBlacklist(errorUrlBlacklist), "g") const blackListRegExp = new RegExp(parseBlacklist(errorUrlBlacklist), "g")
export function errorInterceptor (error: any): any { export function errorInterceptor (error: any): any {
// TODO: log errors and store them somewhere if (!error.response.request.responseURL.match(blackListRegExp)) {
instance.$notify({
if (error.response.request.responseURL.match(blackListRegExp)) { title: 'Request failed.',
return text: parseErrorNotification(error.response),
type: 'error',
duration: -1
})
} }
Sentry.captureException(error)
instance.$notify({
title: 'Request failed.',
text: parseErrorNotification(error.response),
type: 'error',
duration: -1
})
return Promise.reject(error) return Promise.reject(error)
} }
\ No newline at end of file