From cde4d42ca586e40c5be97dfe22d173f415554b8a Mon Sep 17 00:00:00 2001 From: Kory Becker <50708624+kbecker42@users.noreply.github.com> Date: Wed, 17 Aug 2022 15:55:11 -0400 Subject: [PATCH 1/6] Fixed incorrect value in audit log from /assign-users. (#763) --- pybossa/view/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index ddbb4ee28a..f344dd2338 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -3629,7 +3629,7 @@ def assign_users(short_name): project.set_project_users(project_users) project_repo.save(project) auditlogger.log_event(project, current_user, 'update', 'project.assign_users', - 'N/A', users) + 'N/A', project_users) if not project_users: msg = gettext('Users unassigned or no user assigned to project') current_app.logger.info('Project id {} users unassigned from project.'.format(project.id)) From ed2d3a8a32adfd71c267523ac36dfdbdeddfe4e1 Mon Sep 17 00:00:00 2001 From: Xi Chen Date: Thu, 18 Aug 2022 09:49:00 -0400 Subject: [PATCH 2/6] RDISCROWD-2867 Add input-text-area to component builder (#764) --- pybossa/themes/default | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybossa/themes/default b/pybossa/themes/default index 1d0c544ab8..6f8787aba5 160000 --- a/pybossa/themes/default +++ b/pybossa/themes/default @@ -1 +1 @@ -Subproject commit 1d0c544ab8e00715e54a5c94e6676e129593fd49 +Subproject commit 6f8787aba5310cd875a35098bf11e68330e62f91 From 7335339b2860f7006ca9726dd7b48a69e8855f8d Mon Sep 17 00:00:00 2001 From: Deepsingh Chhabda Date: Fri, 19 Aug 2022 09:20:38 -0400 Subject: [PATCH 3/6] Gracefully handle reserve task category for private instance (#765) --- pybossa/sched.py | 5 +++++ test/test_reserve_task_category.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/pybossa/sched.py b/pybossa/sched.py index ab3a888ed6..59359f6c33 100644 --- a/pybossa/sched.py +++ b/pybossa/sched.py @@ -442,6 +442,11 @@ def get_reserve_task_category_info(reserve_task_config, project_id, timeout, use if not reserve_task_config: return sql_filters, category_keys + if current_app.config.get('PRIVATE_INSTANCE'): + current_app.logger.info("Reserve task by category disabled for private instance. project_id %s, reserve_task_config %s", + project_id, str(reserve_task_config)) + return sql_filters, category_keys + category = ":".join(["{}:*".format(field) for field in sorted(reserve_task_config)]) lock_manager = LockManager(sentinel.master, timeout) category_keys = lock_manager.get_task_category_lock(project_id, user_id, category, exclude_user) diff --git a/test/test_reserve_task_category.py b/test/test_reserve_task_category.py index 81e9061cea..8bb2e4a1da 100644 --- a/test/test_reserve_task_category.py +++ b/test/test_reserve_task_category.py @@ -143,6 +143,13 @@ def test_get_reserve_task_category_info(self, get_task_category_lock): assert sql_filters == expected_sql_filter and \ category_keys == expected_category_keys, "sql_filters, category_keys must be non empty" + # reserve task disabled for private instance, + with patch.dict(self.flask_app.config, {'PRIVATE_INSTANCE': True}): + sql_filters, category_keys = get_reserve_task_category_info(reserve_task_config, project.id, timeout, owner.id) + assert not sql_filters, "sql_filters must be empty for private instance" + assert not category_keys, "sql_filters must be empty for private instance" + + @with_context def test_acquire_and_release_reserve_task_lock(self): user = UserFactory.create() From c04a65a509a862951b76578f72ce9b16cb6acb3b Mon Sep 17 00:00:00 2001 From: Kory Becker <50708624+kbecker42@users.noreply.github.com> Date: Tue, 23 Aug 2022 11:24:37 -0400 Subject: [PATCH 4/6] RDISCROWD-4966 Fix for assigned users empty (#766) * Fix for assigned users empty. * cleanup * Unit tests. * fix * cleanup * cleanup * Fix assign workers bulk. * fix * touch * unit test --- README.md | 2 +- pybossa/view/projects.py | 13 ++- test/test_view/test_assign_task_worker.py | 129 ++++++++++++++++++++-- 3 files changed, 131 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b19bb10c8a..7b8c9a024e 100644 --- a/README.md +++ b/README.md @@ -117,4 +117,4 @@ The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. -Documentation and media is under a Creative Commons Attribution License version 3. +Documentation and media is under a Creative Commons Attribution License version 3. \ No newline at end of file diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index f344dd2338..87c3416a9a 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -1784,7 +1784,6 @@ def bulk_priority_update(short_name): @blueprint.route('//tasks/assign-workersupdate', methods=['POST']) @login_required def bulk_update_assign_worker(short_name): - response = {} project, owner, ps = project_by_shortname(short_name) admin_or_project_owner(current_user, project) @@ -1792,7 +1791,6 @@ def bulk_update_assign_worker(short_name): if data.get("add") is None and data.get("remove") is None: # read data and return users - task_id = data.get("taskId") bulk_update = False assign_user_emails = [] @@ -1813,6 +1811,7 @@ def bulk_update_assign_worker(short_name): for task_id in task_ids: t = task_repo.get_task_by(project_id=project.id, id=int(task_id)) + t.user_pref = t.user_pref or {} assign_user_emails = assign_user_emails.union(set(t.user_pref.get("assign_user", []))) assign_users = [] for user_email in assign_user_emails: @@ -1831,7 +1830,6 @@ def bulk_update_assign_worker(short_name): all_users = user_repo.get_all() all_user_data = [] for user in all_users: - # Exclude currently assigned users in the candidate list ONLY for single task update if user.email_addr in assign_user_emails and not bulk_update: continue @@ -1844,6 +1842,7 @@ def bulk_update_assign_worker(short_name): # update tasks with assign worker values assign_workers = data.get('add', []) remove_workers = data.get('remove', []) + assign_users = [] assign_worker_emails = [w["email"] for w in assign_workers] remove_worker_emails = [w["email"] for w in remove_workers] @@ -1873,12 +1872,18 @@ def bulk_update_assign_worker(short_name): if remove_user_email in assign_user: assign_user.remove(remove_user_email) - user_pref["assign_user"] = assign_user + if assign_user: + user_pref["assign_user"] = assign_user + assign_users.append({'taskId': int(task_id), 'assign_user': assign_user}) + elif "assign_user" in user_pref: + del user_pref["assign_user"] t.user_pref = user_pref flag_modified(t, "user_pref") task_repo.update(t) + response['assign_users'] = assign_users + return Response(json.dumps(response), 200, mimetype='application/json') @crossdomain(origin='*', headers=cors_headers) diff --git a/test/test_view/test_assign_task_worker.py b/test/test_view/test_assign_task_worker.py index 296f88e5de..d120c3d14b 100644 --- a/test/test_view/test_assign_task_worker.py +++ b/test/test_view/test_assign_task_worker.py @@ -103,24 +103,137 @@ def test_bulk_priority_update(self): assert task1.priority_0 == .5, task1.priority_0 + @with_context + def test_get_assign_workers_by_bulk(self): + """Test get assign worker by bulk.""" + admin = UserFactory.create(admin=True) + admin.set_password('1234') + user_repo.save(admin) + + csrf = self.get_csrf('/account/signin') + self.signin(email=admin.email_addr, csrf=csrf) + + # Create a project with a worker assigned. + user1 = UserFactory.create(email_addr='a1@a.com', fullname="test_user1") + user2 = UserFactory.create(email_addr='a2@a.com', fullname="test_user2") + project = ProjectFactory.create(published=True, info={'project_users': [user1.id, user2.id]}) + + task1 = TaskFactory.create(project=project) + task_repo.update(task1) + task2 = TaskFactory.create(project=project) + task_repo.update(task2) + + # Assign user to all tasks by filter. + req_data = dict(filters='{"add":[{"fullname":"' + user1.fullname + '","email":"' + user1.email_addr + '"}],"remove":[]}') + + url = '/project/%s/tasks/assign-workersupdate?api_key=%s' % (project.short_name, project.owner.api_key) + res = self.app.post(url, content_type='application/json', data=json.dumps(req_data), follow_redirects=True, headers={'X-CSRFToken': csrf}) + res_data = json.loads(res.data) + + # Verify users are returned. + assert len(res_data['all_users']) > 1 + assert res_data['all_users'][0]['email'] + + # Verify emails are returned. + emails = [user['email'] for user in res_data['all_users']] + assert user1.email_addr in emails + assert user2.email_addr in emails + + @with_context + def test_update_assign_workers_by_task_id(self): + """Test update assign worker by task id.""" + admin = UserFactory.create(admin=True) + admin.set_password('1234') + user_repo.save(admin) + + csrf = self.get_csrf('/account/signin') + self.signin(email=admin.email_addr, csrf=csrf) + + project = ProjectFactory.create(published=True) + user = UserFactory.create(email_addr='a@a.com', fullname="test_user") + + task1 = TaskFactory.create(project=project) + task_repo.update(task1) + + # Assign user to task by task id. + req_data = dict(taskId=str(task1.id), add=[{'email':user.email_addr}]) + + url = '/project/%s/tasks/assign-workersupdate?api_key=%s' % (project.short_name, project.owner.api_key) + res = self.app.post(url, content_type='application/json', data=json.dumps(req_data), follow_redirects=True, headers={'X-CSRFToken': csrf}) + res_data = json.loads(res.data) + + # Verify the user has been assigned to this task. + assert res_data['assign_users'][0]['taskId'] == task1.id + assert res_data['assign_users'][0]['assign_user'][0] == user.email_addr + + task1_modified = task_repo.get_task(task1.id) + assert user.email_addr in task1_modified.user_pref.get('assign_user') @with_context - def test_update_assign_workers(self): - """Test update assign worker.""" + def test_update_assign_workers_by_filter(self): + """Test update assign worker by filter.""" + admin = UserFactory.create(admin=True) + admin.set_password('1234') + user_repo.save(admin) + + csrf = self.get_csrf('/account/signin') + self.signin(email=admin.email_addr, csrf=csrf) + + project = ProjectFactory.create(published=True) + user = UserFactory.create(email_addr='a@a.com', fullname="test_user") + + task1 = TaskFactory.create(project=project) + task_repo.update(task1) + + # Assign user to task by filter. + req_data = dict(filters='{"task_id": "' + str(task1.id) + '"}', add=[{'email':user.email_addr}]) + + url = '/project/%s/tasks/assign-workersupdate?api_key=%s' % (project.short_name, project.owner.api_key) + res = self.app.post(url, content_type='application/json', data=json.dumps(req_data), follow_redirects=True, headers={'X-CSRFToken': csrf}) + res_data = json.loads(res.data) + + # Verify the user has been assigned to this task. + assert res_data['assign_users'][0]['taskId'] == task1.id + assert res_data['assign_users'][0]['assign_user'][0] == user.email_addr + + task1_modified = task_repo.get_task(task1.id) + assert user.email_addr in task1_modified.user_pref.get('assign_user') + + @with_context + def test_update_assign_workers_remove(self): + """Test remove assigned worker.""" + admin = UserFactory.create(admin=True) + admin.set_password('1234') + user_repo.save(admin) + + csrf = self.get_csrf('/account/signin') + self.signin(email=admin.email_addr, csrf=csrf) + project = ProjectFactory.create(published=True) user = UserFactory.create(email_addr='a@a.com', fullname="test_user") task1_user_pref = dict(assign_user=[user.email_addr]) + # Create a task with an assigned user. task1 = TaskFactory.create(project=project, user_pref=task1_user_pref) task_repo.update(task1) - req_data = dict(taskIds=str(task1.id), add=user) + # Verify the user has been assigned to the task. + assert user.email_addr in task1.user_pref.get('assign_user') + + # Load the task and verify the user has been assigned to the task. + task1_modified1 = task_repo.get_task(task1.id) + assert user.email_addr in task1_modified1.user_pref.get('assign_user') + + # Remove the user. + req_data = dict(taskId=str(task1.id), remove=[{'email':user.email_addr}]) url = '/project/%s/tasks/assign-workersupdate?api_key=%s' % (project.short_name, project.owner.api_key) - req_data = dict(taskId=None) - res = self.app.post(url, content_type='application/json', - data=json.dumps(req_data)) + res = self.app.post(url, content_type='application/json', data=json.dumps(req_data), follow_redirects=True, headers={'X-CSRFToken': csrf}) res_data = json.loads(res.data) - assert res_data['assign_users'][0]['fullname'] == user.fullname - assert res_data['assign_users'][0]['email'] == user.email_addr + + assert not res_data['assign_users'] + + # Verify the assign_user parameter has been deleted from the user_pref. + task1_modified2 = task_repo.get_task(task1.id) + assert not task1_modified2.user_pref.get('assign_user') From b5cbd0d5a5dd348020a8873198c082294882a0b3 Mon Sep 17 00:00:00 2001 From: Kory Becker <50708624+kbecker42@users.noreply.github.com> Date: Wed, 24 Aug 2022 11:31:25 -0400 Subject: [PATCH 5/6] RDISCROWD-4966 Fix order_by for assign users (#767) * Fix for assigned users empty. * cleanup * Unit tests. * fix * cleanup * cleanup * Fix assign workers bulk. * fix * touch * unit test * Bug fix for order_by to convert json object to string args. --- pybossa/cache/task_browse_helpers.py | 2 ++ test/test_cache/test_cache_helpers.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/pybossa/cache/task_browse_helpers.py b/pybossa/cache/task_browse_helpers.py index 49a094e168..63978d4770 100644 --- a/pybossa/cache/task_browse_helpers.py +++ b/pybossa/cache/task_browse_helpers.py @@ -297,6 +297,8 @@ def parse_tasks_browse_order_by_args(order_by, display_info_columns): order_by_dict = dict() if order_by: + # Convert dict {'task_id': 'asc'} to string "task_id asc". + order_by = re.sub("[{}:'\"]", '', str(order_by)) if type(order_by).__name__ == 'dict' else order_by order_by_result = order_by.strip() # allowing custom user added task.info columns to be sortable diff --git a/test/test_cache/test_cache_helpers.py b/test/test_cache/test_cache_helpers.py index 4a44902c37..0825ddbb29 100644 --- a/test/test_cache/test_cache_helpers.py +++ b/test/test_cache/test_cache_helpers.py @@ -485,3 +485,20 @@ def test_order_by_args_multiple_columns4(self): assert 'bi' in order_by_dict assert 'companyBbid' in order_by_dict assert 'somethingElse' in order_by_dict + + @with_context + def test_order_by_args_dict_1(self): + """Test parse_tasks_browse_order_by_args as dict 1 field.""" + order_by_result, order_by_dict = parse_tasks_browse_order_by_args({'task_id': 'asc'}, ['bi', 'companyBbid', 'somethingElse']) + + assert order_by_result == "id asc" + assert 'task_id' in order_by_dict + + @with_context + def test_order_by_args_dict_2(self): + """Test parse_tasks_browse_order_by_args as dict 2 fields.""" + order_by_result, order_by_dict = parse_tasks_browse_order_by_args({'task_id': 'asc', 'priority': 'desc'}, ['bi', 'companyBbid', 'somethingElse']) + + assert order_by_result == "id asc, priority_0 desc" + assert 'task_id' in order_by_dict + assert 'priority' in order_by_dict From a8ec45a9c5fd7dc9469e5dde3f2b0d4cf58d3910 Mon Sep 17 00:00:00 2001 From: Kory Becker <50708624+kbecker42@users.noreply.github.com> Date: Thu, 25 Aug 2022 09:38:15 -0400 Subject: [PATCH 6/6] RDISCROWD-4848 - Custom email contacts (#761) * Added API for managing contacts. * Added persistence of contacts. * Added email to custom contact list. * Filter contacts. * Allow adding any user as a contact, only default contacts will be filtered to owners. * opt * fix audit * opt * fix * Refactor. * opt --- pybossa/view/projects.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index 87c3416a9a..b4a06fa21e 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -3134,6 +3134,10 @@ def transfer_ownership(short_name): return handle_content_type(response) +def is_user_enabled_assigned_project(user, project): + # Return true if the user is enabled and assigned to this project or is a sub-admin/admin. + return user.enabled and (user.id == project.owner_id or user.admin or user.subadmin) + @blueprint.route('//coowners', methods=['GET', 'POST']) @login_required def coowners(short_name): @@ -3147,6 +3151,9 @@ def coowners(short_name): for owner, p_owner in zip(owners, pub_owners): if owner.id == project.owner_id: p_owner['is_creator'] = True + contact_owners = [user for user in owners if is_user_enabled_assigned_project(user, project)] + contact_users = user_repo.get_users(project.info.get('contacts')) if project.info.get('contacts') else contact_owners + contacts_dict = [{"id": user.id, "fullname": user.fullname} for user in contact_users] ensure_authorized_to('read', project) ensure_authorized_to('update', project) @@ -3155,6 +3162,7 @@ def coowners(short_name): project=sanitize_project, coowners=pub_owners, coowners_dict=coowners, + contacts_dict=contacts_dict, owner=owner_sanitized, title=gettext("Manage Co-owners"), form=form, @@ -3170,7 +3178,6 @@ def coowners(short_name): filters = {'enabled': True} users = user_repo.search_by_name(query, **filters) - if not users: markup = Markup('{} {} {}') flash(markup.format(gettext("Ooops!"), @@ -3200,6 +3207,13 @@ def coowners(short_name): add = [value for value in new_list if value not in overlap_list] for _id in add: project.owners_ids.append(_id) + + # save contacts + new_list = [int(x) for x in json.loads(request.data).get('contacts', [])] + auditlogger.log_event(project, current_user, 'update', 'project.contacts', + project.info.get('contacts'), new_list) + project.info['contacts'] = new_list + project_repo.save(project) flash(gettext('Configuration updated successfully'), 'success') @@ -3972,9 +3986,12 @@ def contact(short_name): tasks_completed_user=request.body.get("tasksCompletedUser", 0) ) - # Get the email addrs for the owner, and all co-owners of the project who have sub-admin/admin rights and are not disabled. - owners = user_repo.get_users(project.owners_ids) - recipients = [owner.email_addr for owner in owners if owner.enabled and (owner.id == project.owner_id or owner.admin or owner.subadmin)] + # Use the customized list of contacts for the project or default to owners. + contact_ids = project.info.get('contacts', project.owners_ids) + # Load the record for each contact id. + contact_users = user_repo.get_users(contact_ids) + # Get the email address for each contact that was added manually or that is enabled and assigned to this project or is a sub-admin/admin. + recipients = [contact.email_addr for contact in contact_users if project.info.get('contacts') or is_user_enabled_assigned_project(contact, project)] # Send email. email = dict(recipients=recipients, @@ -3982,8 +3999,8 @@ def contact(short_name): body=body) mail_queue.enqueue(send_mail, email) - current_app.logger.info('Contact form email sent to {} at {} for project {} {} {}'.format( - current_user.name, recipients, current_user.id, project.name, short_name, project.id)) + current_app.logger.info('Contact email sent from user id {} ({}) to recipients {} for project id {} ({}, {})'.format( + current_user.id, current_user.name, recipients, project.id, project.name, short_name)) response = { 'success': True