diff --git a/core/models.py b/core/models.py index 80c3a42fde446ee8022779ccca3695c42b868c7f..7e680a2722c9422f32578d4ef699b17c14f66bca 100644 --- a/core/models.py +++ b/core/models.py @@ -244,7 +244,7 @@ class StudentInfo(models.Model): }) @classmethod - def get_overall_score_annotated_submission_list(cls) -> QuerySet: + def get_annotated_score_submission_list(cls) -> QuerySet: """Can be used to quickly annotate a user with the necessary information on the overall score of a student and if he does not need any more correction. @@ -267,7 +267,7 @@ class StudentInfo(models.Model): default=Value(0), output_field=BooleanField() ) - ) + ).order_by('user__username') def disable(self): """The student won't be able to login in anymore, but his current diff --git a/core/tests/test_export.py b/core/tests/test_export.py new file mode 100644 index 0000000000000000000000000000000000000000..182db20688ec5fa72adead7d9b8cc03927acae10 --- /dev/null +++ b/core/tests/test_export.py @@ -0,0 +1,86 @@ +from rest_framework import status +from django.test import TestCase, Client + +from util.factories import make_test_data + + +class ExportCSVTest(TestCase): + + @classmethod + def setUpTestData(cls): + cls.data = make_test_data(data_dict={ + 'submission_types': [ + { + 'name': '01. Sort', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + }, + { + 'name': '02. Shuffle', + 'full_score': 35, + 'description': 'Very complicated', + 'solution': 'Trivial!' + } + ], + 'students': [ + {'username': 'student01'}, + {'username': 'student02'} + ], + 'reviewers': [ + {'username': 'reviewer'} + ], + 'submissions': [ + { + 'text': 'function blabl\n' + ' on multi lines\n' + ' for blabla in bla:\n' + ' lorem ipsum und so\n', + 'type': '01. Sort', + 'user': 'student01', + 'feedback': { + 'score': 5, + 'is_final': True, + 'feedback_lines': { + '1': [{ + 'text': 'This is very bad!', + 'of_tutor': 'reviewer' + }], + } + + } + }, + { + 'text': 'not much', + 'type': '02. Shuffle', + 'user': 'student01' + }, + { + 'text': 'function blabl\n' + ' asasxasx\n' + ' lorem ipsum und so\n', + 'type': '01. Sort', + 'user': 'student02' + }, + { + 'text': 'not much to see here', + 'type': '02. Shuffle', + 'user': 'student02' + } + ]} + ) + + def setUp(self): + client = Client() + client.force_login(user=self.data['reviewers'][0]) + + self.response = client.get('/export/csv/') + + def test_can_access(self): + self.assertEqual(status.HTTP_200_OK, self.response.status_code) + + def test_data_is_correct(self): + head, student1, student2, _ = self.response.content.split(b'\r\n') + self.assertIn(b'Matrikel,Username,Name,Sum,01. Sort,02. Shuffle', head) + self.assertIn(b'student01,,5,5,0', student1) + self.assertIn(b'student02,,0,0,0', student2) diff --git a/core/views/__init__.py b/core/views/__init__.py index 990e8c5c416a1bd605e7b85a4ebbe9d352cf967c..beac6e7a61cd1347994d51b860dbae2f00a17531 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -1,3 +1,4 @@ from .feedback import FeedbackApiView # noqa from .subscription import SubscriptionApiViewSet, AssignmentApiViewSet # noqa from .common_views import * # noqa +from .export import StudentCSVExport # noqa diff --git a/core/views/export.py b/core/views/export.py new file mode 100644 index 0000000000000000000000000000000000000000..e08fa7d854b56b288afc8676e166fc13e1860277 --- /dev/null +++ b/core/views/export.py @@ -0,0 +1,39 @@ +import csv + +from django.contrib.auth.mixins import PermissionRequiredMixin +from django.http import HttpResponse +from django.views.generic import View + +from core.models import StudentInfo, SubmissionType +from core.permissions import IsReviewer + + +class StudentCSVExport(PermissionRequiredMixin, View): + + def has_permission(self): + return IsReviewer().has_permission(self.request, self) + + def get(self, request, format=None): + # Create the HttpResponse object with the appropriate CSV header. + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="results.csv"' + + writer = csv.writer(response) + writer.writerow(['Matrikel', + 'Username', + 'Name', + 'Sum', + *SubmissionType.objects + .order_by('name') + .values_list('name', flat=True)]) + + for student in StudentInfo.get_annotated_score_submission_list(): + writer.writerow([ + student.matrikel_no, + student.user.username, + student.user.fullname, + student.overall_score, + *student.score_per_submission().values() + ]) + + return response diff --git a/grady/urls.py b/grady/urls.py index 8e651c92f337a55689fc2af3e6a79a94df55251f..ae77b57f317046247e23621c766eb26f09bd5c76 100644 --- a/grady/urls.py +++ b/grady/urls.py @@ -3,6 +3,8 @@ from django.urls import include, path from django.views.generic.base import TemplateView from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token +from core import views + urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('core.urls')), @@ -10,6 +12,6 @@ urlpatterns = [ path('api/refresh-token/', refresh_jwt_token), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), - path('', TemplateView.as_view(template_name='index.html')) - + path('', TemplateView.as_view(template_name='index.html')), + path('export/csv/', views.StudentCSVExport.as_view(), name='export-csv'), ] diff --git a/util/format_index.py b/util/format_index.py index c3f1f7d6fd702c637d49ccbbca4f3e64feaade24..bf2ffa8f86f6ef38eacbe2e3795d2e5b14763905 100644 --- a/util/format_index.py +++ b/util/format_index.py @@ -1,5 +1,5 @@ -import sys import fileinput +import sys file = 'core/templates/index.html'