From fd2d9b79b80bf9723f8432098beab538c3a3571a Mon Sep 17 00:00:00 2001 From: Stefan Janssen Date: Thu, 20 Jun 2024 16:55:28 +0200 Subject: [PATCH] Purge non-verified user (#3417) * use SQL's NOW() fct to log creation of a new user account (validated or not). * at -> on * add new col * adding a new page for admins to easily list all users not yet validated and offer means to delete those * flake8 * fix tests * move DB changes to patch * revert file * no change here * execute merge manually * moving data insertion to test_db_sql patch * account for additional test users --- qiita_db/handlers/tests/test_user.py | 5 +- qiita_db/support_files/patches/92.sql | 4 - .../support_files/patches/test_db_sql/92.sql | 13 +++- qiita_db/test/test_meta_util.py | 2 +- qiita_db/test/test_portal.py | 3 +- qiita_db/test/test_setup.py | 2 +- qiita_pet/handlers/user_handlers.py | 74 +++++++++++++++++++ qiita_pet/templates/sitebase.html | 1 + qiita_pet/test/test_user_handlers.py | 31 ++++++++ qiita_pet/webserver.py | 4 +- 10 files changed, 129 insertions(+), 10 deletions(-) diff --git a/qiita_db/handlers/tests/test_user.py b/qiita_db/handlers/tests/test_user.py index 4898976ce..90d412190 100644 --- a/qiita_db/handlers/tests/test_user.py +++ b/qiita_db/handlers/tests/test_user.py @@ -54,7 +54,10 @@ def test_get(self): {'email': 'shared@foo.bar', 'name': 'Shared'}, {'email': 'admin@foo.bar', 'name': 'Admin'}, {'email': 'demo@microbio.me', 'name': 'Demo'}, - {'email': 'test@foo.bar', 'name': 'Dude'} + {'email': 'test@foo.bar', 'name': 'Dude'}, + {'email': 'justnow@nonvalidat.ed', 'name': 'JustNow'}, + {'email': 'ayearago@nonvalidat.ed', 'name': 'Oldie'}, + {'email': '3Xdays@nonvalidat.ed', 'name': 'TooLate'} ]} self.assertEqual(obs, exp) diff --git a/qiita_db/support_files/patches/92.sql b/qiita_db/support_files/patches/92.sql index 2162533d4..19d284b36 100644 --- a/qiita_db/support_files/patches/92.sql +++ b/qiita_db/support_files/patches/92.sql @@ -39,7 +39,3 @@ ALTER TABLE qiita.qiita_user ADD creation_timestamp timestamp without time zone DEFAULT NOW(); COMMENT ON COLUMN qiita.qiita_user.creation_timestamp IS 'The date the user account was created'; - --- for testing: provide creation date for one of the existing users - -UPDATE qiita.qiita_user SET creation_timestamp = '2015-12-03 13:52:42.751331-07' WHERE email = 'test@foo.bar'; diff --git a/qiita_db/support_files/patches/test_db_sql/92.sql b/qiita_db/support_files/patches/test_db_sql/92.sql index 0e9709f95..65266e9f0 100644 --- a/qiita_db/support_files/patches/test_db_sql/92.sql +++ b/qiita_db/support_files/patches/test_db_sql/92.sql @@ -925,4 +925,15 @@ INSERT INTO qiita.slurm_resource_allocations(processing_job_id, samples, columns ('61da73ff-b4ff-49a1-b775-c6215cfbd291', 231, 107, 2, 'nan', 333544000, 200, '2023-02-17T15:05:17', NULL, NULL), ('6c84dcf1-c5ea-4e69-b17f-d2d5b8d48bdf', 123, 50, 2, 'nan', 327520000, 82, '2023-02-18T15:13:15', NULL, NULL), ('dcb12603-4142-44d1-9a52-3ca3511e380e', 320, 44, 2, 'nan', 329448000, 475, '2023-02-19T06:29:32', NULL, NULL), -('a0dd0a4d-b73f-4e9d-87dd-d29efba25336', 41, 50, 2, 'nan', 301108000, 144, '2023-02-19T09:14:27', NULL, NULL); \ No newline at end of file +('a0dd0a4d-b73f-4e9d-87dd-d29efba25336', 41, 50, 2, 'nan', 301108000, 144, '2023-02-19T09:14:27', NULL, NULL); + +-- for testing: provide creation date for one of the existing users + +UPDATE qiita.qiita_user SET creation_timestamp = '2015-12-03 13:52:42.751331-07' WHERE email = 'test@foo.bar'; + +-- Jun 20, 2024 +-- Add some non-verified users to the test DB to test new admin page: /admin/purge_users/ + +INSERT INTO qiita.qiita_user VALUES ('justnow@nonvalidat.ed', 5, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'JustNow', 'NonVeriUser', '1634 Edgemont Avenue', '303-492-1984', NULL, NULL, NULL, false, NULL, NULL, NULL, NOW()); +INSERT INTO qiita.qiita_user VALUES ('ayearago@nonvalidat.ed', 5, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'Oldie', 'NonVeriUser', '172 New Lane', '102-111-1984', NULL, NULL, NULL, false, NULL, NULL, NULL, NOW() - INTERVAL '1 YEAR'); +INSERT INTO qiita.qiita_user VALUES ('3Xdays@nonvalidat.ed', 5, '$2a$12$gnUi8Qg.0tvW243v889BhOBhWLIHyIJjjgaG6dxuRJkUM8nXG9Efe', 'TooLate', 'NonVeriUser', '564 C Street', '508-492-222', NULL, NULL, NULL, false, NULL, NULL, NULL, NOW() - INTERVAL '30 DAY'); diff --git a/qiita_db/test/test_meta_util.py b/qiita_db/test/test_meta_util.py index 8bfe05d9a..e8fe4c0de 100644 --- a/qiita_db/test/test_meta_util.py +++ b/qiita_db/test/test_meta_util.py @@ -281,7 +281,7 @@ def _get_daily_stats(): self.assertDictEqual(f(redis_key), exp) # then the unique values vals = [ - ('num_users', b'4', r_client.get), + ('num_users', b'7', r_client.get), ('lat_longs', b'[]', r_client.get), ('num_studies_ebi', b'1', r_client.get), ('num_samples_ebi', b'27', r_client.get), diff --git a/qiita_db/test/test_portal.py b/qiita_db/test/test_portal.py index 2a5bb5413..59b277a42 100644 --- a/qiita_db/test/test_portal.py +++ b/qiita_db/test/test_portal.py @@ -47,7 +47,8 @@ def test_add_portal(self): qdb.sql_connection.TRN.add("SELECT * FROM qiita.analysis_portal") obs = qdb.sql_connection.TRN.execute_fetchindex() exp = [[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 2], [8, 2], - [9, 2], [10, 2], [11, 4], [12, 4], [13, 4], [14, 4]] + [9, 2], [10, 2], [11, 4], [12, 4], [13, 4], [14, 4], + [15, 4], [16, 4], [17, 4]] self.assertCountEqual(obs, exp) with self.assertRaises(qdb.exceptions.QiitaDBDuplicateError): diff --git a/qiita_db/test/test_setup.py b/qiita_db/test/test_setup.py index 3cdabe255..70825fe59 100644 --- a/qiita_db/test/test_setup.py +++ b/qiita_db/test/test_setup.py @@ -15,7 +15,7 @@ class SetupTest(TestCase): """Tests that the test database have been successfully populated""" def test_qiita_user(self): - self.assertEqual(get_count("qiita.qiita_user"), 4) + self.assertEqual(get_count("qiita.qiita_user"), 7) def test_study_person(self): self.assertEqual(get_count("qiita.study_person"), 3) diff --git a/qiita_pet/handlers/user_handlers.py b/qiita_pet/handlers/user_handlers.py index cd65f18c7..c85677cb8 100644 --- a/qiita_pet/handlers/user_handlers.py +++ b/qiita_pet/handlers/user_handlers.py @@ -7,6 +7,8 @@ # ----------------------------------------------------------------------------- import re +from json import dumps +import warnings from tornado.web import authenticated, HTTPError from wtforms import Form, StringField, BooleanField, validators @@ -14,6 +16,8 @@ from qiita_pet.handlers.base_handlers import BaseHandler from qiita_pet.handlers.api_proxy import user_jobs_get_req +from qiita_pet.handlers.portal import PortalEditBase +import qiita_db as qdb from qiita_db.util import send_email from qiita_db.user import User from qiita_db.logger import LogEntry @@ -375,3 +379,73 @@ class UserJobs(BaseHandler): def get(self): response = user_jobs_get_req(self.current_user) self.write(response) + + +class PurgeUsersAJAXHandler(PortalEditBase): + # define columns besides email that will be displayed on website + FIELDS = ['name', 'affiliation', 'address', 'phone', + 'creation_timestamp'] + + @authenticated + @execute_as_transaction + def get(self): + # retrieving users not yet verified + self.check_admin() + with qdb.sql_connection.TRN: + sql = """SELECT email,{0} + FROM qiita.qiita_user + WHERE (user_level_id=5) AND + (creation_timestamp < (NOW() - INTERVAL '30 DAY')) + """.format(','.join(self.FIELDS)) + qdb.sql_connection.TRN.add(sql) + users = qdb.sql_connection.TRN.execute()[1:] + + # fetching information for each user + result = [] + for list in users: + for user in list: + usermail = user[0] + user_unit = {'email': usermail} + user_infos = User(usermail).info + for col in self.FIELDS: + user_unit[col] = str(user_infos[col]) + result.append(user_unit) + # returning information as JSON + self.write(dumps(result, separators=(',', ':'))) + + +class PurgeUsersHandler(PortalEditBase): + @authenticated + @execute_as_transaction + def get(self): + # render page and transfer headers to be included for the table + self.check_admin() + self.render('admin_purge_users.html', + headers=['email'] + PurgeUsersAJAXHandler.FIELDS, + submit_url="/admin/purge_users/") + + def post(self): + # check if logged in user is admin and fetch all checked boxes as well + # as the action + self.check_admin() + users = map(str, self.get_arguments('selected')) + action = self.get_argument('action') + + # depending on the action delete user from db (remove) + num_deleted_user = 0 + for user in users: + try: + with warnings.catch_warnings(record=True) as warns: + if action == "Remove": + user_to_delete = User(user) + user_to_delete.delete(user) + num_deleted_user += 1 + else: + raise HTTPError( + 400, reason="Unknown action: %s" % action) + except QiitaDBError as e: + self.write(action.upper() + " ERROR:
" + str(e)) + return + msg = '; '.join([str(w.message) for w in warns]) + self.write(("%i non-validated user(s) successfully removed from " + "database
%s") % (num_deleted_user, msg)) diff --git a/qiita_pet/templates/sitebase.html b/qiita_pet/templates/sitebase.html index 5fcc0db12..7165f33a2 100644 --- a/qiita_pet/templates/sitebase.html +++ b/qiita_pet/templates/sitebase.html @@ -383,6 +383,7 @@
  • View Errors
  • View Studies awaiting approval
  • Edit study portal connections
  • +
  • Purge non-validated users
  • {% end %}
  • Sample Validation
  • Processing Jobs
  • diff --git a/qiita_pet/test/test_user_handlers.py b/qiita_pet/test/test_user_handlers.py index 42fd46d8a..a47729ad7 100644 --- a/qiita_pet/test/test_user_handlers.py +++ b/qiita_pet/test/test_user_handlers.py @@ -9,9 +9,13 @@ from unittest import main from wtforms.validators import ValidationError from wtforms import StringField +from mock import Mock +from json import loads from qiita_pet.test.tornado_test_base import TestHandlerBase from qiita_pet.handlers.user_handlers import UserProfile +from qiita_pet.handlers.base_handlers import BaseHandler +from qiita_db.user import User class TestUserProfile(TestHandlerBase): @@ -124,5 +128,32 @@ def test_get(self): self.assertEqual(response.code, 200) +class TestPurgeUsersAJAXHandler(TestHandlerBase): + def setUp(self): + super().setUp() + BaseHandler.get_current_user = Mock(return_value=User("admin@foo.bar")) + + def test_get(self): + response = self.get('/admin/purge_usersAjax/?_=1718805487494') + obs_users_table = loads(response.body.decode('ascii')) + obs_users = {user['email'] for user in obs_users_table} + self.assertIn('ayearago@nonvalidat.ed', obs_users) + self.assertIn('3Xdays@nonvalidat.ed', obs_users) + self.assertNotIn('justnow@nonvalidat.ed', obs_users) + + def test_post_removeBoth(self): + # remove both users + response = self.post('/admin/purge_users/', + {'action': 'Remove', + 'selected': ['ayearago@nonvalidat.ed', + '3Xdays@nonvalidat.ed']}) + self.assertEqual(response.code, 200) + + # test that zero users are listed now + response = self.get('/admin/purge_usersAjax/?_=1718805487495') + obs_users_table = loads(response.body.decode('ascii')) + self.assertEqual(obs_users_table, []) + + if __name__ == "__main__": main() diff --git a/qiita_pet/webserver.py b/qiita_pet/webserver.py index 84d71431a..50b34fd2b 100644 --- a/qiita_pet/webserver.py +++ b/qiita_pet/webserver.py @@ -23,7 +23,7 @@ AuthCreateHandler, AuthLoginHandler, AuthLogoutHandler, AuthVerifyHandler) from qiita_pet.handlers.user_handlers import ( ChangeForgotPasswordHandler, ForgotPasswordHandler, UserProfileHandler, - UserMessagesHander, UserJobs) + UserMessagesHander, UserJobs, PurgeUsersAJAXHandler, PurgeUsersHandler) from qiita_pet.handlers.admin_processing_job import ( AdminProcessingJob, AJAXAdminProcessingJobListing, SampleValidation) from qiita_pet.handlers.analysis_handlers import ( @@ -134,6 +134,8 @@ def __init__(self): (r"/admin/processing_jobs/", AdminProcessingJob), (r"/admin/processing_jobs/list", AJAXAdminProcessingJobListing), (r"/admin/sample_validation/", SampleValidation), + (r"/admin/purge_users/", PurgeUsersHandler), + (r"/admin/purge_usersAjax/", PurgeUsersAJAXHandler), (r"/ebi_submission/(.*)", EBISubmitHandler), # Study handlers (r"/study/create/", StudyEditHandler),