We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Different submission count between course "exercise results" page and assignment submission view
I spotted this in Aalto's production A+ (I'm a student enrolled on O1).

When I view the "exercise results" page, for one assignment I see the following (note the "0/10" submissions):

![Screenshot 2019-11-30 at 19 29 42](https://user-images.githubusercontent.com/6674046/69904228-cba34900-13ac-11ea-8b03-1c05dddb85e4.png)

When I follow the link to the chapter, and the view the assignment, it states I've used one submission (which is currently ungraded).
 
![Screenshot 2019-11-30 at 19 29 55](https://user-images.githubusercontent.com/6674046/69904238-e7a6ea80-13ac-11ea-8e30-17e91ea93eff.png)

It seems to only count graded submissions (this particular assignment is human marked).

This may be the intended behaviour, but it felt wrong that they were not consistent and it caused me a brief moment of intense "did I forget to submit the big assignment before the deadline" panic.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `exercise/cache/points.py`
Content:
```
1 from copy import deepcopy
2 from django.db.models.signals import post_save, post_delete, m2m_changed
3 from django.utils import timezone
4 
5 from lib.cache import CachedAbstract
6 from notification.models import Notification
7 from ..models import LearningObject, Submission
8 from .hierarchy import ContentMixin
9 
10 
11 def has_more_points(submission, current_entry):
12     return submission.grade >= current_entry['points']
13 
14 
15 class CachedPoints(ContentMixin, CachedAbstract):
16     KEY_PREFIX = 'points'
17 
18     def __init__(self, course_instance, user, content):
19         self.content = content
20         self.instance = course_instance
21         self.user = user
22         super().__init__(course_instance, user)
23 
24     def _needs_generation(self, data):
25         return data is None or data['created'] < self.content.created()
26 
27     def _generate_data(self, instance, user, data=None):
28         data = deepcopy(self.content.data)
29         module_index = data['module_index']
30         exercise_index = data['exercise_index']
31         modules = data['modules']
32         categories = data['categories']
33         total = data['total']
34 
35         # Augment submission parameters.
36         def r_augment(children):
37             for entry in children:
38                 if entry['submittable']:
39                     entry.update({
40                         'submission_count': 0,
41                         'submissions': [],
42                         'best_submission': None,
43                         'points': 0,
44                         'passed': entry['points_to_pass'] == 0,
45                         'graded': False,
46                         'unofficial': False, # TODO: this should be True, but we need to ensure nothing breaks when it's changed
47                     })
48                 r_augment(entry.get('children'))
49         for module in modules:
50             module.update({
51                 'submission_count': 0,
52                 'points': 0,
53                 'points_by_difficulty': {},
54                 'unconfirmed_points_by_difficulty': {},
55                 'passed': module['points_to_pass'] == 0,
56             })
57             r_augment(module['children'])
58         for entry in categories.values():
59             entry.update({
60                 'submission_count': 0,
61                 'points': 0,
62                 'points_by_difficulty': {},
63                 'unconfirmed_points_by_difficulty': {},
64                 'passed': entry['points_to_pass'] == 0,
65             })
66         total.update({
67             'submission_count': 0,
68             'points': 0,
69             'points_by_difficulty': {},
70             'unconfirmed_points_by_difficulty': {},
71         })
72 
73         # Augment submission data.
74         if user.is_authenticated:
75             submissions = (
76                 user.userprofile.submissions.exclude_errors()
77                 .filter(exercise__course_module__course_instance=instance)
78                 .prefetch_related('exercise')
79                 .only('id', 'exercise', 'submission_time', 'status', 'grade')
80             )
81             for submission in submissions:
82                 try:
83                     tree = self._by_idx(modules, exercise_index[submission.exercise.id])
84                 except KeyError:
85                     self.dirty = True
86                     continue
87                 entry = tree[-1]
88                 ready = submission.status == Submission.STATUS.READY
89                 unofficial = submission.status == Submission.STATUS.UNOFFICIAL
90                 if ready:
91                     entry['submission_count'] += 1
92                 entry['submissions'].append({
93                     'id': submission.id,
94                     'max_points': entry['max_points'],
95                     'points_to_pass': entry['points_to_pass'],
96                     'confirm_the_level': entry.get('confirm_the_level', False),
97                     'submission_count': 1, # to fool points badge
98                     'points': submission.grade,
99                     'graded': submission.is_graded, # TODO: should this be official (is_graded = ready or unofficial)
100                     'passed': submission.grade >= entry['points_to_pass'],
101                     'submission_status': submission.status if not submission.is_graded else False,
102                     'unofficial': unofficial,
103                     'date': submission.submission_time,
104                     'url': submission.get_url('submission-plain'),
105                 })
106                 # TODO: implement way to select algorithm for the best
107                 is_better_than = has_more_points
108                 # Update best submission if one of these is true
109                 # 1) current submission in ready (thus is not unofficial) AND
110                 #    a) current best is an unofficial OR
111                 #    b) current submission has better grade
112                 # 2) All of:
113                 #    - current submission is unofficial AND
114                 #    - current best is unofficial
115                 #    - current submission has better grade
116                 if (
117                     ready and (
118                         entry['unofficial'] or
119                         is_better_than(submission, entry)
120                     )
121                 ) or (
122                     unofficial and
123                     not entry['graded'] and # NOTE: == entry['unofficial'], but before any submissions entry['unofficial'] is False
124                     is_better_than(submission, entry)
125                 ):
126                     entry.update({
127                         'best_submission': submission.id,
128                         'points': submission.grade,
129                         'passed': ready and submission.grade >= entry['points_to_pass'],
130                         'graded': ready, # != unofficial
131                         'unofficial': unofficial,
132                     })
133                 if submission.notifications.count() > 0:
134                     entry['notified'] = True
135                     if submission.notifications.filter(seen=False).count() > 0:
136                         entry['unseen'] = True
137 
138         # Confirm points.
139         def r_check(parent, children):
140             for entry in children:
141                 if (
142                     entry['submittable']
143                     and entry['confirm_the_level']
144                     and entry['passed']
145                 ):
146                     parent.pop('unconfirmed', None)
147                     for child in parent.get('children', []):
148                         child.pop('unconfirmed', None)
149                         # TODO: should recurse to all descendants
150                 r_check(entry, entry.get('children', []))
151         for module in modules:
152             r_check(module, module['children'])
153 
154         # Collect points and check limits.
155         def add_to(target, entry):
156             target['submission_count'] += entry['submission_count']
157             # NOTE: entry can be only ready or unofficial (exercise level
158             # points are only copied, only if submission is in ready or
159             # unofficial state)
160             if entry.get('unofficial', False):
161                 pass
162             # thus, all points are now ready..
163             elif entry.get('unconfirmed', False):
164                 self._add_by_difficulty(
165                     target['unconfirmed_points_by_difficulty'],
166                     entry['difficulty'],
167                     entry['points']
168                 )
169             # and finally, only remaining points are official (not unofficial & not unconfirmed)
170             else:
171                 target['points'] += entry['points']
172                 self._add_by_difficulty(
173                     target['points_by_difficulty'],
174                     entry['difficulty'],
175                     entry['points']
176                 )
177         def r_collect(module, parent, children):
178             passed = True
179             max_points = 0
180             submissions = 0
181             points = 0
182             confirm_entry = None
183             for entry in children:
184                 if entry['submittable']:
185                     if entry['confirm_the_level']:
186                         confirm_entry = entry
187                     else:
188                         passed = passed and entry['passed']
189                         max_points += entry['max_points']
190                         submissions += entry['submission_count']
191                         if entry['graded']:
192                             points += entry['points']
193                             add_to(module, entry)
194                             add_to(categories[entry['category_id']], entry)
195                             add_to(total, entry)
196                 passed = (
197                     r_collect(module, entry, entry.get('children', []))
198                     and passed
199                 )
200             if confirm_entry and submissions > 0:
201                 confirm_entry['confirmable_points'] = True
202             if parent and not parent['submittable']:
203                 parent['max_points'] = max_points
204                 parent['submission_count'] = submissions
205                 parent['points'] = points
206             return passed
207         for module in modules:
208             passed = r_collect(module, None, module['children'])
209             module['passed'] = (
210                 passed
211                 and module['points'] >= module['points_to_pass']
212             )
213         for category in categories.values():
214             category['passed'] = (
215                 category['points'] >= category['points_to_pass']
216             )
217 
218         data['points_created'] = timezone.now()
219         return data
220 
221     def created(self):
222         return self.data['points_created'], super().created()
223 
224     def submission_ids(self, number=None, category_id=None, module_id=None,
225                        exercise_id=None, filter_for_assistant=False, best=True):
226         exercises = self.search_exercises(
227             number=number,
228             category_id=category_id,
229             module_id=module_id,
230             exercise_id=exercise_id,
231             filter_for_assistant=filter_for_assistant,
232         )
233         submissions = []
234         if best:
235             for entry in exercises:
236                 sid = entry.get('best_submission', None)
237                 if not sid is None:
238                     submissions.append(sid)
239         else:
240             for entry in exercises:
241                 submissions.extend(s['id'] for s in entry.get('submissions', []))
242         return submissions
243 
244 
245 def invalidate_content(sender, instance, **kwargs):
246     course = instance.exercise.course_instance
247     for profile in instance.submitters.all():
248         CachedPoints.invalidate(course, profile.user)
249 
250 def invalidate_content_m2m(sender, instance, action, reverse, model, pk_set, **kwargs):
251     # many-to-many field Submission.submitters may be modified without
252     # triggering the Submission post save hook
253     if action not in ('post_add', 'pre_remove'):
254         return
255     if reverse:
256         # instance is a UserProfile
257         if model == Submission:
258             seen_courses = set()
259             for submission_pk in pk_set:
260                 try:
261                     submission = Submission.objects.get(pk=submission_pk)
262                     course_instance = submission.exercise.course_instance
263                     if course_instance.pk not in seen_courses:
264                         CachedPoints.invalidate(course_instance, instance.user)
265                     else:
266                         seen_courses.add(course_instance.pk)
267                 except Submission.DoesNotExist:
268                     pass
269     else:
270         # instance is a Submission
271         invalidate_content(Submission, instance)
272 
273 def invalidate_notification(sender, instance, **kwargs):
274     course = instance.course_instance
275     if not course and instance.submission:
276         course = instance.submission.exercise.course_instance
277     CachedPoints.invalidate(course, instance.recipient.user)
278 
279 
280 # Automatically invalidate cached points when submissions change.
281 post_save.connect(invalidate_content, sender=Submission)
282 post_delete.connect(invalidate_content, sender=Submission)
283 post_save.connect(invalidate_notification, sender=Notification)
284 post_delete.connect(invalidate_notification, sender=Notification)
285 # listen to the m2m_changed signal since submission.submitters is a many-to-many
286 # field and instances must be saved before the many-to-many fields may be modified,
287 # that is to say, the submission post save hook may see an empty submitters list
288 m2m_changed.connect(invalidate_content_m2m, sender=Submission.submitters.through)
289
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/exercise/cache/points.py b/exercise/cache/points.py
--- a/exercise/cache/points.py
+++ b/exercise/cache/points.py
@@ -87,7 +87,7 @@
                 entry = tree[-1]
                 ready = submission.status == Submission.STATUS.READY
                 unofficial = submission.status == Submission.STATUS.UNOFFICIAL
-                if ready:
+                if ready or submission.status in (Submission.STATUS.WAITING, Submission.STATUS.INITIALIZED):
                     entry['submission_count'] += 1
                 entry['submissions'].append({
                     'id': submission.id,

{"golden_diff": "diff --git a/exercise/cache/points.py b/exercise/cache/points.py\n--- a/exercise/cache/points.py\n+++ b/exercise/cache/points.py\n@@ -87,7 +87,7 @@\n                 entry = tree[-1]\n                 ready = submission.status == Submission.STATUS.READY\n                 unofficial = submission.status == Submission.STATUS.UNOFFICIAL\n-                if ready:\n+                if ready or submission.status in (Submission.STATUS.WAITING, Submission.STATUS.INITIALIZED):\n                     entry['submission_count'] += 1\n                 entry['submissions'].append({\n                     'id': submission.id,\n", "issue": "Different submission count between course \"exercise results\" page and assignment submission view\nI spotted this in Aalto's production A+ (I'm a student enrolled on O1).\r\n\r\nWhen I view the \"exercise results\" page, for one assignment I see the following (note the \"0/10\" submissions):\r\n\r\n![Screenshot 2019-11-30 at 19 29 42](https://user-images.githubusercontent.com/6674046/69904228-cba34900-13ac-11ea-8b03-1c05dddb85e4.png)\r\n\r\nWhen I follow the link to the chapter, and the view the assignment, it states I've used one submission (which is currently ungraded).\r\n \r\n![Screenshot 2019-11-30 at 19 29 55](https://user-images.githubusercontent.com/6674046/69904238-e7a6ea80-13ac-11ea-8e30-17e91ea93eff.png)\r\n\r\nIt seems to only count graded submissions (this particular assignment is human marked).\r\n\r\nThis may be the intended behaviour, but it felt wrong that they were not consistent and it caused me a brief moment of intense \"did I forget to submit the big assignment before the deadline\" panic.\n", "before_files": [{"content": "from copy import deepcopy\nfrom django.db.models.signals import post_save, post_delete, m2m_changed\nfrom django.utils import timezone\n\nfrom lib.cache import CachedAbstract\nfrom notification.models import Notification\nfrom ..models import LearningObject, Submission\nfrom .hierarchy import ContentMixin\n\n\ndef has_more_points(submission, current_entry):\n    return submission.grade >= current_entry['points']\n\n\nclass CachedPoints(ContentMixin, CachedAbstract):\n    KEY_PREFIX = 'points'\n\n    def __init__(self, course_instance, user, content):\n        self.content = content\n        self.instance = course_instance\n        self.user = user\n        super().__init__(course_instance, user)\n\n    def _needs_generation(self, data):\n        return data is None or data['created'] < self.content.created()\n\n    def _generate_data(self, instance, user, data=None):\n        data = deepcopy(self.content.data)\n        module_index = data['module_index']\n        exercise_index = data['exercise_index']\n        modules = data['modules']\n        categories = data['categories']\n        total = data['total']\n\n        # Augment submission parameters.\n        def r_augment(children):\n            for entry in children:\n                if entry['submittable']:\n                    entry.update({\n                        'submission_count': 0,\n                        'submissions': [],\n                        'best_submission': None,\n                        'points': 0,\n                        'passed': entry['points_to_pass'] == 0,\n                        'graded': False,\n                        'unofficial': False, # TODO: this should be True, but we need to ensure nothing breaks when it's changed\n                    })\n                r_augment(entry.get('children'))\n        for module in modules:\n            module.update({\n                'submission_count': 0,\n                'points': 0,\n                'points_by_difficulty': {},\n                'unconfirmed_points_by_difficulty': {},\n                'passed': module['points_to_pass'] == 0,\n            })\n            r_augment(module['children'])\n        for entry in categories.values():\n            entry.update({\n                'submission_count': 0,\n                'points': 0,\n                'points_by_difficulty': {},\n                'unconfirmed_points_by_difficulty': {},\n                'passed': entry['points_to_pass'] == 0,\n            })\n        total.update({\n            'submission_count': 0,\n            'points': 0,\n            'points_by_difficulty': {},\n            'unconfirmed_points_by_difficulty': {},\n        })\n\n        # Augment submission data.\n        if user.is_authenticated:\n            submissions = (\n                user.userprofile.submissions.exclude_errors()\n                .filter(exercise__course_module__course_instance=instance)\n                .prefetch_related('exercise')\n                .only('id', 'exercise', 'submission_time', 'status', 'grade')\n            )\n            for submission in submissions:\n                try:\n                    tree = self._by_idx(modules, exercise_index[submission.exercise.id])\n                except KeyError:\n                    self.dirty = True\n                    continue\n                entry = tree[-1]\n                ready = submission.status == Submission.STATUS.READY\n                unofficial = submission.status == Submission.STATUS.UNOFFICIAL\n                if ready:\n                    entry['submission_count'] += 1\n                entry['submissions'].append({\n                    'id': submission.id,\n                    'max_points': entry['max_points'],\n                    'points_to_pass': entry['points_to_pass'],\n                    'confirm_the_level': entry.get('confirm_the_level', False),\n                    'submission_count': 1, # to fool points badge\n                    'points': submission.grade,\n                    'graded': submission.is_graded, # TODO: should this be official (is_graded = ready or unofficial)\n                    'passed': submission.grade >= entry['points_to_pass'],\n                    'submission_status': submission.status if not submission.is_graded else False,\n                    'unofficial': unofficial,\n                    'date': submission.submission_time,\n                    'url': submission.get_url('submission-plain'),\n                })\n                # TODO: implement way to select algorithm for the best\n                is_better_than = has_more_points\n                # Update best submission if one of these is true\n                # 1) current submission in ready (thus is not unofficial) AND\n                #    a) current best is an unofficial OR\n                #    b) current submission has better grade\n                # 2) All of:\n                #    - current submission is unofficial AND\n                #    - current best is unofficial\n                #    - current submission has better grade\n                if (\n                    ready and (\n                        entry['unofficial'] or\n                        is_better_than(submission, entry)\n                    )\n                ) or (\n                    unofficial and\n                    not entry['graded'] and # NOTE: == entry['unofficial'], but before any submissions entry['unofficial'] is False\n                    is_better_than(submission, entry)\n                ):\n                    entry.update({\n                        'best_submission': submission.id,\n                        'points': submission.grade,\n                        'passed': ready and submission.grade >= entry['points_to_pass'],\n                        'graded': ready, # != unofficial\n                        'unofficial': unofficial,\n                    })\n                if submission.notifications.count() > 0:\n                    entry['notified'] = True\n                    if submission.notifications.filter(seen=False).count() > 0:\n                        entry['unseen'] = True\n\n        # Confirm points.\n        def r_check(parent, children):\n            for entry in children:\n                if (\n                    entry['submittable']\n                    and entry['confirm_the_level']\n                    and entry['passed']\n                ):\n                    parent.pop('unconfirmed', None)\n                    for child in parent.get('children', []):\n                        child.pop('unconfirmed', None)\n                        # TODO: should recurse to all descendants\n                r_check(entry, entry.get('children', []))\n        for module in modules:\n            r_check(module, module['children'])\n\n        # Collect points and check limits.\n        def add_to(target, entry):\n            target['submission_count'] += entry['submission_count']\n            # NOTE: entry can be only ready or unofficial (exercise level\n            # points are only copied, only if submission is in ready or\n            # unofficial state)\n            if entry.get('unofficial', False):\n                pass\n            # thus, all points are now ready..\n            elif entry.get('unconfirmed', False):\n                self._add_by_difficulty(\n                    target['unconfirmed_points_by_difficulty'],\n                    entry['difficulty'],\n                    entry['points']\n                )\n            # and finally, only remaining points are official (not unofficial & not unconfirmed)\n            else:\n                target['points'] += entry['points']\n                self._add_by_difficulty(\n                    target['points_by_difficulty'],\n                    entry['difficulty'],\n                    entry['points']\n                )\n        def r_collect(module, parent, children):\n            passed = True\n            max_points = 0\n            submissions = 0\n            points = 0\n            confirm_entry = None\n            for entry in children:\n                if entry['submittable']:\n                    if entry['confirm_the_level']:\n                        confirm_entry = entry\n                    else:\n                        passed = passed and entry['passed']\n                        max_points += entry['max_points']\n                        submissions += entry['submission_count']\n                        if entry['graded']:\n                            points += entry['points']\n                            add_to(module, entry)\n                            add_to(categories[entry['category_id']], entry)\n                            add_to(total, entry)\n                passed = (\n                    r_collect(module, entry, entry.get('children', []))\n                    and passed\n                )\n            if confirm_entry and submissions > 0:\n                confirm_entry['confirmable_points'] = True\n            if parent and not parent['submittable']:\n                parent['max_points'] = max_points\n                parent['submission_count'] = submissions\n                parent['points'] = points\n            return passed\n        for module in modules:\n            passed = r_collect(module, None, module['children'])\n            module['passed'] = (\n                passed\n                and module['points'] >= module['points_to_pass']\n            )\n        for category in categories.values():\n            category['passed'] = (\n                category['points'] >= category['points_to_pass']\n            )\n\n        data['points_created'] = timezone.now()\n        return data\n\n    def created(self):\n        return self.data['points_created'], super().created()\n\n    def submission_ids(self, number=None, category_id=None, module_id=None,\n                       exercise_id=None, filter_for_assistant=False, best=True):\n        exercises = self.search_exercises(\n            number=number,\n            category_id=category_id,\n            module_id=module_id,\n            exercise_id=exercise_id,\n            filter_for_assistant=filter_for_assistant,\n        )\n        submissions = []\n        if best:\n            for entry in exercises:\n                sid = entry.get('best_submission', None)\n                if not sid is None:\n                    submissions.append(sid)\n        else:\n            for entry in exercises:\n                submissions.extend(s['id'] for s in entry.get('submissions', []))\n        return submissions\n\n\ndef invalidate_content(sender, instance, **kwargs):\n    course = instance.exercise.course_instance\n    for profile in instance.submitters.all():\n        CachedPoints.invalidate(course, profile.user)\n\ndef invalidate_content_m2m(sender, instance, action, reverse, model, pk_set, **kwargs):\n    # many-to-many field Submission.submitters may be modified without\n    # triggering the Submission post save hook\n    if action not in ('post_add', 'pre_remove'):\n        return\n    if reverse:\n        # instance is a UserProfile\n        if model == Submission:\n            seen_courses = set()\n            for submission_pk in pk_set:\n                try:\n                    submission = Submission.objects.get(pk=submission_pk)\n                    course_instance = submission.exercise.course_instance\n                    if course_instance.pk not in seen_courses:\n                        CachedPoints.invalidate(course_instance, instance.user)\n                    else:\n                        seen_courses.add(course_instance.pk)\n                except Submission.DoesNotExist:\n                    pass\n    else:\n        # instance is a Submission\n        invalidate_content(Submission, instance)\n\ndef invalidate_notification(sender, instance, **kwargs):\n    course = instance.course_instance\n    if not course and instance.submission:\n        course = instance.submission.exercise.course_instance\n    CachedPoints.invalidate(course, instance.recipient.user)\n\n\n# Automatically invalidate cached points when submissions change.\npost_save.connect(invalidate_content, sender=Submission)\npost_delete.connect(invalidate_content, sender=Submission)\npost_save.connect(invalidate_notification, sender=Notification)\npost_delete.connect(invalidate_notification, sender=Notification)\n# listen to the m2m_changed signal since submission.submitters is a many-to-many\n# field and instances must be saved before the many-to-many fields may be modified,\n# that is to say, the submission post save hook may see an empty submitters list\nm2m_changed.connect(invalidate_content_m2m, sender=Submission.submitters.through)\n", "path": "exercise/cache/points.py"}], "after_files": [{"content": "from copy import deepcopy\nfrom django.db.models.signals import post_save, post_delete, m2m_changed\nfrom django.utils import timezone\n\nfrom lib.cache import CachedAbstract\nfrom notification.models import Notification\nfrom ..models import LearningObject, Submission\nfrom .hierarchy import ContentMixin\n\n\ndef has_more_points(submission, current_entry):\n    return submission.grade >= current_entry['points']\n\n\nclass CachedPoints(ContentMixin, CachedAbstract):\n    KEY_PREFIX = 'points'\n\n    def __init__(self, course_instance, user, content):\n        self.content = content\n        self.instance = course_instance\n        self.user = user\n        super().__init__(course_instance, user)\n\n    def _needs_generation(self, data):\n        return data is None or data['created'] < self.content.created()\n\n    def _generate_data(self, instance, user, data=None):\n        data = deepcopy(self.content.data)\n        module_index = data['module_index']\n        exercise_index = data['exercise_index']\n        modules = data['modules']\n        categories = data['categories']\n        total = data['total']\n\n        # Augment submission parameters.\n        def r_augment(children):\n            for entry in children:\n                if entry['submittable']:\n                    entry.update({\n                        'submission_count': 0,\n                        'submissions': [],\n                        'best_submission': None,\n                        'points': 0,\n                        'passed': entry['points_to_pass'] == 0,\n                        'graded': False,\n                        'unofficial': False, # TODO: this should be True, but we need to ensure nothing breaks when it's changed\n                    })\n                r_augment(entry.get('children'))\n        for module in modules:\n            module.update({\n                'submission_count': 0,\n                'points': 0,\n                'points_by_difficulty': {},\n                'unconfirmed_points_by_difficulty': {},\n                'passed': module['points_to_pass'] == 0,\n            })\n            r_augment(module['children'])\n        for entry in categories.values():\n            entry.update({\n                'submission_count': 0,\n                'points': 0,\n                'points_by_difficulty': {},\n                'unconfirmed_points_by_difficulty': {},\n                'passed': entry['points_to_pass'] == 0,\n            })\n        total.update({\n            'submission_count': 0,\n            'points': 0,\n            'points_by_difficulty': {},\n            'unconfirmed_points_by_difficulty': {},\n        })\n\n        # Augment submission data.\n        if user.is_authenticated:\n            submissions = (\n                user.userprofile.submissions.exclude_errors()\n                .filter(exercise__course_module__course_instance=instance)\n                .prefetch_related('exercise')\n                .only('id', 'exercise', 'submission_time', 'status', 'grade')\n            )\n            for submission in submissions:\n                try:\n                    tree = self._by_idx(modules, exercise_index[submission.exercise.id])\n                except KeyError:\n                    self.dirty = True\n                    continue\n                entry = tree[-1]\n                ready = submission.status == Submission.STATUS.READY\n                unofficial = submission.status == Submission.STATUS.UNOFFICIAL\n                if ready or submission.status in (Submission.STATUS.WAITING, Submission.STATUS.INITIALIZED):\n                    entry['submission_count'] += 1\n                entry['submissions'].append({\n                    'id': submission.id,\n                    'max_points': entry['max_points'],\n                    'points_to_pass': entry['points_to_pass'],\n                    'confirm_the_level': entry.get('confirm_the_level', False),\n                    'submission_count': 1, # to fool points badge\n                    'points': submission.grade,\n                    'graded': submission.is_graded, # TODO: should this be official (is_graded = ready or unofficial)\n                    'passed': submission.grade >= entry['points_to_pass'],\n                    'submission_status': submission.status if not submission.is_graded else False,\n                    'unofficial': unofficial,\n                    'date': submission.submission_time,\n                    'url': submission.get_url('submission-plain'),\n                })\n                # TODO: implement way to select algorithm for the best\n                is_better_than = has_more_points\n                # Update best submission if one of these is true\n                # 1) current submission in ready (thus is not unofficial) AND\n                #    a) current best is an unofficial OR\n                #    b) current submission has better grade\n                # 2) All of:\n                #    - current submission is unofficial AND\n                #    - current best is unofficial\n                #    - current submission has better grade\n                if (\n                    ready and (\n                        entry['unofficial'] or\n                        is_better_than(submission, entry)\n                    )\n                ) or (\n                    unofficial and\n                    not entry['graded'] and # NOTE: == entry['unofficial'], but before any submissions entry['unofficial'] is False\n                    is_better_than(submission, entry)\n                ):\n                    entry.update({\n                        'best_submission': submission.id,\n                        'points': submission.grade,\n                        'passed': ready and submission.grade >= entry['points_to_pass'],\n                        'graded': ready, # != unofficial\n                        'unofficial': unofficial,\n                    })\n                if submission.notifications.count() > 0:\n                    entry['notified'] = True\n                    if submission.notifications.filter(seen=False).count() > 0:\n                        entry['unseen'] = True\n\n        # Confirm points.\n        def r_check(parent, children):\n            for entry in children:\n                if (\n                    entry['submittable']\n                    and entry['confirm_the_level']\n                    and entry['passed']\n                ):\n                    parent.pop('unconfirmed', None)\n                    for child in parent.get('children', []):\n                        child.pop('unconfirmed', None)\n                        # TODO: should recurse to all descendants\n                r_check(entry, entry.get('children', []))\n        for module in modules:\n            r_check(module, module['children'])\n\n        # Collect points and check limits.\n        def add_to(target, entry):\n            target['submission_count'] += entry['submission_count']\n            # NOTE: entry can be only ready or unofficial (exercise level\n            # points are only copied, only if submission is in ready or\n            # unofficial state)\n            if entry.get('unofficial', False):\n                pass\n            # thus, all points are now ready..\n            elif entry.get('unconfirmed', False):\n                self._add_by_difficulty(\n                    target['unconfirmed_points_by_difficulty'],\n                    entry['difficulty'],\n                    entry['points']\n                )\n            # and finally, only remaining points are official (not unofficial & not unconfirmed)\n            else:\n                target['points'] += entry['points']\n                self._add_by_difficulty(\n                    target['points_by_difficulty'],\n                    entry['difficulty'],\n                    entry['points']\n                )\n        def r_collect(module, parent, children):\n            passed = True\n            max_points = 0\n            submissions = 0\n            points = 0\n            confirm_entry = None\n            for entry in children:\n                if entry['submittable']:\n                    if entry['confirm_the_level']:\n                        confirm_entry = entry\n                    else:\n                        passed = passed and entry['passed']\n                        max_points += entry['max_points']\n                        submissions += entry['submission_count']\n                        if entry['graded']:\n                            points += entry['points']\n                            add_to(module, entry)\n                            add_to(categories[entry['category_id']], entry)\n                            add_to(total, entry)\n                passed = (\n                    r_collect(module, entry, entry.get('children', []))\n                    and passed\n                )\n            if confirm_entry and submissions > 0:\n                confirm_entry['confirmable_points'] = True\n            if parent and not parent['submittable']:\n                parent['max_points'] = max_points\n                parent['submission_count'] = submissions\n                parent['points'] = points\n            return passed\n        for module in modules:\n            passed = r_collect(module, None, module['children'])\n            module['passed'] = (\n                passed\n                and module['points'] >= module['points_to_pass']\n            )\n        for category in categories.values():\n            category['passed'] = (\n                category['points'] >= category['points_to_pass']\n            )\n\n        data['points_created'] = timezone.now()\n        return data\n\n    def created(self):\n        return self.data['points_created'], super().created()\n\n    def submission_ids(self, number=None, category_id=None, module_id=None,\n                       exercise_id=None, filter_for_assistant=False, best=True):\n        exercises = self.search_exercises(\n            number=number,\n            category_id=category_id,\n            module_id=module_id,\n            exercise_id=exercise_id,\n            filter_for_assistant=filter_for_assistant,\n        )\n        submissions = []\n        if best:\n            for entry in exercises:\n                sid = entry.get('best_submission', None)\n                if not sid is None:\n                    submissions.append(sid)\n        else:\n            for entry in exercises:\n                submissions.extend(s['id'] for s in entry.get('submissions', []))\n        return submissions\n\n\ndef invalidate_content(sender, instance, **kwargs):\n    course = instance.exercise.course_instance\n    for profile in instance.submitters.all():\n        CachedPoints.invalidate(course, profile.user)\n\ndef invalidate_content_m2m(sender, instance, action, reverse, model, pk_set, **kwargs):\n    # many-to-many field Submission.submitters may be modified without\n    # triggering the Submission post save hook\n    if action not in ('post_add', 'pre_remove'):\n        return\n    if reverse:\n        # instance is a UserProfile\n        if model == Submission:\n            seen_courses = set()\n            for submission_pk in pk_set:\n                try:\n                    submission = Submission.objects.get(pk=submission_pk)\n                    course_instance = submission.exercise.course_instance\n                    if course_instance.pk not in seen_courses:\n                        CachedPoints.invalidate(course_instance, instance.user)\n                    else:\n                        seen_courses.add(course_instance.pk)\n                except Submission.DoesNotExist:\n                    pass\n    else:\n        # instance is a Submission\n        invalidate_content(Submission, instance)\n\ndef invalidate_notification(sender, instance, **kwargs):\n    course = instance.course_instance\n    if not course and instance.submission:\n        course = instance.submission.exercise.course_instance\n    CachedPoints.invalidate(course, instance.recipient.user)\n\n\n# Automatically invalidate cached points when submissions change.\npost_save.connect(invalidate_content, sender=Submission)\npost_delete.connect(invalidate_content, sender=Submission)\npost_save.connect(invalidate_notification, sender=Notification)\npost_delete.connect(invalidate_notification, sender=Notification)\n# listen to the m2m_changed signal since submission.submitters is a many-to-many\n# field and instances must be saved before the many-to-many fields may be modified,\n# that is to say, the submission post save hook may see an empty submitters list\nm2m_changed.connect(invalidate_content_m2m, sender=Submission.submitters.through)\n", "path": "exercise/cache/points.py"}]}