From e4e234bf9667ba466185b7d431b4749ca4144693 Mon Sep 17 00:00:00 2001
From: Paul Marillonnet <pmarillonnet@entrouvert.com>
Date: Wed, 2 Nov 2022 14:40:14 +0100
Subject: [PATCH 1/8] add user phone field & subsequent migration (#69838)

---
 tests/test_sql.py   | 28 ++++++++++++++++++++++++++++
 tests/test_users.py | 17 +++++++++++++++++
 wcs/sql.py          | 15 ++++++++++++---
 wcs/users.py        | 18 ++++++++++++++++++
 4 files changed, 75 insertions(+), 3 deletions(-)

diff --git a/tests/test_sql.py b/tests/test_sql.py
index f80a82b6f..67bf0bfa2 100644
--- a/tests/test_sql.py
+++ b/tests/test_sql.py
@@ -2209,6 +2209,34 @@ def test_migration_59_all_forms_table(pub):
     assert len(objects) == 2
 
 
+def test_migration_68_add_user_phone_field(pub):
+    conn, cur = sql.get_connection_and_cursor()
+    cur.execute('UPDATE wcs_meta SET value = 67 WHERE key = %s', ('sql_level',))
+    conn.commit()
+    cur.close()
+
+    sql.SqlUser.wipe()
+    user = sql.SqlUser()
+    user.name = 'Jean Sénisme'
+    user.store()
+
+    user2 = sql.SqlUser()
+    user2.name = 'Jean II'
+    user2.store()
+    assert sql.SqlUser.count() == 2
+
+    conn, cur = sql.get_connection_and_cursor()
+    cur.execute('ALTER TABLE users DROP COLUMN phone')
+    assert not column_exists_in_table(cur, 'users', 'phone')
+    sql.migrate()
+    assert column_exists_in_table(cur, 'users', 'phone')
+    assert migration_level(cur) >= 68
+
+    assert sql.SqlUser.count() == 2
+    assert sql.SqlUser.get(id=user.id).phone is None
+    assert sql.SqlUser.get(id=user2.id).phone is None
+
+
 def test_logged_error_store_without_integrity_error(pub, sql_queries):
     sql.LoggedError.record('there was an error')
 
diff --git a/tests/test_users.py b/tests/test_users.py
index a5304126d..5b844d41e 100644
--- a/tests/test_users.py
+++ b/tests/test_users.py
@@ -42,6 +42,23 @@ def test_get_users_with_name_identifier(pub):
     assert pub.user_class.get_users_with_name_identifier('foo')[0].name == 'Pierre'
 
 
+def test_get_users_with_phone(pub):
+    pub.user_class.wipe()
+
+    user = pub.user_class()
+    user.name = 'Pierre'
+    user.phone = '+33123456789'
+    user.store()
+
+    user = pub.user_class()
+    user.name = 'Papier'
+    user.phone = '+33987654321'
+    user.store()
+
+    assert len(pub.user_class.get_users_with_phone('+33123456789')) == 1
+    assert pub.user_class.get_users_with_phone('+33123456789')[0].name == 'Pierre'
+
+
 def test_user_substitution_variables(pub):
     pub.user_class.wipe()
 
diff --git a/wcs/sql.py b/wcs/sql.py
index c3c37c2c8..083a77296 100644
--- a/wcs/sql.py
+++ b/wcs/sql.py
@@ -1118,6 +1118,7 @@ def do_user_table():
                                     name varchar,
                                     ascii_name varchar,
                                     email varchar,
+                                    phone varchar,
                                     roles text[],
                                     is_active bool,
                                     is_admin bool,
@@ -1142,6 +1143,7 @@ def do_user_table():
         'id',
         'name',
         'email',
+        'phone',
         'roles',
         'is_admin',
         'anonymous',
@@ -1200,6 +1202,9 @@ def do_user_table():
         cur.execute('ALTER TABLE %s ADD COLUMN is_active bool DEFAULT TRUE' % table_name)
         cur.execute('UPDATE %s SET is_active = FALSE WHERE deleted_timestamp IS NOT NULL' % table_name)
 
+    if 'phone' not in existing_fields:
+        cur.execute('ALTER TABLE %s ADD COLUMN phone varchar' % table_name)
+
     # delete obsolete fields
     for field in existing_fields - needed_fields:
         cur.execute('''ALTER TABLE %s DROP COLUMN %s''' % (table_name, field))
@@ -3138,6 +3143,7 @@ class SqlUser(SqlMixin, wcs.users.User):
         ('id', 'serial'),
         ('name', 'varchar'),
         ('email', 'varchar'),
+        ('phone', 'varchar'),
         ('roles', 'varchar[]'),
         ('is_admin', 'bool'),
         ('anonymous', 'bool'),
@@ -3165,6 +3171,7 @@ class SqlUser(SqlMixin, wcs.users.User):
             'name': self.name,
             'ascii_name': self.ascii_name,
             'email': self.email,
+            'phone': self.phone,
             'roles': self.roles,
             'is_admin': self.is_admin,
             'anonymous': self.anonymous,
@@ -3261,6 +3268,7 @@ class SqlUser(SqlMixin, wcs.users.User):
             o.id,
             o.name,
             o.email,
+            o.phone,
             o.roles,
             o.is_admin,
             o.anonymous,
@@ -3271,7 +3279,7 @@ class SqlUser(SqlMixin, wcs.users.User):
             ascii_name,  # XXX what's this ? pylint: disable=unused-variable
             o.deleted_timestamp,
             o.is_active,
-        ) = (str_encode(x) for x in tuple(row[:13]))
+        ) = (str_encode(x) for x in tuple(row[:14]))
         if o.last_seen:
             o.last_seen = time.mktime(o.last_seen.timetuple())
         if o.roles:
@@ -4462,7 +4470,7 @@ def get_period_total(
 # latest migration, number + description (description is not used
 # programmaticaly but will make sure git conflicts if two migrations are
 # separately added with the same number)
-SQL_LEVEL = (67, 're-migrate legacy tokens')
+SQL_LEVEL = (68, 'add user phone field')
 
 
 def migrate_global_views(conn, cur):
@@ -4614,7 +4622,7 @@ def migrate():
         # 48: remove acked attribute from LoggedError
         # 53: add kind column to logged_errors table
         do_loggederrors_table()
-    if sql_level < 65:
+    if sql_level < 68:
         # 3: introduction of _structured for user fields
         # 4: removal of identification_token
         # 12: (first part) add fts to users
@@ -4623,6 +4631,7 @@ def migrate():
         # 39: add deleted_timestamp
         # 40: add is_active to users
         # 65: index users(name_identifiers)
+        # 68: add user phone field
         do_user_table()
     if sql_level < 32:
         # 25: create session_table
diff --git a/wcs/users.py b/wcs/users.py
index aefc1ac55..e25aa0bcb 100644
--- a/wcs/users.py
+++ b/wcs/users.py
@@ -35,6 +35,7 @@ class User(StorableObject):
 
     name = None
     email = None
+    phone = None
     roles = None
     is_active = True
     is_admin = False
@@ -109,6 +110,8 @@ class User(StorableObject):
             return self.name
         if self.email:
             return self.email
+        if self.phone:
+            return self.phone
         return str(_('Unknown User'))
 
     display_name = property(get_display_name)
@@ -133,6 +136,13 @@ class User(StorableObject):
         if field_email:
             self.email = formdata.get(field_email)
 
+        if formdata.get('phone'):
+            self.phone = formdata.get('phone')
+
+        field_phone = users_cfg.get('field_phone')
+        if field_phone:
+            self.phone = formdata.get(field_phone)
+
         if users_cfg.get('fullname_template'):
             # apply template
             user_ctx = self.get_substitution_variables(prefix='')
@@ -221,6 +231,10 @@ class User(StorableObject):
     def get_users_with_email(cls, email):
         return cls.select([st.Null('deleted_timestamp'), st.Equal('email', email)])
 
+    @classmethod
+    def get_users_with_phone(cls, phone):
+        return cls.select([st.Null('deleted_timestamp'), st.Equal('phone', phone)])
+
     @classmethod
     def get_users_with_name(cls, name):
         return cls.select([st.Null('deleted_timestamp'), st.Equal('name', name)])
@@ -230,6 +244,7 @@ class User(StorableObject):
             prefix + 'user': self,
             prefix + 'user_display_name': self.display_name,
             prefix + 'user_email': self.email,
+            prefix + 'user_phone': self.phone,
         }
         formdef = self.get_formdef()
         if formdef:
@@ -310,6 +325,8 @@ class User(StorableObject):
         }
         if self.email:
             data['email'] = self.email
+        if self.phone:
+            data['phone'] = self.phone
         if self.name_identifiers:
             data['NameID'] = self.name_identifiers
         if full:
@@ -386,4 +403,5 @@ Substitutions.register(
     comment=_('Session User Display Name'),
 )
 Substitutions.register('session_user_email', category=_('User'), comment=_('Session User Email'))
+Substitutions.register('session_user_phone', category=_('User'), comment=_('Session User Phone'))
 Substitutions.register_dynamic_source(User)
-- 
2.38.1

