From 980efe11ab9b626f129f63bc456c3fb76b1c7260 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:34:21 +0530 Subject: [PATCH 01/10] FWF-4371: [Feature] Backend support to add diffecrent types of filter --- ...ilter_type_parent_filterid_filter_table.py | 40 ++++++++++++ .../src/formsflow_api/models/filter.py | 29 ++++++++- .../src/formsflow_api/resources/filter.py | 6 +- .../src/formsflow_api/schemas/filter.py | 15 +++++ .../src/formsflow_api/services/filter.py | 62 ++++++++++++++++--- 5 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py diff --git a/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py b/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py new file mode 100644 index 0000000000..4afa635beb --- /dev/null +++ b/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py @@ -0,0 +1,40 @@ +"""Add filter type and parent filter id to filter table + +Revision ID: 92bf83135905 +Revises: 524194732683 +Create Date: 2025-02-21 14:44:55.823150 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '92bf83135905' +down_revision = '524194732683' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + # Create the enum type + enum_type = postgresql.ENUM('TASK', 'ATTRIBUTE', 'DATE', name='FilterType') + enum_type.create(op.get_bind(), checkfirst=True) + op.add_column('filter', sa.Column('filter_type', enum_type, nullable=True, server_default='TASK')) + # Update existing rows to set the default value + op.execute("UPDATE filter SET filter_type = 'TASK' WHERE filter_type IS NULL") + # Alter the column to set `nullable=False` + op.alter_column('filter', 'filter_type', nullable=False) + op.create_index(op.f('ix_filter_filter_type'), 'filter', ['filter_type'], unique=False) + op.add_column('filter', sa.Column('parent_filter_id', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_filter_filter_type'), table_name='filter') + op.drop_column('filter', 'filter_type') + op.execute("DROP TYPE \"FilterType\"") + op.drop_column('filter', 'parent_filter_id') + # ### end Alembic commands ### diff --git a/forms-flow-api/src/formsflow_api/models/filter.py b/forms-flow-api/src/formsflow_api/models/filter.py index 95e193e347..c7df27ab08 100644 --- a/forms-flow-api/src/formsflow_api/models/filter.py +++ b/forms-flow-api/src/formsflow_api/models/filter.py @@ -2,11 +2,12 @@ from __future__ import annotations +from enum import Enum, unique from typing import List from formsflow_api_utils.utils.enums import FilterStatus from sqlalchemy import JSON, and_, asc, case, or_ -from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy.dialects.postgresql import ARRAY, ENUM from formsflow_api.models.base_model import BaseModel from formsflow_api.models.db import db @@ -14,6 +15,15 @@ from .audit_mixin import AuditDateTimeMixin, AuditUserMixin +@unique +class FilterType(Enum): + """Filter type enum.""" + + TASK = "TASK" + ATTRIBUTE = "ATTRIBUTE" + DATE = "DATE" + + class Filter(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model): """This class manages filter information.""" @@ -30,6 +40,13 @@ class Filter(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model): status = db.Column(db.String(10), nullable=True) task_visible_attributes = db.Column(JSON, nullable=True) order = db.Column(db.Integer, nullable=True, comment="Display order") + filter_type = db.Column( + ENUM(FilterType, name="FilterType"), + nullable=False, + default=FilterType.TASK, + index=True, + ) + parent_filter_id = db.Column(db.Integer, nullable=True) @classmethod def find_all_active_filters(cls, tenant: str = None) -> List[Filter]: @@ -67,23 +84,31 @@ def create_filter_from_dict(cls, filter_data: dict) -> Filter: filter_obj.task_visible_attributes = filter_data.get( "task_visible_attributes" ) + filter_obj.filter_type = filter_data.get("filter_type") + filter_obj.parent_filter_id = filter_data.get("parent_filter_id") filter_obj.save() return filter_obj return None @classmethod - def find_user_filters( + def find_user_filters( # pylint: disable=too-many-arguments, too-many-positional-arguments cls, roles: List[str] = None, user: str = None, tenant: str = None, admin: bool = False, + filter_type: str = None, + parent_filter_id: int = None, ): """Find active filters of the user.""" query = cls._auth_query( roles, user, tenant, admin, filter_empty_tenant_key=True ) query = query.filter(Filter.status == str(FilterStatus.ACTIVE.value)) + if filter_type: + query = query.filter(Filter.filter_type.in_(filter_type)) + if parent_filter_id: + query = query.filter(Filter.parent_filter_id == parent_filter_id) order_by_user_first = case((Filter.created_by == user, 1), else_=2) query = query.order_by( order_by_user_first, Filter.order, Filter.created_by, asc(Filter.name) diff --git a/forms-flow-api/src/formsflow_api/resources/filter.py b/forms-flow-api/src/formsflow_api/resources/filter.py index 4b68c723eb..c444d10930 100644 --- a/forms-flow-api/src/formsflow_api/resources/filter.py +++ b/forms-flow-api/src/formsflow_api/resources/filter.py @@ -176,7 +176,7 @@ def get(): Get all active filters of current reviewer user for requests with ```reviewer permission```. """ - response, status = FilterService.get_user_filters(), HTTPStatus.OK + response, status = FilterService.get_user_filters(request.args), HTTPStatus.OK return response, status @@ -187,7 +187,7 @@ class FilterResourceById(Resource): """Resource for managing filter by id.""" @staticmethod - @auth.has_one_of_roles([MANAGE_ALL_FILTERS]) + @auth.has_one_of_roles([MANAGE_ALL_FILTERS, VIEW_FILTERS]) @profiletime @API.doc( responses={ @@ -204,7 +204,7 @@ def get(filter_id: int): Get filter details corresponding to a filter id for requests with ```REVIEWER_GROUP``` permission. """ filter_result = FilterService.get_filter_by_id(filter_id) - response, status = filter_schema.dump(filter_result), HTTPStatus.OK + response, status = filter_result, HTTPStatus.OK return response, status diff --git a/forms-flow-api/src/formsflow_api/schemas/filter.py b/forms-flow-api/src/formsflow_api/schemas/filter.py index d616f52cef..abd449fdbb 100644 --- a/forms-flow-api/src/formsflow_api/schemas/filter.py +++ b/forms-flow-api/src/formsflow_api/schemas/filter.py @@ -42,3 +42,18 @@ class Meta: # pylint: disable=too-few-public-methods isMyTasksEnabled = fields.Bool(load_only=True) isTasksForCurrentUserGroupsEnabled = fields.Bool(load_only=True) order = fields.Int(data_key="order", allow_none=True) + filter_type = fields.Method( + "get_filter_type", + deserialize="load_filter_type", + data_key="filterType", + allow_none=True, + ) + parent_filter_id = fields.Int(data_key="parentFilterId", allow_none=True) + + def get_filter_type(self, obj): + """This method is to get the filter type.""" + return obj.filter_type.value + + def load_filter_type(self, value): + """This method is to load the filter type.""" + return value.upper() if value else None diff --git a/forms-flow-api/src/formsflow_api/services/filter.py b/forms-flow-api/src/formsflow_api/services/filter.py index df11f24e7a..057c56caf0 100644 --- a/forms-flow-api/src/formsflow_api/services/filter.py +++ b/forms-flow-api/src/formsflow_api/services/filter.py @@ -45,9 +45,21 @@ def create_filter(filter_payload, **kwargs): filter_data = Filter.create_filter_from_dict(filter_payload) return filter_schema.dump(filter_data) + @staticmethod + def get_attribute_filters(filter_data, filter_type): + """Get attribute filters.""" + attribute_filters = {} + if "TASK" in filter_type: + attribute_filters = { + f["parentFilterId"]: f + for f in filter_data + if f["filterType"] == "ATTRIBUTE" + } + return attribute_filters + @staticmethod @user_context - def get_user_filters(**kwargs): + def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals """Get filters for the user.""" user: UserContext = kwargs["user"] tenant_key = user.tenant_key @@ -99,28 +111,50 @@ def get_user_filters(**kwargs): }, ) filter_obj.save() - + # If filter type is not provided, get all filters for type task and attribute + filter_type = ( + [request_args.get("filterType").upper()] + if request_args.get("filterType") + else ["TASK", "ATTRIBUTE"] + ) filters = Filter.find_user_filters( roles=user.group_or_roles, user=user.user_name, tenant=tenant_key, admin=ADMIN in user.roles, + filter_type=filter_type, ) filter_data = filter_schema.dump(filters, many=True) default_variables = [ {"name": "applicationId", "label": "Submission Id"}, {"name": "formName", "label": "Form Name"}, ] + attribute_filters = FilterService.get_attribute_filters( + filter_data, filter_type + ) + + organized_filters = [] # User who created the filter or admin have edit permission. for filter_item in filter_data: filter_item["editPermission"] = ( filter_item["createdBy"] == user.user_name or ADMIN in user.roles ) - # Check and add default variables if not present - filter_item["variables"] = filter_item["variables"] or [] - filter_item["variables"] += [ - var for var in default_variables if var not in filter_item["variables"] - ] + + if filter_item["filterType"] == "TASK": + # Find all attribute filters that have this task filter as parent + filter_item["attributeFilters"] = attribute_filters.get( + filter_item["id"] + ) + # Check and add default variables if not present + filter_item["variables"] = filter_item["variables"] or [] + filter_item["variables"] += [ + var + for var in default_variables + if var not in filter_item["variables"] + ] + organized_filters.append(filter_item) + filter_data = organized_filters + response = {"filters": filter_data} # get user default filter user_data = User.get_user_by_user_name(user_name=user.user_name) @@ -141,7 +175,19 @@ def get_filter_by_id(filter_id, **kwargs): admin=ADMIN in user.roles, ) if filter_result: - return filter_result + response = filter_schema.dump(filter_result) + attribute_filters = Filter.find_user_filters( + roles=user.group_or_roles, + user=user.user_name, + tenant=tenant_key, + admin=ADMIN in user.roles, + filter_type=["ATTRIBUTE"], + parent_filter_id=response["id"], + ) + response["attributeFilters"] = filter_schema.dump( + attribute_filters, many=True + ) + return response raise BusinessException(BusinessErrorCode.FILTER_NOT_FOUND) @staticmethod From cbef3e85fcd36fc478b2ee263b96b276dc7e46c5 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:20:07 +0530 Subject: [PATCH 02/10] FWF-4371: [Feature] Update in filter apis --- .../src/formsflow_api/models/__init__.py | 2 +- .../src/formsflow_api/models/filter.py | 2 +- .../src/formsflow_api/services/filter.py | 55 +++++++++---------- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/forms-flow-api/src/formsflow_api/models/__init__.py b/forms-flow-api/src/formsflow_api/models/__init__.py index 7989f4948a..83fa3e084f 100644 --- a/forms-flow-api/src/formsflow_api/models/__init__.py +++ b/forms-flow-api/src/formsflow_api/models/__init__.py @@ -6,7 +6,7 @@ from .base_model import BaseModel from .db import db, ma from .draft import Draft -from .filter import Filter +from .filter import Filter, FilterType from .form_history_logs import FormHistory from .form_process_mapper import FormProcessMapper from .process import Process, ProcessStatus, ProcessType diff --git a/forms-flow-api/src/formsflow_api/models/filter.py b/forms-flow-api/src/formsflow_api/models/filter.py index c7df27ab08..cb9d891c61 100644 --- a/forms-flow-api/src/formsflow_api/models/filter.py +++ b/forms-flow-api/src/formsflow_api/models/filter.py @@ -106,7 +106,7 @@ def find_user_filters( # pylint: disable=too-many-arguments, too-many-positiona ) query = query.filter(Filter.status == str(FilterStatus.ACTIVE.value)) if filter_type: - query = query.filter(Filter.filter_type.in_(filter_type)) + query = query.filter(Filter.filter_type == filter_type) if parent_filter_id: query = query.filter(Filter.parent_filter_id == parent_filter_id) order_by_user_first = case((Filter.created_by == user, 1), else_=2) diff --git a/forms-flow-api/src/formsflow_api/services/filter.py b/forms-flow-api/src/formsflow_api/services/filter.py index 057c56caf0..d2d68d6220 100644 --- a/forms-flow-api/src/formsflow_api/services/filter.py +++ b/forms-flow-api/src/formsflow_api/services/filter.py @@ -6,7 +6,7 @@ from formsflow_api_utils.utils.user_context import UserContext, user_context from formsflow_api.constants import BusinessErrorCode -from formsflow_api.models import Filter, User +from formsflow_api.models import Filter, FilterType, User from formsflow_api.schemas import FilterSchema filter_schema = FilterSchema() @@ -46,15 +46,21 @@ def create_filter(filter_payload, **kwargs): return filter_schema.dump(filter_data) @staticmethod - def get_attribute_filters(filter_data, filter_type): - """Get attribute filters.""" + def get_attribute_filters(filter_type, user): + """Get attribute filters & create lookup table with parent filter id.""" attribute_filters = {} - if "TASK" in filter_type: - attribute_filters = { - f["parentFilterId"]: f - for f in filter_data - if f["filterType"] == "ATTRIBUTE" - } + print(filter_type) + if filter_type == FilterType.TASK: + filters = Filter.find_user_filters( + roles=user.group_or_roles, + user=user.user_name, + tenant=user.tenant_key, + admin=ADMIN in user.roles, + filter_type=FilterType.ATTRIBUTE, + ) + filters = filter_schema.dump(filters, many=True) + attribute_filters = {f["parentFilterId"]: f for f in filters} + print(attribute_filters) return attribute_filters @staticmethod @@ -113,9 +119,9 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals filter_obj.save() # If filter type is not provided, get all filters for type task and attribute filter_type = ( - [request_args.get("filterType").upper()] + request_args.get("filterType").upper() if request_args.get("filterType") - else ["TASK", "ATTRIBUTE"] + else FilterType.TASK ) filters = Filter.find_user_filters( roles=user.group_or_roles, @@ -129,32 +135,23 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals {"name": "applicationId", "label": "Submission Id"}, {"name": "formName", "label": "Form Name"}, ] - attribute_filters = FilterService.get_attribute_filters( - filter_data, filter_type - ) + attribute_filters = FilterService.get_attribute_filters(filter_type, user) - organized_filters = [] # User who created the filter or admin have edit permission. for filter_item in filter_data: filter_item["editPermission"] = ( filter_item["createdBy"] == user.user_name or ADMIN in user.roles ) - - if filter_item["filterType"] == "TASK": - # Find all attribute filters that have this task filter as parent + # Check and add default variables if not present + filter_item["variables"] = filter_item["variables"] or [] + filter_item["variables"] += [ + var for var in default_variables if var not in filter_item["variables"] + ] + # Return attribute filters for task filters + if filter_item["filterType"] == FilterType.TASK.value: filter_item["attributeFilters"] = attribute_filters.get( filter_item["id"] ) - # Check and add default variables if not present - filter_item["variables"] = filter_item["variables"] or [] - filter_item["variables"] += [ - var - for var in default_variables - if var not in filter_item["variables"] - ] - organized_filters.append(filter_item) - filter_data = organized_filters - response = {"filters": filter_data} # get user default filter user_data = User.get_user_by_user_name(user_name=user.user_name) @@ -181,7 +178,7 @@ def get_filter_by_id(filter_id, **kwargs): user=user.user_name, tenant=tenant_key, admin=ADMIN in user.roles, - filter_type=["ATTRIBUTE"], + filter_type=FilterType.ATTRIBUTE, parent_filter_id=response["id"], ) response["attributeFilters"] = filter_schema.dump( From 4f5551791e8a0d061f5bf1f62b1f1598aef6a21d Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:22:33 +0530 Subject: [PATCH 03/10] FWF-4371: [Feature] Update in filter list api --- .../src/formsflow_api/resources/filter.py | 6 ++++- .../src/formsflow_api/services/filter.py | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/forms-flow-api/src/formsflow_api/resources/filter.py b/forms-flow-api/src/formsflow_api/resources/filter.py index c444d10930..13febe5604 100644 --- a/forms-flow-api/src/formsflow_api/resources/filter.py +++ b/forms-flow-api/src/formsflow_api/resources/filter.py @@ -57,6 +57,8 @@ "users": fields.List( fields.String(), description="Authorized Users to the filter" ), + "parentFilterId": fields.Integer(description="Parent filter id"), + "filterType": fields.String(description="Filter type"), }, ) filter_response = API.inherit( @@ -143,7 +145,9 @@ def post(): "showUndefinedVariable":false }, "users": [], - "roles": ["/formsflow/formsflow-reviewer"] + "roles": ["/formsflow/formsflow-reviewer"], + "parentFilterId": null, + "filterType": "TASK" } ``` """ diff --git a/forms-flow-api/src/formsflow_api/services/filter.py b/forms-flow-api/src/formsflow_api/services/filter.py index d2d68d6220..616a339a25 100644 --- a/forms-flow-api/src/formsflow_api/services/filter.py +++ b/forms-flow-api/src/formsflow_api/services/filter.py @@ -49,7 +49,6 @@ def create_filter(filter_payload, **kwargs): def get_attribute_filters(filter_type, user): """Get attribute filters & create lookup table with parent filter id.""" attribute_filters = {} - print(filter_type) if filter_type == FilterType.TASK: filters = Filter.find_user_filters( roles=user.group_or_roles, @@ -59,10 +58,22 @@ def get_attribute_filters(filter_type, user): filter_type=FilterType.ATTRIBUTE, ) filters = filter_schema.dump(filters, many=True) - attribute_filters = {f["parentFilterId"]: f for f in filters} - print(attribute_filters) + # Retain only the first filter for each parentFilterId + for f in filters: + parent_filter_id = f["parentFilterId"] + if parent_filter_id not in attribute_filters: + attribute_filters[parent_filter_id] = f return attribute_filters + @staticmethod + def set_attribute_filters(filter_item, attribute_filters): + """Set attribute filters for a filter item if it is of type TASK.""" + if filter_item["filterType"] == FilterType.TASK.value: + matched_filter = attribute_filters.get(filter_item["id"]) + filter_item["attributeFilters"] = ( + [matched_filter] if matched_filter is not None else [] + ) + @staticmethod @user_context def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals @@ -148,10 +159,7 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals var for var in default_variables if var not in filter_item["variables"] ] # Return attribute filters for task filters - if filter_item["filterType"] == FilterType.TASK.value: - filter_item["attributeFilters"] = attribute_filters.get( - filter_item["id"] - ) + FilterService.set_attribute_filters(filter_item, attribute_filters) response = {"filters": filter_data} # get user default filter user_data = User.get_user_by_user_name(user_name=user.user_name) From 1abe7dfc021e3ee680a48f9a316b1add75c5ba06 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:01:03 +0530 Subject: [PATCH 04/10] FWF-4371: [Feature] Added testcase --- .../src/formsflow_api/resources/filter.py | 7 +++ forms-flow-api/tests/unit/api/test_filter.py | 62 +++++++++++++++++++ forms-flow-api/tests/utilities/base_test.py | 4 ++ 3 files changed, 73 insertions(+) diff --git a/forms-flow-api/src/formsflow_api/resources/filter.py b/forms-flow-api/src/formsflow_api/resources/filter.py index 13febe5604..fe2673e814 100644 --- a/forms-flow-api/src/formsflow_api/resources/filter.py +++ b/forms-flow-api/src/formsflow_api/resources/filter.py @@ -168,6 +168,13 @@ class UsersFilterList(Resource): @auth.has_one_of_roles([MANAGE_ALL_FILTERS, VIEW_FILTERS]) @profiletime @API.doc( + params={ + "filterType": { + "in": "query", + "description": "Filter type - TASK/DATE/ATTRIBUTE", + "default": "TASK", + } + }, responses={ 200: "OK:- Successful request.", 403: "FORBIDDEN:- Permission denied", diff --git a/forms-flow-api/tests/unit/api/test_filter.py b/forms-flow-api/tests/unit/api/test_filter.py index efd97cda40..c5fd03739f 100644 --- a/forms-flow-api/tests/unit/api/test_filter.py +++ b/forms-flow-api/tests/unit/api/test_filter.py @@ -147,3 +147,65 @@ def test_get_user_filters_by_order(app, client, session, jwt): assert len(response.json.get("filters")) == 2 assert response.json.get("filters")[0].get("name") == "Reviewer Task" assert response.json.get("filters")[1].get("name") == "Clerk Task" + + +def test_attribute_filter(app, client, session, jwt): + """Test attribute filter with valid payload.""" + token = get_token(jwt, role=CREATE_FILTERS, username="reviewer") + headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} + # Create task filter + response = client.post( + "/filter", headers=headers, json=get_filter_payload(name="Task filter1", roles=["formsflow-reviewer"]) + ) + assert response.status_code == 201 + assert response.json.get("id") is not None + assert response.json.get("name") == "Task filter1" + assert response.json.get("filterType") == "TASK" + assert response.json.get("parentFilterId") is None + + parent_filter_id = response.json.get("id") + # Create attribute filter for the task filter + response = client.post( + "/filter", headers=headers, json=get_filter_payload(name="Attribute filter1", roles=["formsflow-reviewer"], parent_filter_id=parent_filter_id, filter_type="ATTRIBUTE") + ) + assert response.status_code == 201 + assert response.json.get("id") is not None + assert response.json.get("name") == "Attribute filter1" + assert response.json.get("filterType") == "ATTRIBUTE" + + token = get_token(jwt, role=VIEW_FILTERS, username="reviewer") + headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} + # Get task filters for the user + response = client.get("/filter/user", headers=headers) + assert response.status_code == 200 + assert len(response.json.get("filters")) == 1 + assert response.json.get("filters")[0].get("name") == "Task filter1" + assert response.json.get("filters")[0].get("attributeFilters") + + # Get filter by id + response = client.get(f"/filter/{parent_filter_id}", headers=headers) + assert response.status_code == 200 + assert response.json.get("name") == "Task filter1" + assert response.json.get("attributeFilters") + + +def test_date_filter(app, client, session, jwt): + """Test date filter with valid payload.""" + token = get_token(jwt, role=CREATE_FILTERS, username="reviewer") + headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} + # Create task filter + response = client.post( + "/filter", headers=headers, json=get_filter_payload(name="Date filter1", roles=["formsflow-reviewer"], filter_type="DATE") + ) + assert response.status_code == 201 + assert response.json.get("id") is not None + assert response.json.get("name") == "Date filter1" + assert response.json.get("filterType") == "DATE" + + token = get_token(jwt, role=VIEW_FILTERS, username="reviewer") + headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} + # Get task filters for the user + response = client.get("/filter/user?filterType=DATE", headers=headers) + assert response.status_code == 200 + assert len(response.json.get("filters")) == 1 + assert response.json.get("filters")[0].get("name") == "Date filter1" diff --git a/forms-flow-api/tests/utilities/base_test.py b/forms-flow-api/tests/utilities/base_test.py index 041cdda90e..d816bbef40 100644 --- a/forms-flow-api/tests/utilities/base_test.py +++ b/forms-flow-api/tests/utilities/base_test.py @@ -465,6 +465,8 @@ def get_filter_payload( roles: list = [], users: list = [], order: int = None, + filter_type: str = "TASK", + parent_filter_id: int = None, ): """Return filter create payload.""" return { @@ -486,6 +488,8 @@ def get_filter_payload( "priority": True, "groups": True, }, + "parentFilterId": parent_filter_id, + "filterType": filter_type, } From 0bee5498a1b1b46d90bb10d1d02ef5b36479feb8 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:26:46 +0530 Subject: [PATCH 05/10] FWF-4371: [Feature] Added changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b70c6810..2b8bd14e8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ Mark items as `Added`, `Changed`, `Fixed`, `Modified`, `Removed`, `Untested Fea * Added a new column, is_draft, to the application table to identify draft entries. * Added Alembic script to update existing active drafts by setting is_draft to true in the application table. * Added the includeSubmissionsCount=true parameter to the form list endpoint to include the submissions count. +* Added columns filter_type, parent_filter_id to the filter table. +* Added script to migrate existing filters to TASK filter type. +* Added filter_type parameter to filter list `/filter/user` endpoint to filter by filter type. ## 7.0.0 - 2025-01-10 From 812f1fbaeae6e418e8964a85283a794ffacfac5c Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:19:24 +0530 Subject: [PATCH 06/10] FWF-4371: [Feature] Included edit permission to attribute filter response --- .../src/formsflow_api/services/filter.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/forms-flow-api/src/formsflow_api/services/filter.py b/forms-flow-api/src/formsflow_api/services/filter.py index 616a339a25..83b05a7788 100644 --- a/forms-flow-api/src/formsflow_api/services/filter.py +++ b/forms-flow-api/src/formsflow_api/services/filter.py @@ -61,6 +61,7 @@ def get_attribute_filters(filter_type, user): # Retain only the first filter for each parentFilterId for f in filters: parent_filter_id = f["parentFilterId"] + FilterService.set_filter_edit_permission(f, user) if parent_filter_id not in attribute_filters: attribute_filters[parent_filter_id] = f return attribute_filters @@ -74,6 +75,13 @@ def set_attribute_filters(filter_item, attribute_filters): [matched_filter] if matched_filter is not None else [] ) + @staticmethod + def set_filter_edit_permission(filter_item, user): + """Set filter edit permission for a filter item.""" + filter_item["editPermission"] = ( + filter_item["createdBy"] == user.user_name or ADMIN in user.roles + ) + @staticmethod @user_context def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals @@ -150,9 +158,7 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals # User who created the filter or admin have edit permission. for filter_item in filter_data: - filter_item["editPermission"] = ( - filter_item["createdBy"] == user.user_name or ADMIN in user.roles - ) + FilterService.set_filter_edit_permission(filter_item, user) # Check and add default variables if not present filter_item["variables"] = filter_item["variables"] or [] filter_item["variables"] += [ @@ -192,6 +198,8 @@ def get_filter_by_id(filter_id, **kwargs): response["attributeFilters"] = filter_schema.dump( attribute_filters, many=True ) + for filter_item in response["attributeFilters"]: + FilterService.set_filter_edit_permission(filter_item, user) return response raise BusinessException(BusinessErrorCode.FILTER_NOT_FOUND) From bc8383c195b3cdeb8934452f5603c353c5bdcc3b Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:51:38 +0530 Subject: [PATCH 07/10] FWF-4371: Removed attribute list from filter list --- .../src/formsflow_api/services/filter.py | 35 +------------------ forms-flow-api/tests/unit/api/test_filter.py | 23 ------------ 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/forms-flow-api/src/formsflow_api/services/filter.py b/forms-flow-api/src/formsflow_api/services/filter.py index 83b05a7788..6bc1b613e6 100644 --- a/forms-flow-api/src/formsflow_api/services/filter.py +++ b/forms-flow-api/src/formsflow_api/services/filter.py @@ -45,36 +45,6 @@ def create_filter(filter_payload, **kwargs): filter_data = Filter.create_filter_from_dict(filter_payload) return filter_schema.dump(filter_data) - @staticmethod - def get_attribute_filters(filter_type, user): - """Get attribute filters & create lookup table with parent filter id.""" - attribute_filters = {} - if filter_type == FilterType.TASK: - filters = Filter.find_user_filters( - roles=user.group_or_roles, - user=user.user_name, - tenant=user.tenant_key, - admin=ADMIN in user.roles, - filter_type=FilterType.ATTRIBUTE, - ) - filters = filter_schema.dump(filters, many=True) - # Retain only the first filter for each parentFilterId - for f in filters: - parent_filter_id = f["parentFilterId"] - FilterService.set_filter_edit_permission(f, user) - if parent_filter_id not in attribute_filters: - attribute_filters[parent_filter_id] = f - return attribute_filters - - @staticmethod - def set_attribute_filters(filter_item, attribute_filters): - """Set attribute filters for a filter item if it is of type TASK.""" - if filter_item["filterType"] == FilterType.TASK.value: - matched_filter = attribute_filters.get(filter_item["id"]) - filter_item["attributeFilters"] = ( - [matched_filter] if matched_filter is not None else [] - ) - @staticmethod def set_filter_edit_permission(filter_item, user): """Set filter edit permission for a filter item.""" @@ -136,7 +106,7 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals }, ) filter_obj.save() - # If filter type is not provided, get all filters for type task and attribute + # If filter type is not provided, get all filters for type task filter_type = ( request_args.get("filterType").upper() if request_args.get("filterType") @@ -154,7 +124,6 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals {"name": "applicationId", "label": "Submission Id"}, {"name": "formName", "label": "Form Name"}, ] - attribute_filters = FilterService.get_attribute_filters(filter_type, user) # User who created the filter or admin have edit permission. for filter_item in filter_data: @@ -164,8 +133,6 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals filter_item["variables"] += [ var for var in default_variables if var not in filter_item["variables"] ] - # Return attribute filters for task filters - FilterService.set_attribute_filters(filter_item, attribute_filters) response = {"filters": filter_data} # get user default filter user_data = User.get_user_by_user_name(user_name=user.user_name) diff --git a/forms-flow-api/tests/unit/api/test_filter.py b/forms-flow-api/tests/unit/api/test_filter.py index c5fd03739f..f72a66dc1a 100644 --- a/forms-flow-api/tests/unit/api/test_filter.py +++ b/forms-flow-api/tests/unit/api/test_filter.py @@ -180,32 +180,9 @@ def test_attribute_filter(app, client, session, jwt): assert response.status_code == 200 assert len(response.json.get("filters")) == 1 assert response.json.get("filters")[0].get("name") == "Task filter1" - assert response.json.get("filters")[0].get("attributeFilters") # Get filter by id response = client.get(f"/filter/{parent_filter_id}", headers=headers) assert response.status_code == 200 assert response.json.get("name") == "Task filter1" assert response.json.get("attributeFilters") - - -def test_date_filter(app, client, session, jwt): - """Test date filter with valid payload.""" - token = get_token(jwt, role=CREATE_FILTERS, username="reviewer") - headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} - # Create task filter - response = client.post( - "/filter", headers=headers, json=get_filter_payload(name="Date filter1", roles=["formsflow-reviewer"], filter_type="DATE") - ) - assert response.status_code == 201 - assert response.json.get("id") is not None - assert response.json.get("name") == "Date filter1" - assert response.json.get("filterType") == "DATE" - - token = get_token(jwt, role=VIEW_FILTERS, username="reviewer") - headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} - # Get task filters for the user - response = client.get("/filter/user?filterType=DATE", headers=headers) - assert response.status_code == 200 - assert len(response.json.get("filters")) == 1 - assert response.json.get("filters")[0].get("name") == "Date filter1" From f65245f567d3bf4d3b88e8d390fcbaedb225fa7a Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:19:04 +0530 Subject: [PATCH 08/10] FWF-4371: Removed request param filterType from filter list api --- forms-flow-api/src/formsflow_api/resources/filter.py | 9 +-------- forms-flow-api/src/formsflow_api/services/filter.py | 10 ++-------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/forms-flow-api/src/formsflow_api/resources/filter.py b/forms-flow-api/src/formsflow_api/resources/filter.py index fe2673e814..c50345957e 100644 --- a/forms-flow-api/src/formsflow_api/resources/filter.py +++ b/forms-flow-api/src/formsflow_api/resources/filter.py @@ -168,13 +168,6 @@ class UsersFilterList(Resource): @auth.has_one_of_roles([MANAGE_ALL_FILTERS, VIEW_FILTERS]) @profiletime @API.doc( - params={ - "filterType": { - "in": "query", - "description": "Filter type - TASK/DATE/ATTRIBUTE", - "default": "TASK", - } - }, responses={ 200: "OK:- Successful request.", 403: "FORBIDDEN:- Permission denied", @@ -187,7 +180,7 @@ def get(): Get all active filters of current reviewer user for requests with ```reviewer permission```. """ - response, status = FilterService.get_user_filters(request.args), HTTPStatus.OK + response, status = FilterService.get_user_filters(), HTTPStatus.OK return response, status diff --git a/forms-flow-api/src/formsflow_api/services/filter.py b/forms-flow-api/src/formsflow_api/services/filter.py index 6bc1b613e6..9b3351e38d 100644 --- a/forms-flow-api/src/formsflow_api/services/filter.py +++ b/forms-flow-api/src/formsflow_api/services/filter.py @@ -54,7 +54,7 @@ def set_filter_edit_permission(filter_item, user): @staticmethod @user_context - def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals + def get_user_filters(**kwargs): # pylint: disable=too-many-locals """Get filters for the user.""" user: UserContext = kwargs["user"] tenant_key = user.tenant_key @@ -106,18 +106,12 @@ def get_user_filters(request_args, **kwargs): # pylint: disable=too-many-locals }, ) filter_obj.save() - # If filter type is not provided, get all filters for type task - filter_type = ( - request_args.get("filterType").upper() - if request_args.get("filterType") - else FilterType.TASK - ) filters = Filter.find_user_filters( roles=user.group_or_roles, user=user.user_name, tenant=tenant_key, admin=ADMIN in user.roles, - filter_type=filter_type, + filter_type=FilterType.TASK, ) filter_data = filter_schema.dump(filters, many=True) default_variables = [ From bb9b92431e23458f5b71137d6d2daae92f911f55 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:37:02 +0530 Subject: [PATCH 09/10] FWF-4371: [Feature] Added index for parent_filter_id --- ...83135905_add_filter_type_parent_filterid_filter_table.py | 6 +++++- forms-flow-api/src/formsflow_api/models/filter.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py b/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py index 4afa635beb..f3963933c9 100644 --- a/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py +++ b/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py @@ -1,4 +1,5 @@ """Add filter type and parent filter id to filter table + Update existing filter to type TASK Revision ID: 92bf83135905 Revises: 524194732683 @@ -22,17 +23,20 @@ def upgrade(): enum_type = postgresql.ENUM('TASK', 'ATTRIBUTE', 'DATE', name='FilterType') enum_type.create(op.get_bind(), checkfirst=True) op.add_column('filter', sa.Column('filter_type', enum_type, nullable=True, server_default='TASK')) - # Update existing rows to set the default value + # Update existing rows to set the default filter type as task op.execute("UPDATE filter SET filter_type = 'TASK' WHERE filter_type IS NULL") # Alter the column to set `nullable=False` op.alter_column('filter', 'filter_type', nullable=False) op.create_index(op.f('ix_filter_filter_type'), 'filter', ['filter_type'], unique=False) op.add_column('filter', sa.Column('parent_filter_id', sa.Integer(), nullable=True)) + op.create_index(op.f('ix_filter_parent_filter_id'), 'filter', ['parent_filter_id'], unique=False) + # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_filter_parent_filter_id'), table_name='filter') op.drop_index(op.f('ix_filter_filter_type'), table_name='filter') op.drop_column('filter', 'filter_type') op.execute("DROP TYPE \"FilterType\"") diff --git a/forms-flow-api/src/formsflow_api/models/filter.py b/forms-flow-api/src/formsflow_api/models/filter.py index cb9d891c61..e2e53b8d60 100644 --- a/forms-flow-api/src/formsflow_api/models/filter.py +++ b/forms-flow-api/src/formsflow_api/models/filter.py @@ -46,7 +46,7 @@ class Filter(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model): default=FilterType.TASK, index=True, ) - parent_filter_id = db.Column(db.Integer, nullable=True) + parent_filter_id = db.Column(db.Integer, nullable=True, index=True) @classmethod def find_all_active_filters(cls, tenant: str = None) -> List[Filter]: From 684f850a946822a56d7a8e11bd49dd5ee4ef1d13 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:06:10 +0530 Subject: [PATCH 10/10] FWF-4371: [Feature] Updated changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b8bd14e8c..965ec01ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,6 @@ Mark items as `Added`, `Changed`, `Fixed`, `Modified`, `Removed`, `Untested Fea * Added the includeSubmissionsCount=true parameter to the form list endpoint to include the submissions count. * Added columns filter_type, parent_filter_id to the filter table. * Added script to migrate existing filters to TASK filter type. -* Added filter_type parameter to filter list `/filter/user` endpoint to filter by filter type. ## 7.0.0 - 2025-01-10