From 0c27894b6ff4cf1423416cf4a4a42c4dfc0f9e0f Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Thu, 6 Oct 2022 10:52:19 +0200
Subject: [PATCH 3/3] migrations: set existing user phone fields to E.164 value
 format (#69365)

---
 src/authentic2/managers.py                    |  7 +-
 .../0043_attribute_alter_model_managers.py    | 27 ++++++++
 .../migrations/0044_migrate_phone_field.py    | 57 ++++++++++++++++
 tests/test_migrations.py                      | 66 +++++++++++++++++++
 4 files changed, 155 insertions(+), 2 deletions(-)
 create mode 100644 src/authentic2/migrations/0043_attribute_alter_model_managers.py
 create mode 100644 src/authentic2/migrations/0044_migrate_phone_field.py

diff --git a/src/authentic2/managers.py b/src/authentic2/managers.py
index 3ec993a74..951f36797 100644
--- a/src/authentic2/managers.py
+++ b/src/authentic2/managers.py
@@ -109,8 +109,11 @@ class BaseServiceManager(models.Manager):
 
 
 class AttributeManager(managers.QueryManager.from_queryset(GetByNameQuerySet)):
-    pass
+    use_in_migrations = True
+
+
+class AttributeValueManager(managers.QueryManager.from_queryset(AttributeValueQuerySet)):
+    use_in_migrations = True
 
 
 ServiceManager = BaseServiceManager.from_queryset(ServiceQuerySet)
-AttributeValueManager = managers.QueryManager.from_queryset(AttributeValueQuerySet)
diff --git a/src/authentic2/migrations/0043_attribute_alter_model_managers.py b/src/authentic2/migrations/0043_attribute_alter_model_managers.py
new file mode 100644
index 000000000..41614bd9b
--- /dev/null
+++ b/src/authentic2/migrations/0043_attribute_alter_model_managers.py
@@ -0,0 +1,27 @@
+from django.db import migrations
+
+import authentic2.managers
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('authentic2', '0042_api_client'),
+    ]
+
+    operations = [
+        migrations.AlterModelManagers(
+            name='attribute',
+            managers=[
+                ('all_objects', authentic2.managers.AttributeManager()),
+                ('objects', authentic2.managers.AttributeManager(disabled=False)),
+            ],
+        ),
+        migrations.AlterModelManagers(
+            name='attributevalue',
+            managers=[
+                ('all_objects', authentic2.managers.AttributeValueManager()),
+                ('objects', authentic2.managers.AttributeValueManager(attribute__disabled=False)),
+            ],
+        ),
+    ]
diff --git a/src/authentic2/migrations/0044_migrate_phone_field.py b/src/authentic2/migrations/0044_migrate_phone_field.py
new file mode 100644
index 000000000..a42876674
--- /dev/null
+++ b/src/authentic2/migrations/0044_migrate_phone_field.py
@@ -0,0 +1,57 @@
+import phonenumbers
+from django.conf import settings
+from django.db import migrations
+
+
+def migrate_phone_field(apps, schema_editor):
+    Attribute = apps.get_model('authentic2', 'Attribute')
+    AttributeValue = apps.get_model('authentic2', 'AttributeValue')
+    User = apps.get_model(*settings.AUTH_USER_MODEL.split('.', maxsplit=1))
+    ContentType = apps.get_model('contenttypes', 'ContentType')
+
+    user_ct = ContentType.objects.get_for_model(User)
+    try:
+        phone_attr = Attribute.objects.get(name='phone')
+    except Attribute.DoesNotExist:
+        return
+
+    for atv in AttributeValue.objects.filter(attribute=phone_attr):
+        pn = None
+        try:
+            pn = phonenumbers.parse(atv.content)
+        except phonenumbers.NumberParseException:
+            try:
+                pn = phonenumbers.parse(
+                    atv.content,
+                    settings.PHONE_COUNTRY_CODES[settings.DEFAULT_COUNTRY_CODE],
+                )
+            except phonenumbers.NumberParseException:
+                continue
+
+        number_e164 = phonenumbers.format_number(pn, phonenumbers.PhoneNumberFormat.E164)
+        atv.content = number_e164
+        atv.save()
+
+        if not atv.content_type == user_ct:
+            continue
+        try:
+            user = User.objects.get(id=atv.object_id)
+        except User.DoesNotExist:
+            # stale attribute value, should not happen as it is deleted on user deletion
+            continue
+        if hasattr(user, 'phone'):
+            user.phone = number_e164
+            user.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('custom_user', '0032_auto_20220919_1230'),
+        ('authentic2', '0043_attribute_alter_model_managers'),
+    ]
+
+    operations = [
+        migrations.RunPython(migrate_phone_field, reverse_code=migrations.RunPython.noop),
+    ]
diff --git a/tests/test_migrations.py b/tests/test_migrations.py
index ed99329da..dea406e2f 100644
--- a/tests/test_migrations.py
+++ b/tests/test_migrations.py
@@ -14,6 +14,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+from django.conf import settings
 from django.contrib.auth.models import AbstractUser
 from django.utils.timezone import now
 
@@ -58,3 +59,68 @@ def test_migration_custom_user_0028_user_email_verified_date(transactional_db, m
     User = new_apps.get_model('custom_user', 'User')
     user = User.objects.get()
     assert user.email_verified_date == user.date_joined
+
+
+def test_migration_authentic2_0044_migrate_phone_field(transactional_db, migration):
+    old_apps = migration.before([('authentic2', '0043_attribute_alter_model_managers')])
+
+    Attribute = old_apps.get_model('authentic2', 'Attribute')
+    AttributeValue = old_apps.get_model('authentic2', 'AttributeValue')
+    User = old_apps.get_model(*settings.AUTH_USER_MODEL.split('.', maxsplit=1))
+    ContentType = old_apps.get_model('contenttypes', 'ContentType')
+
+    at = Attribute.objects.create(
+        name='phone',
+        label='Phone number',
+        kind='phone_number',
+    )
+
+    user = User.objects.create(
+        email='john.doe@example.com',
+        username='john.doe',
+        phone='12345',
+    )
+    user2 = User.objects.create(
+        email='billy.smith@example.com',
+        username='billy.smith',
+        phone='abcdef',  # erroneous phone number
+    )
+
+    AttributeValue.objects.create(
+        attribute=at,
+        content_type=ContentType.objects.get_for_model(User),
+        object_id=user.id,
+        content='12345',
+    )
+
+    AttributeValue.objects.create(
+        attribute=at,
+        content_type=ContentType.objects.get_for_model(User),
+        object_id=user2.id,
+        content='abcdef',
+    )
+
+    new_apps = migration.apply([('authentic2', '0044_migrate_phone_field')])
+    User = new_apps.get_model('custom_user', 'User')
+
+    assert User.objects.get(username='john.doe').phone == '+3312345'
+    assert User.objects.get(username='billy.smith').phone == 'abcdef'
+
+    Attribute = new_apps.get_model('authentic2', 'Attribute')
+    AttributeValue = new_apps.get_model('authentic2', 'AttributeValue')
+    ContentType = new_apps.get_model('contenttypes', 'ContentType')
+
+    at = Attribute.objects.get(name='phone')
+    atv = AttributeValue.objects.get(
+        attribute=at,
+        content_type=ContentType.objects.get_for_model(User),
+        object_id=user.id,
+    )
+    assert atv.content == '+3312345'
+
+    atv2 = AttributeValue.objects.get(
+        attribute=at,
+        content_type=ContentType.objects.get_for_model(User),
+        object_id=user2.id,
+    )
+    assert atv2.content == 'abcdef'
-- 
2.37.2

