Repository: carpentries/amy

Issue:
Allow null family name
Sometimes people identify themselves by only one name.
It looks like this was historically allowed as we do have people without a family name in the database, but new records require a family name.
Allow null family name
Sometimes people identify themselves by only one name.
It looks like this was historically allowed as we do have people without a family name in the database, but new records require a family name.


Discussion:
Apologies for the regression - @chrismedrela can you please add a test 
for this and then wind us back?  @maneesha how is this showing up - are 
we seeing error messages in AMY in production?

Apologies for the regression - @chrismedrela can you please add a test 
for this and then wind us back?  @maneesha how is this showing up - are 
we seeing error messages in AMY in production?


Resolving patch:
diff --git a/workshops/migrations/0127_make_family_name_optional.py b/workshops/migrations/0127_make_family_name_optional.py
new file mode 100644
--- /dev/null
+++ b/workshops/migrations/0127_make_family_name_optional.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.13 on 2017-04-27 13:38
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0126_auto_20170325_0406'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='person',
+            name='family',
+            field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Family (last) name'),
+        ),
+    ]
diff --git a/workshops/models.py b/workshops/models.py
--- a/workshops/models.py
+++ b/workshops/models.py
@@ -328,7 +328,7 @@ class Person(AbstractBaseUser, PermissionsMixin):
                                    verbose_name='Personal (first) name')
     middle      = models.CharField(max_length=STR_LONG, blank=True,
                                    verbose_name='Middle name')
-    family      = models.CharField(max_length=STR_LONG,
+    family      = models.CharField(max_length=STR_LONG, blank=True, null=True,
                                    verbose_name='Family (last) name')
     email       = models.CharField(max_length=STR_LONG, unique=True, null=True, blank=True,
                                    verbose_name='Email address')
@@ -538,7 +538,8 @@ def save(self, *args, **kwargs):
         # save empty string as NULL to the database - otherwise there are
         # issues with UNIQUE constraint failing
         self.personal = self.personal.strip()
-        self.family = self.family.strip()
+        if self.family is not None:
+            self.family = self.family.strip()
         self.middle = self.middle.strip()
         self.email = self.email.strip() if self.email else None
         self.gender = self.gender or None


Test patch:
diff --git a/workshops/test/test_person.py b/workshops/test/test_person.py
--- a/workshops/test/test_person.py
+++ b/workshops/test/test_person.py
@@ -10,6 +10,7 @@
 from django.core.urlresolvers import reverse
 from django.core.validators import ValidationError
 from django.contrib.auth.models import Permission, Group
+from webtest.forms import Upload
 
 from workshops.filters import filter_taught_workshops
 from ..forms import PersonForm, PersonsMergeForm
@@ -60,7 +61,7 @@ def test_edit_person_empty_family_name(self):
             'family': '',  # family name cannot be empty
         }
         f = PersonForm(data)
-        self.assertIn('family', f.errors)
+        self.assertNotIn('family', f.errors)
 
     def _test_edit_person_email(self, person):
         url, values = self._get_initial_form_index(0, 'person_edit', person.id)
@@ -1134,3 +1135,36 @@ def test_trainer_cannot_edit_stray_profile(self):
                 reverse('person_edit', args=[self.trainee.pk]),
                 user='trainer',
             )
+
+
+class TestRegression1076(TestBase):
+    """Family name should be optional."""
+
+    def setUp(self):
+        self._setUpUsersAndLogin()
+        self._setUpRoles()
+        self._setUpEvents()
+
+    def test_family_name_is_optional(self):
+        self.admin.family = ''
+        self.admin.save()  # no error should be raised
+        self.admin.full_clean()  # no error should be raised
+
+    def test_bulk_upload(self):
+        event_slug = Event.objects.first().slug
+        csv = (
+            'personal,family,email,event,role\n'
+            'John,,john@smith.com,{0},learner\n'
+        ).format(event_slug)
+
+        upload_page = self.app.get(reverse('person_bulk_add'), user='admin')
+        upload_form = upload_page.forms['main-form']
+        upload_form['file'] = Upload('people.csv', csv.encode('utf-8'))
+
+        confirm_page = upload_form.submit().maybe_follow()
+        confirm_form = confirm_page.forms['main-form']
+
+        info_page = confirm_form.submit('confirm').maybe_follow()
+        self.assertIn('Successfully created 1 persons and 1 tasks', info_page)
+        john_created = Person.objects.filter(personal='John', family='').exists()
+        self.assertTrue(john_created)