From f1ef5f2a4ed562d4edb5d28296a070f796efdbf9 Mon Sep 17 00:00:00 2001 From: Dheeraj Kumar Ketireddy Date: Fri, 28 Nov 2025 17:09:45 +0530 Subject: [PATCH 1/2] [SILO-723] feat: Updated custom properties api to support passing options while crating --- plane/models/work_item_properties.py | 2 + tests/scripts/test_property_values.py | 104 ++++++---- .../test_work_item_types_and_properties.py | 128 ++++++++---- tests/unit/test_work_item_properties.py | 183 ++++++++++++++++-- 4 files changed, 323 insertions(+), 94 deletions(-) diff --git a/plane/models/work_item_properties.py b/plane/models/work_item_properties.py index e822bd9..e78bcf1 100644 --- a/plane/models/work_item_properties.py +++ b/plane/models/work_item_properties.py @@ -41,6 +41,7 @@ class WorkItemProperty(BaseModel): workspace: str | None = None project: str | None = None issue_type: str | None = None + options: list["WorkItemPropertyOption"] | None = None @field_serializer("property_type") def serialize_property_type(self, value: PropertyType) -> str: @@ -68,6 +69,7 @@ class CreateWorkItemProperty(BaseModel): validation_rules: Any | None = None external_source: str | None = None external_id: str | None = None + options: list["CreateWorkItemPropertyOption"] | None = None @field_serializer("property_type") def serialize_property_type(self, value: PropertyType) -> str: diff --git a/tests/scripts/test_property_values.py b/tests/scripts/test_property_values.py index 1d60e2d..0bb2e71 100644 --- a/tests/scripts/test_property_values.py +++ b/tests/scripts/test_property_values.py @@ -215,21 +215,31 @@ def main() -> None: properties["datetime"] = datetime_prop print_success(f"DateTime property created: {datetime_prop.display_name}") - # Option property + # Option property (with inline options) option_prop_data = CreateWorkItemProperty( display_name="Status", description="Current status of the task", property_type=PropertyType.OPTION.value, is_required=False, is_active=True, + options=[ + CreateWorkItemPropertyOption(name="Not Started", description="Status: Not Started"), + CreateWorkItemPropertyOption(name="In Progress", description="Status: In Progress"), + CreateWorkItemPropertyOption(name="Review", description="Status: Review"), + CreateWorkItemPropertyOption(name="Completed", description="Status: Completed"), + CreateWorkItemPropertyOption(name="Cancelled", description="Status: Cancelled"), + ], ) option_prop = client.work_item_properties.create( workspace_slug, project.id, task_type.id, option_prop_data ) properties["option"] = option_prop - print_success(f"Option property created: {option_prop.display_name}") + print_success( + f"Option property created: {option_prop.display_name} " + f"(with {len(option_prop.options)} inline options)" + ) - # Multi-value option property for testing multi-value functionality + # Multi-value option property for testing multi-value functionality (with inline options) multi_option_prop_data = CreateWorkItemProperty( display_name="Tags", description="Multiple tags for the task", @@ -237,44 +247,66 @@ def main() -> None: is_required=False, is_active=True, is_multi=True, # Enable multi-value support + options=[ + CreateWorkItemPropertyOption(name="Frontend", description="Tag: Frontend"), + CreateWorkItemPropertyOption(name="Backend", description="Tag: Backend"), + CreateWorkItemPropertyOption(name="Database", description="Tag: Database"), + CreateWorkItemPropertyOption(name="UI/UX", description="Tag: UI/UX"), + CreateWorkItemPropertyOption(name="Testing", description="Tag: Testing"), + CreateWorkItemPropertyOption( + name="Documentation", description="Tag: Documentation" + ), + ], ) multi_option_prop = client.work_item_properties.create( workspace_slug, project.id, task_type.id, multi_option_prop_data ) properties["multi_option"] = multi_option_prop - print_success(f"Multi-value option property created: {multi_option_prop.display_name}") - - # Create options for the option property - print_step(5, "Creating property options") - status_options = [] - tag_options = [] - - statuses = ["Not Started", "In Progress", "Review", "Completed", "Cancelled"] - for status in statuses: - option_data = CreateWorkItemPropertyOption( - name=status, - description=f"Status: {status}", - is_active=True, - ) - option = client.work_item_properties.options.create( - workspace_slug, project.id, option_prop.id, option_data - ) - status_options.append(option) - print_success(f"Status option created: {option.name}") - - # Create options for the multi-value property - tags = ["Frontend", "Backend", "Database", "UI/UX", "Testing", "Documentation"] - for tag in tags: - option_data = CreateWorkItemPropertyOption( - name=tag, - description=f"Tag: {tag}", - is_active=True, - ) - option = client.work_item_properties.options.create( - workspace_slug, project.id, multi_option_prop.id, option_data - ) - tag_options.append(option) - print_success(f"Tag option created: {option.name}") + print_success( + f"Multi-value option property created: {multi_option_prop.display_name} " + f"(with {len(multi_option_prop.options)} inline options)" + ) + + # Get options from the properties (created inline) + print_step(5, "Verifying inline property options") + status_options = option_prop.options + tag_options = multi_option_prop.options + + print_success(f"Status options verified: {len(status_options)} options") + for opt in status_options: + print(f" - {opt.name}") + + print_success(f"Tag options verified: {len(tag_options)} options") + for opt in tag_options: + print(f" - {opt.name}") + + # Verify options are included in list/retrieve responses + print_step(5.5, "Verifying options in list/retrieve responses") + + # Test list response includes options + all_properties = client.work_item_properties.list(workspace_slug, project.id, task_type.id) + print_success(f"Listed {len(all_properties)} properties") + + # Find option properties in list and verify options are included + listed_status_prop = next((p for p in all_properties if p.id == option_prop.id), None) + assert listed_status_prop is not None, "Status property should be in list" + assert listed_status_prop.options is not None, "Options should be in list response" + assert len(listed_status_prop.options) == 5, "Should have 5 status options in list" + print_success("List response includes options for Status property ✓") + + listed_tags_prop = next((p for p in all_properties if p.id == multi_option_prop.id), None) + assert listed_tags_prop is not None, "Tags property should be in list" + assert listed_tags_prop.options is not None, "Options should be in list response" + assert len(listed_tags_prop.options) == 6, "Should have 6 tag options in list" + print_success("List response includes options for Tags property ✓") + + # Test retrieve response includes options + retrieved_status_prop = client.work_item_properties.retrieve( + workspace_slug, project.id, task_type.id, option_prop.id + ) + assert retrieved_status_prop.options is not None, "Options should be in retrieve response" + assert len(retrieved_status_prop.options) == 5, "Should have 5 options in retrieve" + print_success("Retrieve response includes options ✓") # Create work items for testing print_step(6, "Creating work items for property value testing") diff --git a/tests/scripts/test_work_item_types_and_properties.py b/tests/scripts/test_work_item_types_and_properties.py index b21dcf1..30dad31 100644 --- a/tests/scripts/test_work_item_types_and_properties.py +++ b/tests/scripts/test_work_item_types_and_properties.py @@ -29,6 +29,9 @@ CreateWorkItemPropertyOption, CreateWorkItemPropertyValue, ) +from plane.models.work_item_property_configurations import ( # noqa: E402 + TextAttributeSettings, +) from plane.models.work_item_types import CreateWorkItemType # noqa: E402 from plane.models.work_items import CreateWorkItem # noqa: E402 @@ -162,6 +165,7 @@ def main() -> None: property_type=PropertyType.TEXT.value, is_required=True, is_active=True, + settings=TextAttributeSettings(display_format="single-line"), ) severity_prop = client.work_item_properties.create( workspace_slug, project.id, bug_type.id, severity_prop_data @@ -169,19 +173,46 @@ def main() -> None: bug_properties.append(severity_prop) print_success(f"Property created: {severity_prop.display_name} (ID: {severity_prop.id})") - # Option property - Priority + # Option property - Priority (with inline options) priority_prop_data = CreateWorkItemProperty( display_name="Priority", description="Priority level for this bug", property_type=PropertyType.OPTION.value, is_required=True, is_active=True, + options=[ + CreateWorkItemPropertyOption( + name="Critical", + description="Priority level: Critical", + is_active=True, + is_default=False, + ), + CreateWorkItemPropertyOption( + name="High", + description="Priority level: High", + is_active=True, + is_default=False, + ), + CreateWorkItemPropertyOption( + name="Medium", + description="Priority level: Medium", + is_active=True, + is_default=True, # Set Medium as default + ), + CreateWorkItemPropertyOption( + name="Low", + description="Priority level: Low", + is_active=True, + is_default=False, + ), + ], ) priority_prop = client.work_item_properties.create( workspace_slug, project.id, bug_type.id, priority_prop_data ) bug_properties.append(priority_prop) print_success(f"Property created: {priority_prop.display_name} (ID: {priority_prop.id})") + print(f" Options created inline: {len(priority_prop.options)} options") # Boolean property - Is Critical critical_prop_data = CreateWorkItemProperty( @@ -211,23 +242,36 @@ def main() -> None: bug_properties.append(hours_prop) print_success(f"Property created: {hours_prop.display_name} (ID: {hours_prop.id})") - # Create options for the Priority property - print_step(5, "Creating property options for Priority") - priority_options = [] - - priority_levels = ["Critical", "High", "Medium", "Low"] - for level in priority_levels: - option_data = CreateWorkItemPropertyOption( - name=level, - description=f"Priority level: {level}", - is_active=True, - is_default=(level == "Medium"), # Set Medium as default - ) - option = client.work_item_properties.options.create( - workspace_slug, project.id, priority_prop.id, option_data - ) - priority_options.append(option) - print_success(f"Option created: {option.name} (ID: {option.id})") + # Get options from the Priority property (created inline) + print_step(5, "Verifying inline options for Priority property") + priority_options = priority_prop.options + print_success(f"Priority property has {len(priority_options)} options (created inline)") + for option in priority_options: + print(f" - {option.name} (ID: {option.id})") + + # Verify options are included in list and retrieve responses + print_step(5.5, "Verifying options in list/retrieve responses") + + # Test list response includes options + all_properties = client.work_item_properties.list(workspace_slug, project.id, bug_type.id) + print_success(f"Listed {len(all_properties)} properties for Bug type") + + # Find the priority property in the list + listed_priority_prop = next((p for p in all_properties if p.id == priority_prop.id), None) + assert listed_priority_prop is not None, "Priority property should be in list" + assert listed_priority_prop.options is not None, "Options should be in list response" + assert len(listed_priority_prop.options) == 4, "Should have 4 options in list response" + print_success("List response includes options ✓") + for opt in listed_priority_prop.options: + print(f" - {opt.name}") + + # Test retrieve response includes options + retrieved_priority_prop = client.work_item_properties.retrieve( + workspace_slug, project.id, bug_type.id, priority_prop.id + ) + assert retrieved_priority_prop.options is not None, "Options in retrieve response" + assert len(retrieved_priority_prop.options) == 4, "Should have 4 options in retrieve" + print_success("Retrieve response includes options ✓") # Create a work item with the Bug type print_step(6, "Creating a work item with Bug type and custom properties") @@ -247,55 +291,61 @@ def main() -> None: print_step(7, "Assigning custom property values to work item") # Set Severity (text property) - severity_value_data = CreateWorkItemPropertyValue( - values=[CreateWorkItemPropertyValue.ValueItem(value="High")] - ) + severity_value_data = CreateWorkItemPropertyValue(value="High") severity_value = client.work_item_properties.values.create( workspace_slug, project.id, work_item.id, severity_prop.id, severity_value_data ) - print_success(f"Severity value set: {severity_value.values[0].value}") + print_success(f"Severity value set: {severity_value.value}") # Set Priority (option property) - use the High option high_option = next(opt for opt in priority_options if opt.name == "High") - priority_value_data = CreateWorkItemPropertyValue( - values=[CreateWorkItemPropertyValue.ValueItem(value=high_option.id)] - ) + priority_value_data = CreateWorkItemPropertyValue(value=high_option.id) client.work_item_properties.values.create( workspace_slug, project.id, work_item.id, priority_prop.id, priority_value_data ) print_success(f"Priority value set: {high_option.name}") # Set Is Critical (boolean property) - critical_value_data = CreateWorkItemPropertyValue( - values=[CreateWorkItemPropertyValue.ValueItem(value=True)] - ) + critical_value_data = CreateWorkItemPropertyValue(value=True) critical_value = client.work_item_properties.values.create( workspace_slug, project.id, work_item.id, critical_prop.id, critical_value_data ) - print_success(f"Critical value set: {critical_value.values[0].value}") + print_success(f"Critical value set: {critical_value.value}") # Set Estimated Hours (decimal property) - hours_value_data = CreateWorkItemPropertyValue( - values=[CreateWorkItemPropertyValue.ValueItem(value=4.5)] - ) + hours_value_data = CreateWorkItemPropertyValue(value=4.5) hours_value = client.work_item_properties.values.create( workspace_slug, project.id, work_item.id, hours_prop.id, hours_value_data ) - print_success(f"Estimated hours value set: {hours_value.values[0].value}") + print_success(f"Estimated hours value set: {hours_value.value}") # Retrieve and verify the work item with its property values print_step(8, "Verifying work item and property values") retrieved_work_item = client.work_items.retrieve(workspace_slug, project.id, work_item.id) print_success(f"Work item retrieved: {retrieved_work_item.name}") - # Get all property values for the work item - property_values = client.work_item_properties.values.list( - workspace_slug, project.id, work_item.id + # Retrieve individual property values to verify they were set correctly + retrieved_severity = client.work_item_properties.values.retrieve( + workspace_slug, project.id, work_item.id, severity_prop.id + ) + print(f" Severity: {retrieved_severity.value}") + + retrieved_priority = client.work_item_properties.values.retrieve( + workspace_slug, project.id, work_item.id, priority_prop.id + ) + print(f" Priority: {retrieved_priority.value}") + + retrieved_critical = client.work_item_properties.values.retrieve( + workspace_slug, project.id, work_item.id, critical_prop.id + ) + print(f" Is Critical: {retrieved_critical.value}") + + retrieved_hours = client.work_item_properties.values.retrieve( + workspace_slug, project.id, work_item.id, hours_prop.id ) - print_success(f"Retrieved {len(property_values)} property values") + print(f" Estimated Hours: {retrieved_hours.value}") - for prop_value in property_values: - print(f" Property {prop_value.property_id}: {prop_value.values}") + print_success("All property values retrieved successfully") # Test creating a work item with Feature type print_step(9, "Creating a work item with Feature type") diff --git a/tests/unit/test_work_item_properties.py b/tests/unit/test_work_item_properties.py index 37d8b92..7e4bbe2 100644 --- a/tests/unit/test_work_item_properties.py +++ b/tests/unit/test_work_item_properties.py @@ -5,7 +5,11 @@ from plane.client import PlaneClient from plane.models.enums import PropertyType, RelationType from plane.models.projects import Project -from plane.models.work_item_properties import CreateWorkItemProperty, UpdateWorkItemProperty +from plane.models.work_item_properties import ( + CreateWorkItemProperty, + CreateWorkItemPropertyOption, + UpdateWorkItemProperty, +) from plane.models.work_item_property_configurations import ( DateAttributeSettings, TextAttributeSettings, @@ -16,21 +20,19 @@ class TestWorkItemPropertiesAPI: """Test WorkItemProperties API resource.""" @pytest.fixture - def work_item_type( - self, client: PlaneClient, workspace_slug: str, project: Project - ): + def work_item_type(self, client: PlaneClient, workspace_slug: str, project: Project): """Get or create a work item type.""" import time from plane.models.work_item_types import CreateWorkItemType - + # Try to get an existing work item type types = client.work_item_types.list(workspace_slug, project.id) if types: work_item_type = types[0] yield work_item_type return - + # Create a new work item type type_data = CreateWorkItemType( name=f"Test Type {int(time.time())}", @@ -53,30 +55,80 @@ def test_list_work_item_properties( work_item_type, ) -> None: """Test listing work item properties.""" - properties = client.work_item_properties.list( - workspace_slug, project.id, work_item_type.id - ) + properties = client.work_item_properties.list(workspace_slug, project.id, work_item_type.id) assert isinstance(properties, list) + def test_list_work_item_properties_includes_options( + self, + client: PlaneClient, + workspace_slug: str, + project: Project, + work_item_type, + ) -> None: + """Test that listing work item properties includes options in the response.""" + import time + + # Create a property with inline options + property_data = CreateWorkItemProperty( + display_name=f"List Test Property {int(time.time())}", + description="Property to test list includes options", + property_type=PropertyType.OPTION, + is_required=False, + is_active=True, + options=[ + CreateWorkItemPropertyOption(name="Option A"), + CreateWorkItemPropertyOption(name="Option B"), + CreateWorkItemPropertyOption(name="Option C"), + ], + ) + created_prop = client.work_item_properties.create( + workspace_slug, project.id, work_item_type.id, property_data + ) + + try: + # List all properties + properties = client.work_item_properties.list( + workspace_slug, project.id, work_item_type.id + ) + assert isinstance(properties, list) + + # Find the created property in the list + found_prop = next((p for p in properties if p.id == created_prop.id), None) + assert found_prop is not None, "Created property should be in the list" + + # Verify options are included in the list response + assert found_prop.options is not None, "Options should be included in list response" + assert len(found_prop.options) == 3, "Should have 3 options" + option_names = [opt.name for opt in found_prop.options] + assert "Option A" in option_names + assert "Option B" in option_names + assert "Option C" in option_names + finally: + # Cleanup + try: + client.work_item_properties.delete( + workspace_slug, project.id, work_item_type.id, created_prop.id + ) + except Exception: + pass + class TestWorkItemPropertiesAPICRUD: """Test WorkItemProperties API CRUD operations.""" @pytest.fixture - def work_item_type( - self, client: PlaneClient, workspace_slug: str, project: Project - ): + def work_item_type(self, client: PlaneClient, workspace_slug: str, project: Project): """Get or create a work item type.""" import time from plane.models.work_item_types import CreateWorkItemType - + types = client.work_item_types.list(workspace_slug, project.id) if types: work_item_type = types[0] yield work_item_type return - + type_data = CreateWorkItemType( name=f"Test Type {int(time.time())}", description="Test type", @@ -94,6 +146,7 @@ def work_item_type( def property_data(self) -> CreateWorkItemProperty: """Create test property data.""" import time + return CreateWorkItemProperty( display_name=f"Test Property {int(time.time())}", description="Test property", @@ -139,7 +192,7 @@ def test_create_work_item_property( assert prop is not None assert prop.id is not None assert prop.display_name == property_data.display_name - + # Cleanup try: client.work_item_properties.delete( @@ -164,6 +217,55 @@ def test_retrieve_work_item_property( assert retrieved.id == work_item_property.id assert retrieved.display_name == work_item_property.display_name + def test_retrieve_work_item_property_includes_options( + self, + client: PlaneClient, + workspace_slug: str, + project: Project, + work_item_type, + ) -> None: + """Test that retrieving a work item property includes options in the response.""" + import time + + # Create a property with inline options + property_data = CreateWorkItemProperty( + display_name=f"Retrieve Test Property {int(time.time())}", + description="Property to test retrieve includes options", + property_type=PropertyType.OPTION, + is_required=False, + is_active=True, + options=[ + CreateWorkItemPropertyOption(name="Choice 1", is_default=True), + CreateWorkItemPropertyOption(name="Choice 2"), + ], + ) + created_prop = client.work_item_properties.create( + workspace_slug, project.id, work_item_type.id, property_data + ) + + try: + # Retrieve the property + retrieved = client.work_item_properties.retrieve( + workspace_slug, project.id, work_item_type.id, created_prop.id + ) + assert retrieved is not None + assert retrieved.id == created_prop.id + + # Verify options are included in the retrieve response + assert retrieved.options is not None, "Options should be in retrieve response" + assert len(retrieved.options) == 2, "Should have 2 options" + option_names = [opt.name for opt in retrieved.options] + assert "Choice 1" in option_names + assert "Choice 2" in option_names + finally: + # Cleanup + try: + client.work_item_properties.delete( + workspace_slug, project.id, work_item_type.id, created_prop.id + ) + except Exception: + pass + def test_update_work_item_property( self, client: PlaneClient, @@ -186,9 +288,7 @@ class TestWorkItemPropertyTypes: """Test creating work item properties of different types.""" @pytest.fixture - def work_item_type( - self, client: PlaneClient, workspace_slug: str, project: Project - ): + def work_item_type(self, client: PlaneClient, workspace_slug: str, project: Project): """Create a work item type.""" import time @@ -467,6 +567,52 @@ def test_create_option_property( except Exception: pass + def test_create_option_property_with_inline_options( + self, + client: PlaneClient, + workspace_slug: str, + project: Project, + work_item_type, + ) -> None: + """Test creating an OPTION property with inline options.""" + import time + + property_data = CreateWorkItemProperty( + display_name=f"Priority Property {int(time.time())}", + description="Test option property with inline options", + property_type=PropertyType.OPTION, + is_required=False, + is_active=True, + options=[ + CreateWorkItemPropertyOption(name="Low", is_default=False), + CreateWorkItemPropertyOption(name="Medium", is_default=True), + CreateWorkItemPropertyOption(name="High", is_default=False), + CreateWorkItemPropertyOption(name="Critical", is_default=False), + ], + ) + prop = client.work_item_properties.create( + workspace_slug, project.id, work_item_type.id, property_data + ) + assert prop is not None + assert prop.property_type == PropertyType.OPTION + assert prop.display_name == property_data.display_name + # Verify options were created + assert prop.options is not None + assert len(prop.options) == 4 + option_names = [opt.name for opt in prop.options] + assert "Low" in option_names + assert "Medium" in option_names + assert "High" in option_names + assert "Critical" in option_names + + # Cleanup + try: + client.work_item_properties.delete( + workspace_slug, project.id, work_item_type.id, prop.id + ) + except Exception: + pass + def test_create_file_property( self, client: PlaneClient, @@ -498,4 +644,3 @@ def test_create_file_property( ) except Exception: pass - From d0ce071438d8ef3a409979b699c2c528272f901b Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Sat, 29 Nov 2025 15:29:05 +0530 Subject: [PATCH 2/2] upgrade package version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1f667cf..e10d90a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plane-sdk" -version = "0.2.1" +version = "0.2.2" description = "Python SDK for Plane API" readme = "README.md" requires-python = ">=3.10"