Repository: AOT-Technologies/forms-flow-ai
Pull request #2596: Feature/fwf 4371 Backend support different filter types
Author: login: auslin-aot
Labels: name: enhancement
description: New feature or request
Created: 2025-02-25T06:16:55Z | Merged: 2025-03-10T11:37:52Z

# Issue Tracking

JIRA: 
Issue Type: BUG/ FEATURE
https://aottech.atlassian.net/browse/FWF-4371

# Changes

- Added column filter_type  with values TASK (default value), ATTRIBUTE, DATE
- Updated existing filter to type TASK
- Added a new column called parent_filter_id 
- Changes are done in the get filter by id API to return attribute filters corresponding to the task filter

# Screenshots 
![image](https://github.com/user-attachments/assets/006ad09b-f7c0-492a-a86f-0d2327d002f6)
![image](https://github.com/user-attachments/assets/f6294ba1-ff06-4899-bb0c-b8ee61653d01)


# Checklist
- [X] Updated changelog
- [X] Added meaningful title for pull request


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0b70c6810..965ec01ae4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@ 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.
 
 ## 7.0.0 - 2025-01-10
 
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..4c6c3eb6f9
--- /dev/null
+++ b/forms-flow-api/migrations/versions/92bf83135905_add_filter_type_parent_filterid_filter_table.py
@@ -0,0 +1,44 @@
+"""Add filter type and parent filter id to filter table
+   Update existing filter to type TASK
+
+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', 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 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\"")
+    op.drop_column('filter', 'parent_filter_id')
+    # ### end Alembic commands ###
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 95e193e347..1bc8b31478 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,14 @@
 from .audit_mixin import AuditDateTimeMixin, AuditUserMixin
 
 
+@unique
+class FilterType(Enum):
+    """Filter type enum."""
+
+    TASK = "TASK"
+    ATTRIBUTE = "ATTRIBUTE"
+
+
 class Filter(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model):
     """This class manages filter information."""
 
@@ -30,6 +39,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, index=True)
 
     @classmethod
     def find_all_active_filters(cls, tenant: str = None) -> List[Filter]:
@@ -67,23 +83,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 == 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..c50345957e 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"
         }
         ```
         """
@@ -187,7 +191,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 +208,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..9b3351e38d 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()
@@ -45,9 +45,16 @@ def create_filter(filter_payload, **kwargs):
         filter_data = Filter.create_filter_from_dict(filter_payload)
         return filter_schema.dump(filter_data)
 
+    @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(**kwargs):
+    def get_user_filters(**kwargs):  # pylint: disable=too-many-locals
         """Get filters for the user."""
         user: UserContext = kwargs["user"]
         tenant_key = user.tenant_key
@@ -99,23 +106,22 @@ def get_user_filters(**kwargs):
                     },
                 )
                 filter_obj.save()
-
         filters = Filter.find_user_filters(
             roles=user.group_or_roles,
             user=user.user_name,
             tenant=tenant_key,
             admin=ADMIN in user.roles,
+            filter_type=FilterType.TASK,
         )
         filter_data = filter_schema.dump(filters, many=True)
         default_variables = [
             {"name": "applicationId", "label": "Submission Id"},
             {"name": "formName", "label": "Form Name"},
         ]
+
         # 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"] += [
@@ -141,7 +147,21 @@ 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=FilterType.ATTRIBUTE,
+                parent_filter_id=response["id"],
+            )
+            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)
 
     @staticmethod
diff --git a/forms-flow-api/tests/unit/api/test_filter.py b/forms-flow-api/tests/unit/api/test_filter.py
index efd97cda40..f72a66dc1a 100644
--- a/forms-flow-api/tests/unit/api/test_filter.py
+++ b/forms-flow-api/tests/unit/api/test_filter.py
@@ -147,3 +147,42 @@ 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"
+
+    # 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")
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,
     }
 
 


Reviews:
user: login: sumesh-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T18:38:01Z
user: login: arun-s-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T07:03:52Z
user: login: sumesh-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T21:37:33Z
user: login: sumesh-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-28T21:39:08Z
user: login: arun-s-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T04:45:11Z
user: login: auslin-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T07:13:45Z
user: login: sumesh-aot
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T18:00:17Z
user: login: arun-s-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T11:20:35Z
user: login: arun-s-aot
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T11:34:41Z
user: login: auslin-aot
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T11:37:13Z

Review comments:
diff_hunk: @@ -121,6 +158,8 @@ def get_user_filters(**kwargs):
             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)
path: forms-flow-api/src/formsflow_api/services/filter.py
user: login: sumesh-aot
body: Do we need this here ?
Can't we have 2 endpoints which I think will be easy to handle on the UI ?
Please note that there are other changes coming in to the picture on show/hide and re-ordering. So I feel it would be better to keep each filter as separate response. 
Endpoints would be as simple as /filters/TASK 
/filters/ATTRIBUTE?parenFilterId=<>

What do you think ?
created_at: 2025-02-26T18:37:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +158,8 @@ def get_user_filters(**kwargs):
             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)
path: forms-flow-api/src/formsflow_api/services/filter.py
user: login: arun-s-aot
body: The approach what auslin has followed is to quickly load the initial data when user comes to review screen. 
1) Suppose if we have more number of attribute filters and task filters, if we go with task api which will also have all attributes inside it , TIME TO GET THE RESPONSE WILL BE MORE and only after the response is received , the task list will be refreshed and shown. 

2) If we go with initially loading the TASK FILTER list , and ATTRIBUTE FILTER list after that , Task listing will have to wait for 2 APIs ( synchoronous ) to complete inorder to refresh and show. 

Inorder to mitigate the above 2 issues, we thought of returning the TASK filter initially with either the first ATTRIBUTE filter alone in an array , or already selected 1 ATTRIBUTE filter as part of TASK filter and parallely we will call the full ATTRIBUTE filter endpoint so that user wont feel the delay in screen. By the time user plans to change the attibute filter, the ATTRIBUTE filter list will be available to frontend 

We will definitely need attibute listing GET endpoint as a separate one so that if we change the TASK filter, updated attributes can be retrieved. Any other use cases like show/hide and re-ordering might need this endpoint.

@sumesh-aot Please share your thoughts
cc @auslin-aot 

created_at: 2025-02-28T07:03:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +158,8 @@ def get_user_filters(**kwargs):
             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)
path: forms-flow-api/src/formsflow_api/services/filter.py
user: login: sumesh-aot
body: Understood.
Adding just one filter to the list is misrepresentation of the resource. 
- For 7.1.0 : we will only default the main filter
- Keep the attribute filter selection as empty on first load
- Let the user select the attribute filter based on their need
- On change of task filter, save it as the default filter
- Keep the main filter, attribute filter stored in state, so that if the user navigates on the same session and comes back they will see the last worked state

If we go with this, we wouldn't need the attributes filter to load initially right ?
created_at: 2025-02-28T21:37:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +158,8 @@ def get_user_filters(**kwargs):
             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)
path: forms-flow-api/src/formsflow_api/services/filter.py
user: login: sumesh-aot
body: Below are the agreed scope for 7.1.0
- User can save TASK filter and ATTRIBUTES filter
- Date filter cannot be saved
- When a reviewer selects the TASK filter, we will save it as the default filter for the user
- We won't save the ATTRIBUTES filter on the selection
- Numbers displayed on the label would be the total number of tasks under TASK filter
- By default only TASK filter will be displayed, user will need to select ATTRIBUTES filter and put in dates if needed
- On same session, state will be maintained so that user can navigate and come back to the previous page

created_at: 2025-02-28T21:39:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +158,8 @@ def get_user_filters(**kwargs):
             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)
path: forms-flow-api/src/formsflow_api/services/filter.py
user: login: arun-s-aot
body: Thanks for the clarification @sumesh-aot We will separate task and attribute filter and do according to the requirements 
created_at: 2025-03-04T04:45:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -121,6 +158,8 @@ def get_user_filters(**kwargs):
             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)
path: forms-flow-api/src/formsflow_api/services/filter.py
user: login: auslin-aot
body: Removed  attributeFilters from the response of filter list API
created_at: 2025-03-04T07:13:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,18 +2,28 @@
 
 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
 
 from .audit_mixin import AuditDateTimeMixin, AuditUserMixin
 
 
+@unique
+class FilterType(Enum):
+    """Filter type enum."""
+
+    TASK = "TASK"
+    ATTRIBUTE = "ATTRIBUTE"
+    DATE = "DATE"
path: forms-flow-api/src/formsflow_api/models/filter.py
user: login: arun-s-aot
body: do we need DATE here ?
created_at: 2025-03-10T11:20:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,18 +2,28 @@
 
 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
 
 from .audit_mixin import AuditDateTimeMixin, AuditUserMixin
 
 
+@unique
+class FilterType(Enum):
+    """Filter type enum."""
+
+    TASK = "TASK"
+    ATTRIBUTE = "ATTRIBUTE"
+    DATE = "DATE"
path: forms-flow-api/src/formsflow_api/models/filter.py
user: login: auslin-aot
body: done
created_at: 2025-03-10T11:37:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-10T11:34:40Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=AOT-Technologies_forms-flow-ai&pullRequest=2596) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=AOT-Technologies_forms-flow-ai&pullRequest=2596&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=AOT-Technologies_forms-flow-ai&pullRequest=2596&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=AOT-Technologies_forms-flow-ai&pullRequest=2596&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=AOT-Technologies_forms-flow-ai&pullRequest=2596&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=AOT-Technologies_forms-flow-ai&pullRequest=2596&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=AOT-Technologies_forms-flow-ai&pullRequest=2596)


author_association: NONE