- Cleaned up admin.py by removing unnecessary dictionary wrapping for fields. - Adjusted migration files to maintain consistent formatting and removed redundant lines. - Updated models.py and tests to remove unnecessary blank lines and improve readability. - Enhanced error handling in the analytics API to properly forward upstream status and messages. - Added comprehensive tests for the analytics proxy to ensure correct status and body forwarding. - Updated contributing guidelines to clarify the use of ruff for code style enforcement.
240 lines
7.7 KiB
Python
240 lines
7.7 KiB
Python
"""Tasks-side tests for custom fields: Task create/update payloads,
|
|
list filter, and detail-page response shape.
|
|
|
|
Mirrors opportunity/tests/test_custom_fields.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from django.db import connection
|
|
|
|
from common.models import CustomFieldDefinition
|
|
from tasks.models import Task
|
|
|
|
pg_only = pytest.mark.skipif(
|
|
connection.vendor != "postgresql",
|
|
reason="JSONField __contains lookup requires PostgreSQL",
|
|
)
|
|
|
|
|
|
TASKS_LIST_URL = "/api/tasks/"
|
|
|
|
|
|
def _set_rls(org):
|
|
if connection.vendor != "postgresql":
|
|
return
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("SELECT set_config('app.current_org', %s, false)", [str(org.id)])
|
|
|
|
|
|
def _make_complexity_def(org, **overrides):
|
|
defaults = {
|
|
"target_model": "Task",
|
|
"key": "complexity",
|
|
"label": "Complexity",
|
|
"field_type": "dropdown",
|
|
"options": [
|
|
{"value": "trivial", "label": "Trivial"},
|
|
{"value": "epic", "label": "Epic"},
|
|
],
|
|
"is_required": False,
|
|
"is_active": True,
|
|
}
|
|
defaults.update(overrides)
|
|
return CustomFieldDefinition.objects.create(org=org, **defaults)
|
|
|
|
|
|
@pytest.fixture
|
|
def task_a(org_a, admin_user):
|
|
_set_rls(org_a)
|
|
return Task.objects.create(
|
|
title="Primary Task",
|
|
status="New",
|
|
priority="Medium",
|
|
org=org_a,
|
|
created_by=admin_user,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def task_a2(org_a, admin_user):
|
|
_set_rls(org_a)
|
|
return Task.objects.create(
|
|
title="Secondary Task",
|
|
status="New",
|
|
priority="Low",
|
|
org=org_a,
|
|
created_by=admin_user,
|
|
)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestTaskCreateWithCustomFields:
|
|
def test_create_with_valid_dropdown_value(self, admin_client, org_a):
|
|
_make_complexity_def(org_a)
|
|
response = admin_client.post(
|
|
TASKS_LIST_URL,
|
|
{
|
|
"title": "Tiered Task",
|
|
"status": "New",
|
|
"priority": "Low",
|
|
"custom_fields": {"complexity": "epic"},
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
task = Task.objects.get(title="Tiered Task", org=org_a)
|
|
assert task.custom_fields == {"complexity": "epic"}
|
|
|
|
def test_create_rejects_invalid_dropdown_value(self, admin_client, org_a):
|
|
_make_complexity_def(org_a)
|
|
response = admin_client.post(
|
|
TASKS_LIST_URL,
|
|
{
|
|
"title": "Bad Tier Task",
|
|
"status": "New",
|
|
"priority": "Low",
|
|
"custom_fields": {"complexity": "platinum"},
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 400
|
|
assert "custom_fields" in response.json()["errors"]
|
|
assert "complexity" in response.json()["errors"]["custom_fields"]
|
|
|
|
def test_create_drops_unknown_keys(self, admin_client, org_a):
|
|
_make_complexity_def(org_a)
|
|
response = admin_client.post(
|
|
TASKS_LIST_URL,
|
|
{
|
|
"title": "Unknown Key Task",
|
|
"status": "New",
|
|
"priority": "Low",
|
|
"custom_fields": {"complexity": "trivial", "made_up": "ignored"},
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200
|
|
task = Task.objects.get(title="Unknown Key Task", org=org_a)
|
|
assert task.custom_fields == {"complexity": "trivial"}
|
|
|
|
def test_create_required_missing_returns_400(self, admin_client, org_a):
|
|
_make_complexity_def(org_a, is_required=True)
|
|
response = admin_client.post(
|
|
TASKS_LIST_URL,
|
|
{"title": "No CF Task", "status": "New", "priority": "Low"},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 400
|
|
assert "complexity" in response.json()["errors"]["custom_fields"]
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestTaskUpdateWithCustomFields:
|
|
def test_patch_merges_with_existing(self, admin_client, task_a, org_a):
|
|
_make_complexity_def(org_a)
|
|
_make_complexity_def(
|
|
org_a,
|
|
key="story_points",
|
|
field_type="number",
|
|
options=None,
|
|
)
|
|
task_a.custom_fields = {"complexity": "epic"}
|
|
task_a.save()
|
|
|
|
response = admin_client.patch(
|
|
f"{TASKS_LIST_URL}{task_a.id}/",
|
|
{"custom_fields": {"story_points": 13}},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
task_a.refresh_from_db()
|
|
assert task_a.custom_fields == {
|
|
"complexity": "epic",
|
|
"story_points": 13.0,
|
|
}
|
|
|
|
def test_patch_invalid_returns_400(self, admin_client, task_a, org_a):
|
|
_make_complexity_def(org_a)
|
|
response = admin_client.patch(
|
|
f"{TASKS_LIST_URL}{task_a.id}/",
|
|
{"custom_fields": {"complexity": "platinum"}},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
def test_put_replaces_custom_fields(self, admin_client, task_a, org_a):
|
|
_make_complexity_def(org_a)
|
|
task_a.custom_fields = {"complexity": "trivial"}
|
|
task_a.save()
|
|
|
|
response = admin_client.put(
|
|
f"{TASKS_LIST_URL}{task_a.id}/",
|
|
{
|
|
"title": task_a.title,
|
|
"status": task_a.status,
|
|
"priority": task_a.priority,
|
|
"custom_fields": {"complexity": "epic"},
|
|
},
|
|
format="json",
|
|
)
|
|
assert response.status_code == 200, response.content
|
|
task_a.refresh_from_db()
|
|
assert task_a.custom_fields == {"complexity": "epic"}
|
|
|
|
|
|
@pg_only
|
|
@pytest.mark.django_db
|
|
class TestTaskListFilter:
|
|
def test_cf_filter_returns_matching(self, admin_client, task_a, task_a2, org_a):
|
|
_make_complexity_def(org_a)
|
|
task_a.custom_fields = {"complexity": "epic"}
|
|
task_a.save()
|
|
task_a2.custom_fields = {"complexity": "trivial"}
|
|
task_a2.save()
|
|
|
|
response = admin_client.get(f"{TASKS_LIST_URL}?cf_complexity=epic")
|
|
body = response.json()
|
|
ids = [t["id"] for t in body["tasks"]]
|
|
assert str(task_a.id) in ids
|
|
assert str(task_a2.id) not in ids
|
|
|
|
|
|
@pytest.mark.django_db
|
|
class TestTaskDetailResponse:
|
|
def test_detail_includes_definitions_and_values(self, admin_client, task_a, org_a):
|
|
_make_complexity_def(org_a)
|
|
task_a.custom_fields = {"complexity": "trivial"}
|
|
task_a.save()
|
|
|
|
response = admin_client.get(f"{TASKS_LIST_URL}{task_a.id}/")
|
|
assert response.status_code == 200, response.content
|
|
body = response.json()
|
|
assert body["task_obj"]["custom_fields"] == {"complexity": "trivial"}
|
|
keys = [d["key"] for d in body["custom_field_definitions"]]
|
|
assert "complexity" in keys
|
|
|
|
def test_detail_excludes_inactive_definitions(self, admin_client, task_a, org_a):
|
|
_make_complexity_def(org_a)
|
|
_make_complexity_def(
|
|
org_a, key="legacy", field_type="text", options=None, is_active=False
|
|
)
|
|
response = admin_client.get(f"{TASKS_LIST_URL}{task_a.id}/")
|
|
keys = [d["key"] for d in response.json()["custom_field_definitions"]]
|
|
assert "complexity" in keys
|
|
assert "legacy" not in keys
|
|
|
|
def test_cross_org_does_not_leak_definitions(
|
|
self, admin_client, task_a, org_a, org_b
|
|
):
|
|
CustomFieldDefinition.objects.create(
|
|
org=org_b,
|
|
target_model="Task",
|
|
key="other_org_field",
|
|
label="Other org",
|
|
field_type="text",
|
|
)
|
|
response = admin_client.get(f"{TASKS_LIST_URL}{task_a.id}/")
|
|
keys = [d["key"] for d in response.json()["custom_field_definitions"]]
|
|
assert "other_org_field" not in keys
|