From 744df9529e6c6a531449d5a7008e29ad67b53436 Mon Sep 17 00:00:00 2001
From: janmax <j.michal@stud.uni-goettingen.de>
Date: Sat, 17 Feb 2018 15:34:26 +0100
Subject: [PATCH] Fixes a nasty race condition

---
 core/apps.py                                  |  3 +++
 .../0003_submissiondoneassignmentscount.py    | 22 +++++++++++++++++++
 core/models.py                                | 19 +++++++++++-----
 core/signals.py                               | 13 +++++++++++
 .../test_subscription_assignment_service.py   | 18 +++++----------
 util/factories.py                             |  4 ++--
 util/importer.py                              |  2 +-
 7 files changed, 60 insertions(+), 21 deletions(-)
 create mode 100644 core/migrations/0003_submissiondoneassignmentscount.py
 create mode 100644 core/signals.py

diff --git a/core/apps.py b/core/apps.py
index 089650b7..17b18064 100644
--- a/core/apps.py
+++ b/core/apps.py
@@ -4,3 +4,6 @@ from django.apps import AppConfig
 class CoreConfig(AppConfig):
     name = 'core'
     verbose_name = 'where everything comes together'
+
+    def ready(self):
+        import core.signals  # noqa
diff --git a/core/migrations/0003_submissiondoneassignmentscount.py b/core/migrations/0003_submissiondoneassignmentscount.py
new file mode 100644
index 00000000..0d1f0b23
--- /dev/null
+++ b/core/migrations/0003_submissiondoneassignmentscount.py
@@ -0,0 +1,22 @@
+# Generated by Django 2.0.2 on 2018-02-17 12:01
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0002_auto_20180210_1727'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='SubmissionDoneAssignmentsCount',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('done_assignments', models.PositiveIntegerField(default=0)),
+                ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='counter', to='core.Submission')),
+            ],
+        ),
+    ]
diff --git a/core/models.py b/core/models.py
index 7e680a27..6b823bdc 100644
--- a/core/models.py
+++ b/core/models.py
@@ -502,6 +502,7 @@ class SubmissionSubscription(models.Model):
         if self.query_type == self.RANDOM:
             return Submission.objects.all()
 
+        TutorSubmissionAssignment.objects.select_for_update()
         return Submission.objects.filter(
             **{self.type_query_mapper[self.query_type]: self.query_key})
 
@@ -521,11 +522,6 @@ class SubmissionSubscription(models.Model):
             Q(assignments__subscription__owner=self.owner)
         ).exclude(
             assignments__is_done=False
-        ).annotate(
-            done_assignments_count=Count(
-                Case(When(assignments__is_done=True, then=Value(1)),
-                     output_field=IntegerField(),)
-            )
         )
 
     def _get_available_submissions_in_subscription_stage(self) -> QuerySet:
@@ -546,7 +542,7 @@ class SubmissionSubscription(models.Model):
 
         done_assignments_count = self.assignment_count_on_stage[self.feedback_stage]  # noqa
         stage_candiates = candidates.filter(
-            done_assignments_count=done_assignments_count,
+            counter__done_assignments=done_assignments_count,
         )
 
         if stage_candiates.count() == 0:
@@ -607,6 +603,14 @@ class DeletionOfDoneAssignmentsNotPermitted(Exception):
     pass
 
 
+class SubmissionDoneAssignmentsCount(models.Model):
+
+    submission = models.OneToOneField('submission',
+                                      related_name='counter',
+                                      on_delete=models.CASCADE)
+    done_assignments = models.PositiveIntegerField(default=0)
+
+
 class TutorSubmissionAssignment(models.Model):
 
     assignment_id = models.UUIDField(primary_key=True,
@@ -623,6 +627,9 @@ class TutorSubmissionAssignment(models.Model):
 
     @transaction.atomic
     def set_done(self):
+        if not self.is_done:
+            self.submission.counter.done_assignments += 1
+            self.submission.counter.save()
         self.is_done = True
         self.save()
 
diff --git a/core/signals.py b/core/signals.py
new file mode 100644
index 00000000..57173f12
--- /dev/null
+++ b/core/signals.py
@@ -0,0 +1,13 @@
+from core.models import Submission, SubmissionDoneAssignmentsCount
+
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+
+@receiver(post_save, sender=Submission)
+def create_counter_after_submission_create(sender,
+                                           instance,
+                                           created,
+                                           **kwargs):
+    if created:
+        SubmissionDoneAssignmentsCount.objects.create(submission=instance)
diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py
index ea2533b8..b2c1c7e4 100644
--- a/core/tests/test_subscription_assignment_service.py
+++ b/core/tests/test_subscription_assignment_service.py
@@ -109,12 +109,6 @@ class TestApiEndpoints(APITestCase):
                     'full_score': 35,
                     'description': 'Very complicated',
                     'solution': 'Trivial!'
-                },
-                {
-                    'name': '03. This one exists for the sole purpose to test',
-                    'full_score': 30,
-                    'description': 'Very complicated',
-                    'solution': 'Trivial!'
                 }
             ],
             'students': [
@@ -160,12 +154,12 @@ class TestApiEndpoints(APITestCase):
                             '   on multi lines\n'
                             '       asasxasx\n'
                             '           lorem ipsum und so\n',
-                    'type': '03. This one exists for the sole purpose to test',
-                    'user': 'student01'
+                    'type': '01. Sort this or that',
+                    'user': 'student02'
                 },
                 {
                     'text': 'function lorem ipsum etc\n',
-                    'type': '03. This one exists for the sole purpose to test',
+                    'type': '02. Merge this or that or maybe even this',
                     'user': 'student02'
                 },
             ]}
@@ -242,17 +236,17 @@ class TestApiEndpoints(APITestCase):
         client = APIClient()
         client.force_authenticate(user=self.data['reviewers'][0])
 
-        student01 = self.data['students'][0]
+        student = self.data['students'][0]
 
         response = client.post(
             '/api/subscription/', {
                 'query_type': 'student',
-                'query_key': student01.student.student_id,
+                'query_key': student.student.student_id,
                 'stage': 'feedback-creation'
             })
 
         assignments = response.data['assignments']
-        self.assertEqual(2, len(assignments))
+        self.assertEqual(1, len(assignments))
 
     def test_two_tutors_cant_have_assignments_for_same_submission(self):
         client = APIClient()
diff --git a/util/factories.py b/util/factories.py
index 89957819..db42ebcb 100644
--- a/util/factories.py
+++ b/util/factories.py
@@ -53,7 +53,7 @@ class GradyUserFactory:
         }[role]
 
     def _make_base_user(self, username, role, password=None,
-                        store_pw=False, name='', **kwargs):
+                        store_pw=False, fullname='', **kwargs):
         """ This is a specific wrapper for the django update_or_create method of
         objects.
             * If now username is passed, a generic one will be generated
@@ -72,7 +72,7 @@ class GradyUserFactory:
 
         user, created = UserAccount.objects.update_or_create(
             username=username,
-            fullname=name,
+            fullname=fullname,
             role=role,
             defaults=kwargs)
 
diff --git a/util/importer.py b/util/importer.py
index 683dccc7..9f60042b 100644
--- a/util/importer.py
+++ b/util/importer.py
@@ -110,7 +110,7 @@ def add_tests(submission_obj, tests):
 
     for name, test_data in ((name, tests[name]) for name in TEST_ORDER):
         test_obj, created = Test.objects.update_or_create(
-            name=test_data['fullname'],
+            name=test_data['name'],
             submission=submission_obj,
             defaults={
                 'label': test_data['label'],
-- 
GitLab