Repository: dotkom/onlineweb4

Issue:
Attendance API tries to query attendees by RFID value "None"
Sometimes the regme application will (probably?) send the "rfid" payload as null which queries the database for `event=event_id` and `user__rfid=None`. This might result in a "MultipleObjectsReturned" since there may be more than one attended user which haven't set up their RFID.

Can be reproduced;
* Create attendance event
* Attend at least two people who does not have set their RFID
* Try to verify attendance  of the users by POSTing `rfid` payload as null

---

https://sentry.io/dotkom/onlineweb-4/issues/353044724/

```
MultipleObjectsReturned: get() returned more than one Attendee -- it returned 2!
(7 additional frame(s) were not displayed)
...
  File "rest_framework/views.py", line 483, in dispatch
    response = self.handle_exception(exc)
  File "rest_framework/views.py", line 443, in handle_exception
    self.raise_uncaught_exception(exc)
  File "rest_framework/views.py", line 480, in dispatch
    response = handler(request, *args, **kwargs)
  File "apps/events/views.py", line 364, in post
    attendee = Attendee.objects.get(event=event, user__rfid=rfid)

MultipleObjectsReturned: get() returned more than one Attendee -- it returned 2!
```


Discussion:

Resolving patch:
diff --git a/apps/events/views.py b/apps/events/views.py
--- a/apps/events/views.py
+++ b/apps/events/views.py
@@ -395,12 +395,13 @@ class AttendViewSet(views.APIView):
     permission_classes = [TokenHasScope]
     required_scopes = ['regme.readwrite']
 
-    def post(self, request, format=None):
-
-        rfid = request.data.get('rfid')
-        event = request.data.get('event')
-        username = request.data.get('username')
-        waitlist_approved = request.data.get('approved')
+    @staticmethod
+    def _validate_attend_params(rfid, username):
+        if not (username or rfid):
+            return {
+                'message': 'Mangler både RFID og brukernavn. Vennligst prøv igjen.',
+                'attend_status': 41,
+            }
 
         # If attendee has typed in username to bind a new card to their user
         if username is not None and rfid is not None:
@@ -409,12 +410,30 @@ def post(self, request, format=None):
                 user.rfid = rfid
                 user.save()
             except User.DoesNotExist:
-                return Response({'message': 'Brukernavnet finnes ikke. Husk at det er et online.ntnu.no brukernavn! '
-                                            '(Prøv igjen, eller scan nytt kort for å avbryte.)', 'attend_status': 50},
-                                status=status.HTTP_400_BAD_REQUEST)
+                return {
+                    'message': 'Brukernavnet finnes ikke. Husk at det er et online.ntnu.no brukernavn! '
+                               '(Prøv igjen, eller scan nytt kort for å avbryte.)',
+                    'attend_status': 50,
+                }
+
+        return {}
+
+    def post(self, request, format=None):
+
+        rfid = request.data.get('rfid')
+        event = request.data.get('event')
+        username = request.data.get('username')
+        waitlist_approved = request.data.get('approved')
+
+        error = self._validate_attend_params(rfid, username)
+        if 'message' in error and 'attend_status' in error:
+            return Response({'message': error.get('message'), 'attend_status': error.get('attend_status')},
+                            status=status.HTTP_400_BAD_REQUEST
+                            )
+
         try:
             # If attendee is trying to attend by username
-            if rfid is None:
+            if not rfid:
                 attendee = Attendee.objects.get(event=event, user__username=username)
             else:
                 attendee = Attendee.objects.get(event=event, user__rfid=rfid)


Test patch:
diff --git a/apps/events/tests/api_tests.py b/apps/events/tests/api_tests.py
--- a/apps/events/tests/api_tests.py
+++ b/apps/events/tests/api_tests.py
@@ -204,3 +204,37 @@ def test_user_is_on_waitlist_approved(self):
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()['attend_status'], 10)
+
+    def test_api_does_not_try_to_get_user_by_rfid_empty_string(self):
+        self.event.attendance_event.max_capacity = 2
+        self.event.attendance_event.save()
+
+        self.attendee1.user.rfid = ''
+        self.attendee2.user.rfid = ''
+        self.attendee1.user.save()
+        self.attendee2.user.save()
+
+        response = self.client.post(self.url, {
+            'event': self.event.id,
+            'rfid': '',
+        }, **self.headers)
+
+        self.refresh_attendees()
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertFalse(self.attendees[0].attended)
+        self.assertFalse(self.attendees[1].attended)
+        self.assertEqual(response.json()['attend_status'], 41)
+
+    def test_save_rfid_give_no_username_gives_useful_error_message(self):
+        self.attendee1.user.rfid = None
+        self.attendee1.user.save()
+
+        response = self.client.post(self.url, {
+            'event': self.event.id,
+            'rfid': self.attendee1.user.rfid,
+        }, **self.headers)
+
+        self.refresh_attendees()
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertFalse(self.attendees[0].attended)
+        self.assertEqual(response.json()['attend_status'], 41)