diff --git a/core/serializers/__init__.py b/core/serializers/__init__.py index 5cafb28c394618046fc8a9d31c79b496a01cd422..0306ce607329110c9e7268cf88d30968638a2615 100644 --- a/core/serializers/__init__.py +++ b/core/serializers/__init__.py @@ -1,5 +1,5 @@ from .common_serializers import * # noqa -from .feedback import FeedbackSerializer # noqa +from .feedback import FeedbackSerializer, FeedbackCommentSerializer # noqa from .subscription import * # noqa from .student import * # noqa from .submission import * # noqa diff --git a/core/tests/test_feedback.py b/core/tests/test_feedback.py index c219388ed43498336a754a6d572db27745edcfad..6677eb066caa509bdab25fac4c937d055fe0ad52 100644 --- a/core/tests/test_feedback.py +++ b/core/tests/test_feedback.py @@ -1,7 +1,7 @@ import unittest from rest_framework import status -from rest_framework.test import (APIRequestFactory, APITestCase) +from rest_framework.test import APIRequestFactory, APITestCase from core import models from core.models import Feedback, FeedbackComment, Submission, SubmissionType @@ -447,12 +447,12 @@ class FeedbackCommentApiEndpointTest(APITestCase): comment = FeedbackComment.objects.get(of_tutor=self.tutor02) self.client.force_authenticate(self.tutor01) response = self.client.delete(self.url % comment.pk) - self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) + self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code) def test_reviewer_can_delete_everything_they_want(self): reviewer = self.data['reviewers'][0] self.client.force_authenticate(user=reviewer) - comment01 = FeedbackComment.objects.get(of_tutor=self.tutor02) + comment01 = FeedbackComment.objects.get(of_tutor=self.tutor01) comment02 = FeedbackComment.objects.get(of_tutor=self.tutor02) response = self.client.delete(self.url % comment01.pk) @@ -460,3 +460,26 @@ class FeedbackCommentApiEndpointTest(APITestCase): response = self.client.delete(self.url % comment02.pk) self.assertEqual(status.HTTP_204_NO_CONTENT, response.status_code) + try: + FeedbackComment.objects.get(of_tutor=self.tutor01) + FeedbackComment.objects.get(of_tutor=self.tutor02) + except FeedbackComment.DoesNotExist: + pass + else: + self.fail('No exception raised') + + def test_reviewer_can_set_comment_visibility(self): + reviewer = self.data['reviewers'][0] + self.client.force_authenticate(user=reviewer) + comment = FeedbackComment.objects.get(of_tutor=self.tutor01) + self.assertTrue(comment.visible_to_student) + + data = { + 'visible_to_student': False + } + + response = self.client.patch(self.url % comment.pk, data) + + self.assertFalse(response.data['visible_to_student']) + comment.refresh_from_db() + self.assertFalse(comment.visible_to_student) diff --git a/core/views/feedback.py b/core/views/feedback.py index 3e7a10cf2713cf9b7f12acd799ba173bdd793cbe..1eb11a056e7f8fae24044e8616358cec9ca2b907 100644 --- a/core/views/feedback.py +++ b/core/views/feedback.py @@ -112,16 +112,27 @@ class FeedbackApiView( return Response(serializer.data) -class FeedbackCommentApiView(viewsets.GenericViewSet): +class FeedbackCommentApiView( + mixins.DestroyModelMixin, + viewsets.GenericViewSet): """ Gets a list of an individual exam by Id if provided """ permission_classes = (permissions.IsTutorOrReviewer,) queryset = models.FeedbackComment.objects.all() + serializer_class = serializers.FeedbackCommentSerializer - def destroy(self, request, *args, **kwargs): - comment = self.get_object() + def get_queryset(self): + user = self.request.user + if user.role == models.UserAccount.REVIEWER: + return self.queryset + return self.queryset.filter(of_tutor=user) - user = request.user - if user.role == models.UserAccount.TUTOR and user != comment.of_tutor: - raise PermissionDenied(detail='Can only delete your own commits.') + def partial_update(self, request, **kwargs): + keys = self.request.data.keys() + if keys - {'visible_to_student', 'of_line', 'text'}: + raise PermissionDenied('These fields cannot be changed.') - return Response(status=status.HTTP_204_NO_CONTENT) + comment = self.get_object() + serializer = self.get_serializer(comment, request.data, partial=True) + serializer.is_valid() + serializer.save() + return Response(serializer.data) diff --git a/util/convert.py b/util/convert.py index 82041e1997e20dfb14e7494ca8d76444759d4552..0c45f9472e83c59c0d2e752097fab37ae5301ae9 100755 --- a/util/convert.py +++ b/util/convert.py @@ -60,7 +60,7 @@ parser.add_argument( user_t = namedtuple('user_head', 'name matrikel_no') # one task has a title and id and hpfly code -task_head_re = re.compile(r'^Quellcode Frage (?P<title>.*) ?(\d{8})?$') +task_head_re = re.compile(r'^Quellcode Frage (?P<title>.*?) ?(\d{8})?$') # nor parsing the weird mat no matno_re = re.compile(r'^(?P<matrikel_no>\d{8})-(\d+)-(\d+)$') @@ -73,8 +73,9 @@ def converter(infile, usernames=None, number_of_tasks=0,): def sheet_iter_meta(sheet): """ yield first and second col entry as tuple of (name, matnr) """ for row in (sheet.row(i) for i in range(1, sheet.nrows)): - m = re.search(matno_re, row[1].value) - yield row[0].value, m.group('matrikel_no') if m else row[1].value + match = re.search(matno_re, row[1].value) + if match: + yield row[0].value, match.group('matrikel_no') def sheet_iter_data(sheet): """ yields all source code titel and code tuples """ @@ -90,7 +91,7 @@ def converter(infile, usernames=None, number_of_tasks=0,): # nice! name2mat = dict(sheet_iter_meta(meta)) - assert meta.nrows - 1 == len(name2mat), f'{meta.nrows} != {len(name2mat)}' + assert len(name2mat) == len(data), f'{len(name2mat)} names != {len(data)} sheets' # noqa # from xls to lists and namedtuples # [ [user0, task0_h, code0, ..., taskn, coden ], ..., [...] ] diff --git a/util/importer.py b/util/importer.py index 9f60042b62339373c21f7a43fb1a95da937202e1..e83312bee8c6b3e882d91d5e4aa5cca826c8dc6b 100644 --- a/util/importer.py +++ b/util/importer.py @@ -216,7 +216,7 @@ def do_load_submission_types(): for row in csv_rows: tid, name, score = (col.strip() for col in row) with \ - open(os.path.join(lsg_dir, tid + '-lsg.c'), + open(os.path.join(lsg_dir, tid + '.c'), encoding='utf-8') as lsg, \ open(os.path.join(desc_dir, tid + '.html'), encoding='utf-8') as desc: diff --git a/util/processing.py b/util/processing.py index 58b0420f835aae0edba04f650d81d49d9ad9f7b7..2ffd2d86010103b110c5cc5aeaa20cd765c6c4f8 100644 --- a/util/processing.py +++ b/util/processing.py @@ -1,6 +1,7 @@ import abc import hashlib import json +import logging import os import re import shutil @@ -14,6 +15,8 @@ try: except ModuleNotFoundError: from util import testcases +log = logging.getLogger(__name__) + def run_cmd(cmd, stdin=None, check=False, timeout=1): return subprocess.run( @@ -37,6 +40,12 @@ def sha1(submission_obj): return hashlib.sha1(submission_obj['code'].encode()).hexdigest() +def get_submission_id(submission_obj): + t = submission_obj['type'] + m = re.search(r'(a0\d)', t) + return m.group(0) + + class Test(metaclass=abc.ABCMeta): """docstring for IliasQuestion""" @@ -127,11 +136,11 @@ class LinkTest(Test): def run_test(self, submission_obj): - t = submission_obj['type'] - m = re.search(r'(a0\d)', t) + if submission_obj['type'] not in testcases_dict: + return False, 'This program was not required to be executable.' - ret = run_cmd( - f"gcc-7 -o code objects/{m.group(0)}-testing.o code.o") + cid = get_submission_id(submission_obj) + ret = run_cmd(f"gcc-7 -o ./bin/{cid} objects/{cid}-testing.o code.o") return not ret.returncode, ret.stderr @@ -143,9 +152,9 @@ class UnitTestTest(Test): label_failure = 'UNITTEST_FAILED' @staticmethod - def testcase(i, args, stdout): + def testcase(i, args, stdout, cid): try: - ret = run_cmd("./code %s" % args, check=True, timeout=0.1) + ret = run_cmd("./bin/%s %s" % (cid, args), check=True, timeout=0.1) assert ret.stdout == stdout except AssertionError: return False, "Case #{}: [ASSERT FAIL] ./prog {:>2} WAS '{}' SHOULD '{}'".format( # noqa: E501 @@ -160,10 +169,13 @@ class UnitTestTest(Test): def run_test(self, submission_obj): - task = self.testcases_dict[submission_obj['type']] - results, messages = zip(*list(self.testcase(i, case, result) - for i, (case, result) in enumerate( - zip(task['cases'], task['results'])))) + task = testcases_dict[submission_obj['type']] + cid = get_submission_id(submission_obj) + return_data = [self.testcase(i, case, result, cid) + for i, (case, result) in enumerate(zip(task['cases'], + task['results'])) + ] + results, messages = zip(*return_data) return all(results), '\n'.join(messages) @@ -171,8 +183,8 @@ class UnitTestTest(Test): def process(descfile, binaries, objects, submissions, header, highest_test): if isinstance(highest_test, str): highestTestClass = Test.available_tests()[highest_test] - highestTestClass.testcases_dict = testcases.evaluated_testcases( - descfile, binaries) + global testcases_dict + testcases_dict = testcases.evaluated_testcases(descfile, binaries) with open(submissions) as submission_file: submissions_json = json.JSONDecoder().decode( @@ -180,10 +192,11 @@ def process(descfile, binaries, objects, submissions, header, highest_test): # Get something disposable path = tempfile.mkdtemp() - run_cmd(f'cp -r {objects} {path}') + run_cmd(f'cp -r {objects} {path}') run_cmd(f'cp -r {binaries} {path}') run_cmd(f'cp -r {header} {path}') os.chdir(path) + os.makedirs('bin') def iterate_submissions(): yield from (obj @@ -207,6 +220,7 @@ def parseme(): parser.add_argument('objects') parser.add_argument('submissions') parser.add_argument('header') + parser.add_argument('test') return parser.parse_args() @@ -214,10 +228,12 @@ if __name__ == '__main__': args = parseme() testcases_dict = testcases.evaluated_testcases(args.descfile, args.binaries) + print(json.dumps(process(args.descfile, args.binaries, args.objects, args.submissions, - args.header, UnitTestTest), + args.header, + args.test), sort_keys=True, indent=4)) diff --git a/util/testcases.py b/util/testcases.py index 99729af47c06f6fa4d79ad25bace529f0ee955f6..874d8549c7aea1c64f36ef82657d6ad05a86216a 100644 --- a/util/testcases.py +++ b/util/testcases.py @@ -62,6 +62,10 @@ def testcases_generator(task, n=10): if not syntax: return + if syntax == 'NO INPUT': + yield 'NO INPUT' + return + yield '' yield '0'