From 2b9efc67fe1a20e7c8ace9eeffa797e2bc61ab5d Mon Sep 17 00:00:00 2001 From: Deepsingh Chhabda Date: Fri, 10 Feb 2023 17:44:16 -0500 Subject: [PATCH] RDISCROWD-5651: Enable browse task for regular user (#811) * enable task browse for regular user * update themes to enable customized column for all scheudulers * add tests. fix state filter * update themes * rebase to main, update themes --- pybossa/cache/task_browse_helpers.py | 15 +++-- pybossa/themes/default | 2 +- pybossa/view/projects.py | 27 ++++++++- test/test_cache/test_cache_projects.py | 15 +++++ test/test_web.py | 83 ++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 8 deletions(-) diff --git a/pybossa/cache/task_browse_helpers.py b/pybossa/cache/task_browse_helpers.py index 03dcfe93e6..4067bbab53 100644 --- a/pybossa/cache/task_browse_helpers.py +++ b/pybossa/cache/task_browse_helpers.py @@ -91,15 +91,20 @@ def get_task_filters(args): user_pref_db_clause = get_user_pref_db_clause(user_pref) filters += " AND ( {} )".format(user_pref_db_clause) - # for task queue - if args.get("filter_by_wfilter_upref"): + # for regular user, only include tasks that user has worked on + if args.get("allow_taskrun_edit"): + filters += ''' AND EXISTS + (SELECT 1 FROM task_run WHERE project_id=:project_id AND + user_id=:user_id AND task_id=task.id)''' + params["user_id"] = args.get('user_id') + elif args.get("filter_by_wfilter_upref"): # for task queue # task queue exclude completed tasks filters += " AND state!='completed'" - # exclude tasks that the current worker has worked on before filters += ''' AND NOT EXISTS - (SELECT 1 FROM task_run WHERE project_id=:project_id AND - user_id=:user_id AND task_id=task.id)''' + (SELECT 1 FROM task_run WHERE project_id=:project_id AND + user_id=:user_id AND task_id=task.id)''' + params["user_id"] = args.get('user_id') # include additional filters diff --git a/pybossa/themes/default b/pybossa/themes/default index b0f06e817f..15630ab34c 160000 --- a/pybossa/themes/default +++ b/pybossa/themes/default @@ -1 +1 @@ -Subproject commit b0f06e817f1c9774da4d0a8bf9b30831209e5b61 +Subproject commit 15630ab34c5cead9ba12291934144cc9b6eff1b4 diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index 52b965c53d..d1a582a67c 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -1309,6 +1309,10 @@ def task_presenter(short_name, task_id, task_submitter_id=None): # Allow lock when scheduler is task_queue and user is a worker or user is admin/subadminm/coowner in task view. if scheduler == sched.Schedulers.task_queue and mode == "cherry_pick": lock_task_for_user(task_id, project.id, current_user.id) + elif project.info.get("allow_taskrun_edit") and task_submitter_id: + # with project with edit submissions enabled and task_submitter_id passed + # task response would be displayed later + pass elif not sched.can_read_task(task, current_user): raise abort(403) @@ -1601,6 +1605,9 @@ def tasks_browse(short_name, page=1, records_per_page=None): title = project_title(project, "Tasks") pro = pro_features() allowed_records_per_page = [10, 20, 30, 50, 70, 100] + admin_subadmin_coowner = current_user.subadmin or current_user.admin or current_user.id in project.owners_ids + regular_user = not admin_subadmin_coowner + allow_taskrun_edit = project.info.get("allow_taskrun_edit") or False try: columns = get_searchable_columns(project.id) @@ -1615,11 +1622,24 @@ def tasks_browse(short_name, page=1, records_per_page=None): view_type = request.args.get('view') task_browse_default_records_per_page = 10 task_list_default_records_per_page = 30 - if view_type != 'tasklist' and (current_user.subadmin or current_user.admin or current_user.id in project.owners_ids): + if view_type != 'tasklist' and admin_subadmin_coowner: # owners and (sub)admin have full access, default size page for owner view is 10 per_page = records_per_page if records_per_page in allowed_records_per_page else task_browse_default_records_per_page # parse args args = parse_tasks_browse_args(request.args.to_dict()) + elif view_type != 'tasklist' and allow_taskrun_edit and regular_user: + # browse tasks for a regular user to be available when + # 1. project is configured to allow editing of task runs + # 2. browse task request is not for task list + dict_args = request.args.to_dict() + # show columns that are permitted for regular users + dict_args["display_columns"] = ["task_id", "priority", "created"] + # show task.info columns that are configured under tasklist_columns + dict_args["display_info_columns"] = project.info.get('tasklist_columns', []) + args = parse_tasks_browse_args(dict_args) + args["user_id"] = current_user.id + args["allow_taskrun_edit"] = True + per_page = records_per_page if records_per_page in allowed_records_per_page else task_browse_default_records_per_page elif scheduler == Schedulers.task_queue: # worker can access limited tasks only when task_queue_scheduler is selected user = cached_users.get_user_by_id(current_user.id) @@ -1647,6 +1667,7 @@ def tasks_browse(short_name, page=1, records_per_page=None): args["sql_params"] = dict(assign_user=json.dumps({'assign_user': [user_email]})) args["display_columns"] = ['task_id', 'priority', 'created'] args["view"] = view_type + args["regular_user"] = regular_user # default page size for worker view is 100 per_page = records_per_page if records_per_page in allowed_records_per_page else task_list_default_records_per_page else: @@ -1751,7 +1772,9 @@ def get_users_completed(task): location_options=location_options, reserved_options=RESERVED_TASKLIST_COLUMNS, rdancy_upd_exp=rdancy_upd_exp, - can_know_task_is_gold=can_know_task_is_gold) + can_know_task_is_gold=can_know_task_is_gold, + allow_taskrun_edit=allow_taskrun_edit, + regular_user=regular_user) return handle_content_type(data) diff --git a/test/test_cache/test_cache_projects.py b/test/test_cache/test_cache_projects.py index 3b1f8bf6fe..87b77e04de 100644 --- a/test/test_cache/test_cache_projects.py +++ b/test/test_cache/test_cache_projects.py @@ -882,6 +882,21 @@ def test_task_browse_get_task_filters(self): assert filters == expected_filter_query, filters assert params == expected_params, params + # with allow_taskrun_edit, user submitted responses to be returned + user_submitted_responses = "(SELECT 1 FROM task_run WHERE project_id=:project_id AND\n user_id=:user_id AND task_id=task.id)" + expected_user_id = 239 + filters = dict(task_id=1, allow_taskrun_edit=True, order_by='task_id', user_id=expected_user_id) + filters, params = get_task_filters(filters) + assert filters.find(user_submitted_responses) > -1, "only user submitted responses to be returned by the query" + assert params["user_id"] == expected_user_id, "user id to be present to filter user responses by id" + + filters = dict(task_id=1, allow_taskrun_edit=False, order_by='task_id', user_id=239) + filters, params = get_task_filters(filters) + assert filters.find(user_submitted_responses) == -1, "user submitted responses NOT to be returned by the query" + assert "user_id" not in params + + + def test_task_browse_gold_task_filters(self): filters = dict(task_id=1,hide_completed=True, gold_task='1', order_by='task_id') expected_filter_query = " AND task.id = :task_id AND task.state='ongoing' AND task.calibration = :calibration" diff --git a/test/test_web.py b/test/test_web.py index 183bb61b9b..91375b6746 100644 --- a/test/test_web.py +++ b/test/test_web.py @@ -3400,6 +3400,67 @@ def test_task_presenter(self): res = self.app.get('/project/%s/task/%s/%s' % (project_short_name, task.id, user.id)) assert b'TaskPresenter' in res.data + @with_context + def test_task_presenter_with_allow_taskrun_edit_works(self): + """Test WEB with taskrun edit permitted, get expected task based on user access""" + self.register() + self.signin() + self.create() + project = db.session.query(Project).get(1) + project.info = dict(allow_taskrun_edit=True) + db.session.commit() + self.new_task(project.id) + project_short_name = project.short_name + + task = db.session.query(Task).filter(Task.project_id == 1).first() + + regular_user = UserFactory.create(id=999, subadmin=False, admin=False) + regular_user.set_password('1234') + user_repo.save(regular_user) + self.signin(email=regular_user.email_addr, password='1234') + task_run = TaskRun(project_id=project.id, task_id=task.id, + info={'answer': 1, + 'odfoa': {'version': 1, 'source-uri': 'http://fake.com', 'odf': {}, 'oa': {}}, + 'fake': {'b': 27}}, + user_id=regular_user.id) + db.session.add(task_run) + db.session.commit() + + # passing task_submitter_id, task response retrieved and no 403 + res = self.app.get('/project/%s/task/%s/%s' % (project_short_name, task.id, regular_user.id)) + assert res.status_code == 200, res.status_code + + @with_context + def test_task_presenter_with_allow_taskrun_edit_raises_forbidden(self): + """Test WEB with taskrun edit permitted, task_submitter_id not passed raises 403""" + self.register() + self.signin() + self.create() + project = db.session.query(Project).get(1) + project.info = dict(allow_taskrun_edit=True) + db.session.commit() + self.new_task(project.id) + project_short_name = project.short_name + + task = db.session.query(Task).filter(Task.project_id == 1).first() + user = db.session.query(User).first() + task_run = TaskRun(project_id=project.id, task_id=task.id, + info={'answer': 1, + 'odfoa': {'version': 1, 'source-uri': 'http://fake.com', 'odf': {}, 'oa': {}}, + 'fake': {'b': 27}}, + user_id=user.id) + db.session.add(task_run) + db.session.commit() + + # task_submitter_id is passed to fetch task response recorded by the user + # 403 is returned when not passing task_submitter_id + regular_user = UserFactory.create(id=999, subadmin=False, admin=False) + regular_user.set_password('1234') + user_repo.save(regular_user) + self.signin(email=regular_user.email_addr, password='1234') + res = self.app.get('/project/%s/task/%s' % (project_short_name, task.id)) + assert res.status_code == 403, res.status_code + @with_context @patch('pybossa.view.projects.uploader.upload_file', return_value=True) def test_25_get_wrong_task_app(self, mock): @@ -9301,6 +9362,28 @@ def test_task_list_worker_view_1(self, check_password): res = self.app.get(url, follow_redirects=True) assert res.status_code == 403, res.status_code + @with_context + @patch('pybossa.view.projects._check_if_redirect_to_password') + def test_tasks_browse_allow_taskrun_edit_works(self, check_password): + """Test tasks browse with edit submission permitted works for regular user.""" + check_password.return_value = None + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(zip_download=True, owner=owner, info={"sched": "default", "allow_taskrun_edit": True}) + task = TaskFactory.create_batch(20, project=project) + url = '/project/%s/tasks/browse?api_key=%s' \ + % (project.short_name, user.api_key) + res = self.app.get(url, follow_redirects=True) + assert res.status_code == 200, res.status_code + dom = BeautifulSoup(res.data) + # confirm that only "Tash #, Priority and Created are listed" + th_tags = dom.findAll("th", {"class": "sortable"}) + expected_columns = ["Task #", "Priority", "Created"] + assert len(th_tags) == len(expected_columns), th_tags + th_tag_1, th_tag_2, th_tag_3 = th_tags[0].text.strip(), th_tags[1].text.strip(), th_tags[2].text.strip() + assert th_tag_1 == expected_columns[0], f"found column {th_tag_1}, expected column {expected_columns[0]} not present" + assert th_tag_2 == expected_columns[1], f"found column {th_tag_3}, expected column {expected_columns[1]} not present" + assert th_tag_3 == expected_columns[2], f"found column {th_tag_3}, expected column {expected_columns[2]} not present" + @with_context def test_projects_account(self): """Test projecs on profiles are good."""