Repository: learningequality/kolibri

Issue:
 Invalid prop: type check failed for prop "completionTimestamp"
### Observed behavior

When viewing an exam report in Coach at this URL:

```
http://localhost:8000/coach/#/eba8f3c694d0d9cb3a9170c81f7e076d/fb51dae6df7545af8455aa3a0c32048d/exams/1d271e1a668d735b254de7a52920f01c/users/d98b14a36191d92a0e85b277a54c3d62/0/0
```

saw this error a few times;

```
Invalid prop: type check failed for prop "completionTimestamp". Expected Date, got String.
```

### Steps to reproduce

* create and activate exam
* take and submit exam without answering any questions
* deactivate exam and view report

### Context

* Kolibri version: 0.7




Discussion:
@indirectlylit I was trying to reproduce this issue but I'm getting this console error instead of the one you posted

```
[DEBUG: ../kolibri_exercise_perseus_plugin/kolibri_exercise_perseus_plugin/assets/src/views/index.vue] Error during unmounting of item renderer Error: Minified React error #40; visit http://facebook.github.io/react/docs/error-decoder.html?invariant=40 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at r (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:1)
    at Object.unmountComponentAtNode (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:62)
    at t.componentWillUnmount (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:78)
    at p.unmountComponent (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:83)
    at Object.unmountComponent (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:37)
    at p.unmountComponent (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:83)
    at Object.unmountComponent (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:37)
    at l (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:62)
    at r.perform (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:37)
    at Object.batchedUpdates (exercise_perseus_render_module-0.6.1.dev020171205010816-git.js:84)
```

Used the CK-12 Math channel
@rtibbles has been doing some recent work on this component, and may even be deprecating it

let's assign to him

Resolving patch:
diff --git a/kolibri/core/exams/api.py b/kolibri/core/exams/api.py
--- a/kolibri/core/exams/api.py
+++ b/kolibri/core/exams/api.py
@@ -29,10 +29,27 @@ class Meta:
         model = models.Exam
         fields = ['collection', ]
 
+def _ensure_raw_dict(d):
+    if hasattr(d, "dict"):
+        d = d.dict()
+    return dict(d)
+
+
+class ExamPermissions(KolibriAuthPermissions):
+    # Overrides the default validator to sanitize the Exam POST Payload
+    # before validation
+    def validator(self, request, view, datum):
+        model = view.get_serializer_class().Meta.model
+        validated_data = view.get_serializer().to_internal_value(_ensure_raw_dict(datum))
+        # Cannot have create assignments without creating the Exam first,
+        # so this doesn't try to validate the Exam with a non-empty assignments list
+        validated_data.pop('assignments')
+        return request.user.can_create(model, validated_data)
+
 class ExamViewset(viewsets.ModelViewSet):
     serializer_class = serializers.ExamSerializer
     pagination_class = OptionalPageNumberPagination
-    permission_classes = (KolibriAuthPermissions,)
+    permission_classes = (ExamPermissions,)
     filter_backends = (KolibriAuthPermissionsFilter, DjangoFilterBackend)
     filter_class = ExamFilter
 
diff --git a/kolibri/core/exams/serializers.py b/kolibri/core/exams/serializers.py
--- a/kolibri/core/exams/serializers.py
+++ b/kolibri/core/exams/serializers.py
@@ -2,13 +2,13 @@
 
 from django.db.models import Sum
 from rest_framework import serializers
-from rest_framework.validators import UniqueTogetherValidator
 
 from kolibri.auth.models import Collection
 from kolibri.auth.models import FacilityUser
 from kolibri.core.exams.models import Exam
 from kolibri.core.exams.models import ExamAssignment
 from kolibri.logger.models import ExamLog
+from rest_framework.serializers import SerializerMethodField
 
 
 class NestedCollectionSerializer(serializers.ModelSerializer):
@@ -33,13 +33,17 @@ class Meta:
 class ExamAssignmentCreationSerializer(serializers.ModelSerializer):
     assigned_by = serializers.PrimaryKeyRelatedField(read_only=False, queryset=FacilityUser.objects.all())
     collection = serializers.PrimaryKeyRelatedField(read_only=False, queryset=Collection.objects.all())
+    collection_kind = SerializerMethodField()
 
     class Meta:
         model = ExamAssignment
         fields = (
-            'id', 'exam', 'collection', 'assigned_by',
+            'id', 'exam', 'collection', 'assigned_by', 'collection_kind',
         )
-        read_only_fields = ('assigned_by',)
+        read_only_fields = ('assigned_by', 'exam', 'collection_kind')
+
+    def get_collection_kind(self, instance):
+        return instance.collection.kind
 
     def to_internal_value(self, data):
         # Make a new OrderedDict from the input, which could be an immutable QueryDict
@@ -63,7 +67,7 @@ class Meta:
 
 class ExamSerializer(serializers.ModelSerializer):
 
-    assignments = ExamAssignmentRetrieveSerializer(many=True, read_only=True)
+    assignments = ExamAssignmentCreationSerializer(many=True)
     question_sources = serializers.JSONField(default='[]')
     creator = serializers.PrimaryKeyRelatedField(read_only=False, queryset=FacilityUser.objects.all())
 
@@ -73,14 +77,6 @@ class Meta:
             'id', 'title', 'channel_id', 'question_count', 'question_sources', 'seed',
             'active', 'collection', 'archive', 'assignments', 'creator',
         )
-        read_only_fields = ('assignments',)
-
-        validators = [
-            UniqueTogetherValidator(
-                queryset=Exam.objects.all(),
-                fields=('collection', 'title')
-            )
-        ]
 
     def to_internal_value(self, data):
         # Make a new OrderedDict from the input, which could be an immutable QueryDict
@@ -93,6 +89,51 @@ def to_internal_value(self, data):
                 self.partial = True
         return super(ExamSerializer, self).to_internal_value(data)
 
+    def create(self, validated_data):
+        assignees = validated_data.pop('assignments')
+        new_exam = Exam.objects.create(**validated_data)
+        # Create all of the new ExamAssignment
+        for assignee in assignees:
+            self._create_exam_assignment(
+                exam=new_exam,
+                collection=assignee['collection']
+            )
+        return new_exam
+
+    def _create_exam_assignment(self, **params):
+        return ExamAssignment.objects.create(
+            assigned_by=self.context['request'].user,
+            **params
+        )
+
+    def update(self, instance, validated_data):
+        # Update the scalar fields
+        instance.title = validated_data.get('title', instance.title)
+        instance.active = validated_data.get('active', instance.active)
+
+        # Add/delete any new/removed Assignments
+        if 'assignments' in validated_data:
+            assignees = validated_data.pop('assignments')
+            current_assignments = (instance.assignments).all()
+            current_group_ids = [x.collection.id for x in list(current_assignments)]
+            new_group_ids = [x['collection'].id for x in list(assignees)]
+
+            ids_to_add = set(new_group_ids) - set(current_group_ids)
+            for id in ids_to_add:
+                self._create_exam_assignment(
+                    exam=instance,
+                    collection=Collection.objects.get(id=id)
+                )
+
+            ids_to_delete = set(current_group_ids) - set(new_group_ids)
+            ExamAssignment.objects.filter(
+                exam_id=instance.id,
+                collection_id__in=ids_to_delete
+            ).delete()
+
+        instance.save()
+        return instance
+
 
 class UserExamSerializer(serializers.ModelSerializer):
 


Test patch: