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
Select Git revision

Target

Select target project
  • j.michal/grady
1 result
Select Git revision
  • 169-add-date-to-examtype
  • 233-make-exam-a-many-to-many-field-on-studentinfo-model
  • 236-improve-importer-experience
  • 243-replace-toggle-buttons-with-switches
  • 250-update-vuetify
  • 258-add-markdown-viewer
  • 265-fix-selection-changing-on-window-switching
  • 272-reviewers-should-be-able-to-assign-exercise-groups-to-tutors
  • 276-create-new-yarn-lockfile
  • 279-tutor-overview-no-scrolling
  • 282-copy-button-does-not-work-when-reviewing-corrections
  • 286-fix-misalignment-of-hide-show-sidebar-buttons
  • 287-build-test-image-constantly-failing
  • 288-add-dropdown-to-participantspage-to-set-students-groups
  • 289-fix-change-log-card
  • 291-revise-to-old-export-scheme
  • 292-update-gitlab-ci-config-for-new-runner
  • 292-update-gitlab-ci-config-for-new-runner-2
  • add-exercise-util-script
  • document-frontend-components
  • grady-exam
  • jakob.dieterle-master-patch-13835
  • master
  • parallel-test
  • test-233-branch-remove-examtype-foreign-key-on-group
  • update-export-dialogs
  • 0.0.1
  • 0.1
  • 0.2
  • 0.3
  • 0.4
  • 0.4.1
  • 0.4.2
  • 0.5.0
  • 0.5.1
  • 1.0.0
  • 1.1.0
  • 2.0.0
  • 2.0.1
  • 2.1.0
  • 2.1.1
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 4.0.0
  • 4.1.0
  • 4.2.0
  • 4.3.0
  • 4.4.0
  • 4.4.1
  • 5.0.0
  • 5.0.1
  • 5.1.0
  • 5.1.1
  • 5.1.2
  • 5.1.3
  • 5.1.4
  • 5.1.5
  • 5.1.6
  • 5.1.7
  • 5.2.0
  • 5.3.0
  • 5.3.1
  • 5.3.2
  • 5.4.0
  • 5.4.1
  • 5.4.2
  • 6.0.0
  • 6.1.0
  • legacy
70 results
Show changes
Commits on Source (2)
Showing
with 674 additions and 111 deletions
...@@ -34,7 +34,7 @@ teste2e: ...@@ -34,7 +34,7 @@ teste2e:
cd frontend && yarn build && cp dist/index.html ../core/templates && cd .. && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest --ds=grady.settings $(path); git checkout core/templates/index.html cd frontend && yarn build && cp dist/index.html ../core/templates && cd .. && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest --ds=grady.settings $(path); git checkout core/templates/index.html
teste2e-nc: teste2e-nc:
cp frontend/dist/index.html ./core/templates && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest -n 4 --ds=grady.settings $(path); git checkout core/templates/index.html cp frontend/dist/index.html ./core/templates && python util/format_index.py && python manage.py collectstatic --no-input && HEADLESS_TESTS=$(headless) pytest --ds=grady.settings $(path); git checkout core/templates/index.html
coverage: coverage:
......
# Generated by Django 2.1.4 on 2019-05-14 15:00
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0015_feedbacklabel_colour'),
]
operations = [
migrations.CreateModel(
name='SolutionComment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('of_line', models.PositiveIntegerField()),
('of_submission_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to='core.SubmissionType')),
('of_user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='solution_comments', to=settings.AUTH_USER_MODEL)),
],
),
]
# Generated by Django 2.1.11 on 2019-09-02 12:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0016_solutioncomment'),
('core', '0020_auto_20190831_1417'),
]
operations = [
]
from .exam_type import ExamType # noqa from .exam_type import ExamType # noqa
from .submission_type import SubmissionType # noqa from .submission_type import SubmissionType, SolutionComment # noqa
from .user_account import UserAccount, TutorReviewerManager # noqa
from .user_account import UserAccount, TutorReviewerManager # noqa from .user_account import UserAccount, TutorReviewerManager # noqa
from .student_info import StudentInfo, random_matrikel_no # noqa from .student_info import StudentInfo, random_matrikel_no # noqa
from .test import Test # noqa from .test import Test # noqa
......
...@@ -98,3 +98,21 @@ class SubmissionType(models.Model): ...@@ -98,3 +98,21 @@ class SubmissionType(models.Model):
), ),
submission_count=Count('submissions'), submission_count=Count('submissions'),
).order_by('name') ).order_by('name')
class SolutionComment(models.Model):
text = models.TextField()
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
of_line = models.PositiveIntegerField()
of_user = models.ForeignKey(
'UserAccount',
related_name="solution_comments",
on_delete=models.PROTECT
)
of_submission_type = models.ForeignKey(
SubmissionType,
related_name="solution_comments",
on_delete=models.PROTECT,
)
from .common_serializers import * # noqa from .common_serializers import * # noqa
from .submission_type import (SubmissionTypeListSerializer, SubmissionTypeSerializer, # noqa
SolutionCommentSerializer) # noqa
from .feedback import (FeedbackSerializer, FeedbackCommentSerializer, # noqa from .feedback import (FeedbackSerializer, FeedbackCommentSerializer, # noqa
VisibleCommentFeedbackSerializer) # noqa VisibleCommentFeedbackSerializer) # noqa
from .subscription import * # noqa from .subscription import * # noqa
......
import logging import logging
from collections import defaultdict
import django.contrib.auth.password_validation as validators import django.contrib.auth.password_validation as validators
from django.core import exceptions from django.core import exceptions
from django.db.models.manager import Manager
from rest_framework import serializers from rest_framework import serializers
from rest_framework.utils import html
from core import models from core import models
...@@ -26,25 +29,6 @@ class TestSerializer(DynamicFieldsModelSerializer): ...@@ -26,25 +29,6 @@ class TestSerializer(DynamicFieldsModelSerializer):
fields = ('pk', 'name', 'label', 'annotation') fields = ('pk', 'name', 'label', 'annotation')
class SubmissionTypeListSerializer(DynamicFieldsModelSerializer):
class Meta:
model = models.SubmissionType
fields = ('pk', 'name', 'full_score')
class SubmissionTypeSerializer(SubmissionTypeListSerializer):
class Meta:
model = models.SubmissionType
fields = ('pk',
'name',
'full_score',
'description',
'solution',
'programming_language')
class UserAccountSerializer(DynamicFieldsModelSerializer): class UserAccountSerializer(DynamicFieldsModelSerializer):
def validate(self, data): def validate(self, data):
...@@ -63,3 +47,49 @@ class UserAccountSerializer(DynamicFieldsModelSerializer): ...@@ -63,3 +47,49 @@ class UserAccountSerializer(DynamicFieldsModelSerializer):
fields = ('pk', 'username', 'role', 'is_admin', 'password') fields = ('pk', 'username', 'role', 'is_admin', 'password')
read_only_fields = ('pk', 'username', 'role', 'is_admin') read_only_fields = ('pk', 'username', 'role', 'is_admin')
extra_kwargs = {'password': {'write_only': True}} extra_kwargs = {'password': {'write_only': True}}
class CommentDictionarySerializer(serializers.ListSerializer):
def to_internal_value(self, comment_dict):
""" Converts a line_no -> comment list dictionary back to a list
of comments. Currently we do not have any information about the
feedback since it is not available in this scope. Feedback is
responsible to add it later on update/creation """
if html.is_html_input(comment_dict):
comment_dict = html.parse_html_list(comment_dict)
if not isinstance(comment_dict, dict):
raise serializers.ValidationError(
'Comments have to be provided as a dict'
'with: line -> list of comments'
)
ret = []
errors = []
for line, comment in comment_dict.items():
try:
comment['of_line'] = line
validated = self.child.run_validation(comment)
except serializers.ValidationError as err:
errors.append(err.detail)
else:
ret.append(validated)
errors.append({})
if any(errors):
raise serializers.ValidationError(errors)
return ret
def to_representation(self, comments):
""" Provides a dict where all the keys correspond to lines and contain
a list of comments on that line. """
if isinstance(comments, Manager):
comments = comments.all()
ret = defaultdict(list)
for comment in comments:
ret[comment.of_line].append(self.child.to_representation(comment))
return ret
import logging import logging
from collections import defaultdict
from django.db import transaction from django.db import transaction
from django.db.models.manager import Manager
from rest_framework import serializers from rest_framework import serializers
from rest_framework.utils import html
from core import models from core import models
from core.models import Feedback, UserAccount from core.models import Feedback, UserAccount
from core.serializers import CommentDictionarySerializer
from util.factories import GradyUserFactory from util.factories import GradyUserFactory
from .generic import DynamicFieldsModelSerializer from .generic import DynamicFieldsModelSerializer
...@@ -16,52 +14,6 @@ log = logging.getLogger(__name__) ...@@ -16,52 +14,6 @@ log = logging.getLogger(__name__)
user_factory = GradyUserFactory() user_factory = GradyUserFactory()
class FeedbackCommentDictionarySerializer(serializers.ListSerializer):
def to_internal_value(self, comment_dict):
""" Converts a line_no -> comment list dictionary back to a list
of comments. Currently we do not have any information about the
feedback since it is not availiable in this scope. Feedback is
responsible to add it later on update/creation """
if html.is_html_input(comment_dict):
comment_dict = html.parse_html_list(comment_dict)
if not isinstance(comment_dict, dict):
raise serializers.ValidationError(
'Comments have to be provided as a dict'
'with: line -> list of comments'
)
ret = []
errors = []
for line, comment in comment_dict.items():
try:
comment['of_line'] = line
validated = self.child.run_validation(comment)
except serializers.ValidationError as err:
errors.append(err.detail)
else:
ret.append(validated)
errors.append({})
if any(errors):
raise serializers.ValidationError(errors)
return ret
def to_representation(self, comments):
""" Provides a dict where all the keys correspond to lines and contain
a list of comments on that line. """
if isinstance(comments, Manager):
comments = comments.all()
ret = defaultdict(list)
for comment in comments:
ret[comment.of_line].append(self.child.to_representation(comment))
return ret
class FeedbackCommentSerializer(DynamicFieldsModelSerializer): class FeedbackCommentSerializer(DynamicFieldsModelSerializer):
of_tutor = serializers.StringRelatedField(source='of_tutor.username') of_tutor = serializers.StringRelatedField(source='of_tutor.username')
labels = serializers.PrimaryKeyRelatedField(many=True, required=False, labels = serializers.PrimaryKeyRelatedField(many=True, required=False,
...@@ -84,7 +36,7 @@ class FeedbackCommentSerializer(DynamicFieldsModelSerializer): ...@@ -84,7 +36,7 @@ class FeedbackCommentSerializer(DynamicFieldsModelSerializer):
'of_feedback': {'write_only': True}, 'of_feedback': {'write_only': True},
'of_line': {'write_only': True}, 'of_line': {'write_only': True},
} }
list_serializer_class = FeedbackCommentDictionarySerializer list_serializer_class = CommentDictionarySerializer
class FeedbackSerializer(DynamicFieldsModelSerializer): class FeedbackSerializer(DynamicFieldsModelSerializer):
...@@ -99,7 +51,7 @@ class FeedbackSerializer(DynamicFieldsModelSerializer): ...@@ -99,7 +51,7 @@ class FeedbackSerializer(DynamicFieldsModelSerializer):
""" Search for the assignment of this feedback and report in which """ Search for the assignment of this feedback and report in which
stage the tutor has worked on it. stage the tutor has worked on it.
Note: This method is unorthodox since it mingles the rather dump TODO Note: This method is unorthodox since it mingles the rather dump
feedback object with assignment logic. The reverse lookups in the feedback object with assignment logic. The reverse lookups in the
method are not pre-fetched. Remove if possible. """ method are not pre-fetched. Remove if possible. """
if 'request' not in self.context: if 'request' not in self.context:
......
import logging
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from core import models
from core.serializers import DynamicFieldsModelSerializer, CommentDictionarySerializer
log = logging.getLogger(__name__)
class SolutionCommentSerializer(DynamicFieldsModelSerializer):
of_user = serializers.StringRelatedField(source='of_user.username')
def validate(self, attrs):
super().validate(attrs)
submission_type = attrs.get('of_submission_type')
of_line = attrs.get('of_line')
if self.instance:
submission_type = self.instance.of_submission_type
of_line = self.instance.of_line
max_line_number = len(submission_type.solution.split('\n'))
if not (0 < of_line <= max_line_number):
raise ValidationError('Invalid line number for comment')
return attrs
def create(self, validated_data):
validated_data['of_user'] = self.context['request'].user
return super().create(validated_data)
class Meta:
model = models.SolutionComment
fields = (
'pk',
'text',
'created',
'of_user',
'of_line',
'of_submission_type'
)
read_only_fields = ('pk', 'created', 'of_user')
list_serializer_class = CommentDictionarySerializer
class SubmissionTypeListSerializer(DynamicFieldsModelSerializer):
class Meta:
model = models.SubmissionType
fields = ('pk', 'name', 'full_score')
class SubmissionTypeSerializer(DynamicFieldsModelSerializer):
solution_comments = SolutionCommentSerializer(many=True, required=False)
class Meta:
model = models.SubmissionType
fields = ('pk',
'name',
'full_score',
'description',
'solution',
'programming_language',
'solution_comments')
...@@ -25,6 +25,7 @@ router.register('statistics', views.StatisticsEndpoint, basename='statistics') ...@@ -25,6 +25,7 @@ router.register('statistics', views.StatisticsEndpoint, basename='statistics')
router.register('user', views.UserAccountViewSet, basename='user') router.register('user', views.UserAccountViewSet, basename='user')
router.register('label', views.LabelApiViewSet, basename='label') router.register('label', views.LabelApiViewSet, basename='label')
router.register('label-statistics', views.LabelStatistics, basename='label-statistics') router.register('label-statistics', views.LabelStatistics, basename='label-statistics')
router.register('solution-comment', views.SolutionCommentApiViewSet, basename='solution-comment')
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
......
...@@ -24,7 +24,7 @@ from core.serializers import (ExamSerializer, StudentInfoSerializer, ...@@ -24,7 +24,7 @@ from core.serializers import (ExamSerializer, StudentInfoSerializer,
StudentInfoForListViewSerializer, StudentInfoForListViewSerializer,
SubmissionNoTypeSerializer, StudentSubmissionSerializer, SubmissionNoTypeSerializer, StudentSubmissionSerializer,
SubmissionTypeSerializer, CorrectorSerializer, SubmissionTypeSerializer, CorrectorSerializer,
UserAccountSerializer) UserAccountSerializer, SolutionCommentSerializer)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -130,6 +130,29 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet): ...@@ -130,6 +130,29 @@ class SubmissionTypeApiView(viewsets.ReadOnlyModelViewSet):
permission_classes = (IsTutorOrReviewer, ) permission_classes = (IsTutorOrReviewer, )
class SolutionCommentApiViewSet(
mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):
permission_classes = (IsTutorOrReviewer, )
queryset = models.SolutionComment.objects.all()
serializer_class = SolutionCommentSerializer
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
if not request.user.is_reviewer() and instance.of_user != request.user:
raise PermissionDenied(detail="You can only delete comments you made")
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
def update(self, request, *args, **kwargs):
instance = self.get_object()
if instance.of_user != request.user:
raise PermissionDenied(detail="You can only update comments you made")
return super().update(request, *args, **kwargs)
class StatisticsEndpoint(viewsets.ViewSet): class StatisticsEndpoint(viewsets.ViewSet):
permission_classes = (IsTutorOrReviewer, ) permission_classes = (IsTutorOrReviewer, )
......
...@@ -7,7 +7,7 @@ import xkcdpass.xkcd_password as xp ...@@ -7,7 +7,7 @@ import xkcdpass.xkcd_password as xp
from core.models import StudentInfo, UserAccount, ExamType, SubmissionType from core.models import StudentInfo, UserAccount, ExamType, SubmissionType
from core.permissions import IsReviewer from core.permissions import IsReviewer
from core.serializers.common_serializers import SubmissionTypeSerializer, \ from core.serializers import SubmissionTypeSerializer, \
ExamSerializer, UserAccountSerializer ExamSerializer, UserAccountSerializer
from core.serializers.student import StudentExportSerializer from core.serializers.student import StudentExportSerializer
from core.serializers.tutor import CorrectorSerializer from core.serializers.tutor import CorrectorSerializer
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vuetify": "^1.1.9", "vuetify": "^1.1.9",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"vuex-persistedstate": "^2.5.4",
"vuex-typex": "https://github.com/robinhundt/vuex-typex.git" "vuex-typex": "https://github.com/robinhundt/vuex-typex.git"
}, },
"devDependencies": { "devDependencies": {
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
SubmissionNoType, SubmissionType, SubmissionNoType, SubmissionType,
Subscription, Subscription,
Tutor, UserAccount, LabelStatisticsForSubType, Tutor, UserAccount, LabelStatisticsForSubType,
FeedbackLabel, FeedbackLabel, SolutionComment,
CreateUpdateFeedback CreateUpdateFeedback
} from '@/models' } from '@/models'
...@@ -27,12 +27,6 @@ function getInstanceBaseUrl (): string { ...@@ -27,12 +27,6 @@ function getInstanceBaseUrl (): string {
let ax: AxiosInstance = axios.create({ let ax: AxiosInstance = axios.create({
baseURL: getInstanceBaseUrl() baseURL: getInstanceBaseUrl()
}) })
{
let token = window.sessionStorage.getItem('token')
if (token) {
ax.defaults.headers['Authorization'] = `JWT ${token}`
}
}
export async function registerTutor (credentials: Credentials): Promise<AxiosResponse<Tutor>> { export async function registerTutor (credentials: Credentials): Promise<AxiosResponse<Tutor>> {
return ax.post<Tutor>('/api/corrector/register/', credentials) return ax.post<Tutor>('/api/corrector/register/', credentials)
...@@ -171,6 +165,26 @@ export async function fetchSubmissionTypes (): Promise<Array<SubmissionType>> { ...@@ -171,6 +165,26 @@ export async function fetchSubmissionTypes (): Promise<Array<SubmissionType>> {
return (await ax.get(url)).data return (await ax.get(url)).data
} }
export async function fetchSubmissionType (pk: string): Promise<SubmissionType> {
const url = `/api/submissiontype/${pk}`
return (await ax.get(url)).data
}
export async function deleteSolutionComment (pk: number): Promise<AxiosResponse<void>> {
const url = `/api/solution-comment/${pk}/`
return ax.delete(url)
}
export async function createSolutionComment(comment: Partial<SolutionComment>): Promise<SolutionComment> {
const url = `/api/solution-comment/`
return (await ax.post(url, comment)).data
}
export async function patchSolutionComment(comment: Partial<SolutionComment>): Promise<SolutionComment> {
const url = `/api/solution-comment/${comment.pk}/`
return (await ax.patch(url, comment)).data
}
export async function fetchAllAssignments (): Promise<Array<Assignment>> { export async function fetchAllAssignments (): Promise<Array<Assignment>> {
const url = '/api/assignment/' const url = '/api/assignment/'
return (await ax.get(url)).data return (await ax.get(url)).data
......
...@@ -186,7 +186,7 @@ export default { ...@@ -186,7 +186,7 @@ export default {
const hasUpdatedComment = this.updatedFeedback && this.updatedFeedback[lineNo] const hasUpdatedComment = this.updatedFeedback && this.updatedFeedback[lineNo]
return !this.showFeedback && (hasOrigComment || hasUpdatedComment) return !this.showFeedback && (hasOrigComment || !!hasUpdatedComment)
}, },
init () { init () {
SubmissionNotes.RESET_STATE() SubmissionNotes.RESET_STATE()
......
<template> <template>
<div> <div>
<td class="line-number-cell"> <td
<v-btn v-if="hint" :style="backgroundColor"
block class="line-number-cell">
depressed
class="line-number-btn"
color="error"
@click="toggleEditor"
>
{{ lineNo }}
</v-btn>
<v-btn <v-btn
v-else
flat flat
block block
depressed depressed
...@@ -20,6 +12,7 @@ ...@@ -20,6 +12,7 @@
> >
{{ lineNo }} {{ lineNo }}
</v-btn> </v-btn>
</v-btn>
</td> </td>
<td class="code-cell-content pl-2"> <td class="code-cell-content pl-2">
<span v-html="code" class="code-line"></span> <span v-html="code" class="code-line"></span>
...@@ -49,6 +42,11 @@ export default { ...@@ -49,6 +42,11 @@ export default {
default: false, default: false,
}, },
}, },
computed: {
backgroundColor() {
return this.hint ? 'background-color: #F44336;' : 'background-color: transparent;'
}
},
methods: { methods: {
toggleEditor () { toggleEditor () {
this.$emit('toggleEditor') this.$emit('toggleEditor')
......
...@@ -7,7 +7,16 @@ ...@@ -7,7 +7,16 @@
v-for="(item, i) in typeItems" v-for="(item, i) in typeItems"
:key="i" :key="i"
> >
<div slot="header"><b>{{ item.title }}</b></div> <div slot="header">
<b>{{ item.title }}</b>
<v-btn
class="ml-5"
color="info"
flat
v-if="item.title == 'Solution'"
@click.stop="showSolutionComments = !showSolutionComments"
>Toggle Comments</v-btn>
</div>
<v-card <v-card
v-if="item.title === 'Description'" v-if="item.title === 'Description'"
class="type-description" class="type-description"
...@@ -19,10 +28,14 @@ ...@@ -19,10 +28,14 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<div v-else-if="item.title === 'Solution'"> <div v-else-if="item.title === 'Solution'">
<pre <solution
class="elevation-2 solution-code pl-2" :pk=pk
:class="programmingLanguage" :solution=solution
><span v-html="highlightedSolution"></span></pre> :programmingLanguage=programmingLanguage
:solutionComments=solutionComments
:showSolutionComments=showSolutionComments
>
</solution>
</div> </div>
</v-expansion-panel-content> </v-expansion-panel-content>
</v-expansion-panel> </v-expansion-panel>
...@@ -36,9 +49,17 @@ import Component from 'vue-class-component' ...@@ -36,9 +49,17 @@ import Component from 'vue-class-component'
import { Prop } from 'vue-property-decorator' import { Prop } from 'vue-property-decorator'
import { highlight } from 'highlight.js' import { highlight } from 'highlight.js'
import { UI } from '@/store/modules/ui' import { UI } from '@/store/modules/ui'
import { SolutionComment } from '../../models';
import Solution from '@/components/submission_type/solution/Solution.vue'
@Component @Component({
components: {Solution}
})
export default class SubmissionType extends Vue { export default class SubmissionType extends Vue {
@Prop({
type: String,
required: true,
}) pk!: string
@Prop({ @Prop({
type: String, type: String,
required: true required: true
...@@ -64,6 +85,10 @@ export default class SubmissionType extends Vue { ...@@ -64,6 +85,10 @@ export default class SubmissionType extends Vue {
type: Boolean, type: Boolean,
default: false default: false
}) reverse!: boolean }) reverse!: boolean
@Prop({
type: Object,
default: {},
}) solutionComments!: {[ofLine: number]: SolutionComment[]}
@Prop({ @Prop({
type: Object, type: Object,
default: function () { default: function () {
...@@ -78,6 +103,8 @@ export default class SubmissionType extends Vue { ...@@ -78,6 +103,8 @@ export default class SubmissionType extends Vue {
? [this.expandedByDefault.Description, this.expandedByDefault.Solution] ? [this.expandedByDefault.Description, this.expandedByDefault.Solution]
: [this.expandedByDefault.Solution, this.expandedByDefault.Description] : [this.expandedByDefault.Solution, this.expandedByDefault.Description]
showSolutionComments = true
get typeItems () { get typeItems () {
let items = [ let items = [
{ {
...@@ -107,10 +134,6 @@ export default class SubmissionType extends Vue { ...@@ -107,10 +134,6 @@ export default class SubmissionType extends Vue {
</script> </script>
<style> <style>
.solution-code {
border-width: 0px;
white-space: pre-wrap;
}
.type-description code { .type-description code {
background-color: lightgrey; background-color: lightgrey;
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<v-card-title class="title">Task types</v-card-title> <v-card-title class="title">Task types</v-card-title>
<v-layout row wrap> <v-layout row wrap>
<v-flex xs3> <v-flex xs3>
<v-list> <v-list id="submission-types-list">
<v-list-tile <v-list-tile
v-for="submissionType in sortedSubmissionTypes" :key="submissionType.pk" v-for="submissionType in sortedSubmissionTypes" :key="submissionType.pk"
@click="selectedSubmissionType = submissionType" @click="selectedSubmissionType = submissionType"
...@@ -25,20 +25,29 @@ ...@@ -25,20 +25,29 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex' import SubmissionType from '@/components/submission_type/SubmissionType'
import SubmissionType from '@/components/SubmissionType' import store from '@/store/store';
export default { export default {
name: 'SubmissionTypesOverview', name: 'SubmissionTypesOverview',
components: { SubmissionType }, components: { SubmissionType },
data () { data () {
return { return {
selectedSubmissionType: null selectedSubmissionTypePk: null
} }
}, },
computed: { computed: {
...mapState([ submissionTypes () {
'submissionTypes' return store.state.submissionTypes
]), },
// needed to keep selectedSubmissionType reactive
selectedSubmissionType: {
get: function () {
return store.state.submissionTypes[this.selectedSubmissionTypePk]
},
set: function (newSubType) {
this.selectedSubmissionTypePk = newSubType.pk
}
},
sortedSubmissionTypes () { sortedSubmissionTypes () {
return Object.values(this.submissionTypes).sort((t1, t2) => { return Object.values(this.submissionTypes).sort((t1, t2) => {
let lowerName1 = t1.name.toLowerCase() let lowerName1 = t1.name.toLowerCase()
......
<template>
<table class="solution-table">
<tr v-for="(code, lineNo) in highlightedSolution" :key="`${pk}:${lineNo}`" :id="`solution-line-${lineNo}`">
<td class="line-number-cell" :style="backgroundColor(lineNo)">
<v-btn
flat
block
depressed
class="line-number-btn"
@click="toggleEditor(lineNo)">{{lineNo}}</v-btn>
</td>
<td class="code-cell-content pl-2">
<span v-html="code" class="code-line"></span>
<template
v-if="solutionComments[lineNo] && solutionComments[lineNo].length && showSolutionComments">
<solution-comment
v-for="comment in solutionComments[lineNo]"
v-bind="comment"
:key="comment.pk"
@update-submission-type="updateSubmissionType"
@toggle-editor="toggleEditor(lineNo)"
@toggle-eidt-editor="toggleEditor(lineNo)"
/>
</template>
<template v-if="showEditorOnline[lineNo]">
<v-textarea
name="solution-comment-input"
label="Here you can comment the solution. Other tutors will see those comments."
v-model="editedSolutionComments[lineNo]"
@keyup.enter.ctrl.exact="submitSolutionComment(lineNo)"
@keyup.esc="collapseTextField(lineNo)"
@focus="selectInput($event)"
rows="2"
outline
autofocus
auto-grow
hide-details
class="mx-2"
/>
<v-btn id="submit-comment" color="success" @click="submitSolutionComment(lineNo)"><v-icon>check</v-icon>Submit</v-btn>
<v-btn id="cancel-comment" @click="toggleEditor(lineNo)"><v-icon>cancel</v-icon>cancel</v-btn>
</template>
</td>
</tr>
</table>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator"
import { SolutionComment, FeedbackComment } from "../../../models"
import { highlight } from "highlight.js"
import { syntaxPostProcess, objectifyArray } from "../../../util/helpers"
import SolutionCommentComponent from "@/components/submission_type/solution/SolutionComment.vue"
import * as api from '@/api'
import { actions } from '../../../store/actions';
@Component({
components: {'SolutionComment': SolutionCommentComponent}
})
export default class Solution extends Vue {
@Prop({
type: String,
required: true
})
pk!: string
@Prop({
type: String,
required: false,
default: ""
})
solution!: string
@Prop({
type: String,
default: "c"
})
programmingLanguage!: string
@Prop({
type: Object,
default: {}
})
solutionComments!: { [ofLine: number]: SolutionComment[] }
@Prop({
type: Boolean,
default: true
})
showSolutionComments!: boolean
timer = 0
showEditorOnline: {[ofLine: number]: boolean} = {}
editedSolutionComments: {[ofLine: number]: string} = {}
get highlightedSolution() {
const highlighted = highlight(this.programmingLanguage, this.solution, true)
.value
const postprocessed = syntaxPostProcess(highlighted)
return postprocessed
.split("\n")
.reduce((acc: { [k: number]: string }, curr, index) => {
acc[index + 1] = curr
return acc
}, {})
}
get lineNoHint() {
if (this.showSolutionComments) {
// will return a falsy value if indexed with a line number
// meaning no hint will be displayed
return {}
} else {
// returning the solutionComments will return a truthy value
// if indexed with the line number where comments are located
return this.solutionComments
}
}
backgroundColor(lineNo: number) {
if (this.lineNoHint[lineNo]) {
return 'backgroundColor: #64B5F6;'
} else {
'backgroundColor: transparent;'
}
}
selectInput (event: Event) {
if (event !== null) {
const target = event.target as HTMLTextAreaElement
target.select()
}
}
toggleEditor(lineNo: number) {
Vue.set(this.showEditorOnline, lineNo, !this.showEditorOnline[lineNo])
}
async submitSolutionComment(lineNo: number) {
const comment = {
text: this.editedSolutionComments[lineNo],
ofLine: lineNo,
ofSubmissionType: this.pk
}
await api.createSolutionComment(comment)
await actions.updateSubmissionType(this.pk)
this.toggleEditor(lineNo)
this.editedSolutionComments[lineNo] = ''
}
updateSubmissionType() {
actions.updateSubmissionType(this.pk)
}
mounted() {
this.timer = setInterval(() => {
actions.updateSubmissionType(this.pk)
}, 10 * 1e3)
}
beforeDestroy() {
clearInterval(this.timer)
}
}
</script>
<style scoped>
.solution-table {
table-layout: auto;
border-collapse: collapse;
width: 100%;
}
.line-number-cell {
vertical-align: top
}
.code-cell-content {
width: 100%
}
.code-line {
white-space: pre-wrap;
font-family: monospace;
}
.line-number-btn {
height: fit-content;
min-width: 50px;
margin: 0;
border-radius: 0px;
}
</style>
<template>
<div>
<div class="dialog-box" @click="$emit('toggle-editor')">
<div class="body elevation-1" :style="{borderColor: '#3D8FC1', backgroundColor}">
<span class="tip tip-up" :style="{borderBottomColor: '#3D8FC1'}"></span>
<span v-if="ofUser" class="of-user">Of user: {{ofUser}}</span>
<span class="comment-created">{{parsedCreated}}</span>
<div class="message">{{text}}</div>
<v-btn
flat icon absolute
class="delete-button"
v-if="deletable"
@click.stop="deleteConfirmation = true"
>
<v-icon color="grey darken-1" size="20px">delete_forever</v-icon>
</v-btn>
<v-btn
flat icon absolute
class="edit-button"
v-if="editable"
@click.stop="toggleEditing()"
>
<v-icon color="grey darken-1" size="20px">edit</v-icon>
</v-btn>
</div>
</div>
<template v-if="editing">
<v-textarea
name="solution-comment-edit"
label="Here you can edit your comment"
v-model="editedText"
@keyup.enter.ctrl.exact="submitEdit"
@keyup.esc="editing = false"
@focus="selectInput($event)"
rows="2"
outline
autofocus
auto-grow
hide-details
class="mx-2"
/>
<v-btn id="submit-comment" color="success" @click="submitEdit"><v-icon>check</v-icon>Submit</v-btn>
<v-btn id="cancel-comment" @click="editing = false"><v-icon>cancel</v-icon>cancel</v-btn>
</template>
<v-dialog
v-model="deleteConfirmation"
max-width="max-content"
>
<v-card
class="text-xs-center pa-2"
>
<v-card-title class="title">
Delete permanently?
</v-card-title>
<v-card-actions>
<v-btn :id="`confirm-delete-comment`" color="red lighten-1" @click="deleteComment">delete</v-btn>
<v-btn @click="deleteConfirmation = false">cancel</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script lang="ts">
import {Vue, Component, Prop, Provide} from 'vue-property-decorator'
import { UI } from '@/store/modules/ui'
import { SubmissionNotes } from '@/store/modules/submission-notes'
import { Authentication } from '../../../store/modules/authentication';
import * as api from "@/api"
import { actions } from '@/store/actions';
@Component
export default class SolutionComment extends Vue {
@Prop({
type: Number,
required: true
}) pk!: number
@Prop({
type: String,
required: true
}) text!: string
@Prop({
type: String,
required: false
}) created?: string
@Prop({
type: String,
required: true
}) ofUser!: string
@Prop({
type: Number,
required: true
}) ofLine!: number
editing: boolean = false
editedText: string = ''
deleteConfirmation: boolean = false
get parsedCreated() {
if (this.created) {
return new Date(this.created).toLocaleString()
} else {
return 'Just now'
}
}
get backgroundColor () {
return UI.state.darkMode ? 'grey' : '#F3F3F3'
}
get deletable() {
return Authentication.state.user.username === this.ofUser || Authentication.isReviewer
}
get editable() {
return Authentication.state.user.username === this.ofUser
}
toggleEditing() {
console.log('adasd')
this.editing = !this.editing
this.editedText = this.text
}
async deleteComment() {
await api.deleteSolutionComment(this.pk)
this.$emit('update-submission-type')
}
async submitEdit() {
await api.patchSolutionComment({pk: this.pk, text: this.editedText})
this.editing = false
this.$emit('update-submission-type')
}
selectInput (event: Event) {
if (event !== null) {
const target = event.target as HTMLTextAreaElement
target.select()
}
}
}
</script>
<style scoped>
.tip {
width: 0px;
height: 0px;
position: absolute;
background: transparent;
border: 10px solid;
}
.tip-up {
top: -22px; /* Same as body margin top + border */
left: 10px;
border-right-color: transparent;
border-left-color: transparent;
border-top-color: transparent;
}
.dialog-box .body {
cursor: pointer;
position: relative;
height: auto;
margin: 20px 10px 10px 10px;
padding: 5px;
border-radius: 0px;
border: 2px solid;
}
.body .message {
min-height: 30px;
border-radius: 3px;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
}
.delete-button {
bottom: -12px;
left: -42px;
}
.edit-button {
bottom: 15px;
left: -42px;
}
.comment-created {
position: absolute;
font-size: 10px;
right: 4px;
top: -20px;
}
.of-user {
position: absolute;
font-size: 13px;
top: -20px;
left: 50px;
}
</style>