Repository: carpentries/amy

Issue:
Bulk add persons error
Bulk add persons is throwing a Server Error (500).

On the preview screen it looks fine but it returns an error when you click "Verify" or "Confirm and Save" 


Discussion:
@maneesha with a very short check I was unable to verify this error on current development code. Can you please send me an email with the file you're using for this bulk upload?
@shapiromatron   Just sent you an email with the file.  I can't see anything immediately wrong with it.  Othe users have reported this issue as well.
@maneesha thanks, I'll look into it tomorrow and see where the error is coming from.
@maneesha did you mean me? I haven't been active in this project for quite some time...
@maneesha the issue is that email address is required, but the "bulk upload" doesn't indicate it strong enough and doesn't correctly verify the incoming data.

For now you can circumvent the error by adding email addresses to your bulk upload data records.
I didn't realize email address was required.  I don't remember it being this way.  We don't always have email addresses on our learners (it's not required for a Person) so we can't do that.  Putting in an issue to fix this.

@shapiromatron  I have no idea how my typo managed to tag you!  Sorry about that!
@maneesha I was wrong, we have people in the database with empty email. I made a hotfix on the server, and I will prepare a solid fix for the `develop` branch (for the next release). Unfortunately the next release already contains too many changes to make a minor release with the hotfix in the meantime.

@maneesha can you please check if the issue is fixed on the server? I tested against the file you sent me and it worked fine.

Resolving patch:
diff --git a/amy/workshops/util.py b/amy/workshops/util.py
--- a/amy/workshops/util.py
+++ b/amy/workshops/util.py
@@ -152,7 +152,7 @@ def verify_upload_person_task(data, match=False):
         if match is True:
             try:
                 person = Person.objects.get(email=email)
-            except Person.DoesNotExist:
+            except (Person.DoesNotExist, Person.MultipleObjectsReturned):
                 person = None
             else:
                 info.append('Existing record for person will be used.')
@@ -171,7 +171,7 @@ def verify_upload_person_task(data, match=False):
         elif not person_id:
             try:
                 Person.objects.get(email=email)
-            except Person.DoesNotExist:
+            except (Person.DoesNotExist, Person.MultipleObjectsReturned):
                 pass
             else:
                 errors.append('Person with this email address already exists.')


Test patch:
diff --git a/amy/workshops/tests/test_bulk_add.py b/amy/workshops/tests/test_bulk_add.py
--- a/amy/workshops/tests/test_bulk_add.py
+++ b/amy/workshops/tests/test_bulk_add.py
@@ -178,6 +178,49 @@ def test_verify_email_caseinsensitive_matches(self):
             has_errors = verify_upload_person_task(bad_data, match=True)
             self.assertFalse(has_errors, 'Bad email: {}'.format(email))
 
+    def test_email_missing(self):
+        """This tests against regression in:
+        https://github.com/swcarpentry/amy/issues/1394
+
+        The issue: entries without emails (email=None) were caught in
+        `Person.objects.get(email=email)`, which returned multiple objects
+        (because we have many users with empty emails, and it doesn't violate
+        UNIQUE constraint."""
+
+        # make existing users loose their emails
+        usernames = ['potter_harry', 'granger_hermione', 'weasley_ron']
+        Person.objects.filter(username__in=usernames).update(email=None)
+
+        bad_data = [
+            {
+                'email': None,
+                'personal': 'Harry',
+                'family': 'Potter',
+                'event': 'foobar',
+                'role': 'learner'
+            },
+            {
+                'email': None,
+                'personal': 'Hermione',
+                'family': 'Granger',
+                'event': 'foobar',
+                'role': 'learner'
+            },
+            {
+                'email': None,
+                'personal': 'Ron',
+                'family': 'Weasley',
+                'event': 'foobar',
+                'role': 'learner'
+            },
+        ]
+        # test for first occurrence of the error
+        has_errors = verify_upload_person_task(bad_data, match=True)
+        self.assertFalse(has_errors)
+        # test for second occurrence of the error
+        has_errors = verify_upload_person_task(bad_data, match=False)
+        self.assertFalse(has_errors)
+
     def test_verify_existing_user_has_workshop_role_provided(self):
         bad_data = [
             {