From 38c42d20becac0c15aeca1b13e14e57ba2f182e1 Mon Sep 17 00:00:00 2001
From: janmax <j.michal@stud.uni-goettingen.de>
Date: Fri, 16 Feb 2018 17:46:00 +0100
Subject: [PATCH] Added an export endpoint that produces a CSV file

---
 core/models.py            |  4 +-
 core/tests/test_export.py | 86 +++++++++++++++++++++++++++++++++++++++
 core/views/__init__.py    |  1 +
 core/views/export.py      | 39 ++++++++++++++++++
 grady/urls.py             |  6 ++-
 util/format_index.py      |  2 +-
 6 files changed, 133 insertions(+), 5 deletions(-)
 create mode 100644 core/tests/test_export.py
 create mode 100644 core/views/export.py

diff --git a/core/models.py b/core/models.py
index 80c3a42f..7e680a27 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 00000000..182db206
--- /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 990e8c5c..beac6e7a 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 00000000..e08fa7d8
--- /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 8e651c92..ae77b57f 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 c3f1f7d6..bf2ffa8f 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'
 
-- 
GitLab