Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions libzapi/application/commands/ticketing/ticket_field_cmds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Iterable, TypeAlias


@dataclass(frozen=True, slots=True)
class CreateTicketFieldCmd:
title: str
type: str
description: str | None = None
active: bool | None = None
required: bool | None = None
collapsed_for_agents: bool | None = None
regexp_for_validation: str | None = None
title_in_portal: str | None = None
visible_in_portal: bool | None = None
editable_in_portal: bool | None = None
required_in_portal: bool | None = None
agent_can_edit: bool | None = None
tag: str | None = None
position: int | None = None
custom_field_options: Iterable[dict[str, Any]] | None = None
sub_type_id: int | None = None
relationship_target_type: str | None = None
relationship_filter: dict[str, Any] | None = None
agent_description: str | None = None


@dataclass(frozen=True, slots=True)
class UpdateTicketFieldCmd:
title: str | None = None
description: str | None = None
active: bool | None = None
required: bool | None = None
collapsed_for_agents: bool | None = None
regexp_for_validation: str | None = None
title_in_portal: str | None = None
visible_in_portal: bool | None = None
editable_in_portal: bool | None = None
required_in_portal: bool | None = None
agent_can_edit: bool | None = None
tag: str | None = None
position: int | None = None
custom_field_options: Iterable[dict[str, Any]] | None = None
sub_type_id: int | None = None
relationship_target_type: str | None = None
relationship_filter: dict[str, Any] | None = None
agent_description: str | None = None


@dataclass(frozen=True, slots=True)
class TicketFieldOptionCmd:
name: str
value: str
id: int | None = None


TicketFieldCmd: TypeAlias = CreateTicketFieldCmd | UpdateTicketFieldCmd

Check warning on line 59 in libzapi/application/commands/ticketing/ticket_field_cmds.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a "type" statement instead of this "TypeAlias".

See more on https://sonarcloud.io/project/issues?id=BCR-CX_libzapi&issues=AZ2yJQp51bH6TsjJf-uh&open=AZ2yJQp51bH6TsjJf-uh&pullRequest=88
50 changes: 41 additions & 9 deletions libzapi/application/services/ticketing/ticket_fields_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
from typing import Iterable
from __future__ import annotations

from typing import Any, Iterable

from libzapi.application.commands.ticketing.ticket_field_cmds import (
CreateTicketFieldCmd,
TicketFieldOptionCmd,
UpdateTicketFieldCmd,
)
from libzapi.domain.models.ticketing.ticket_field import TicketField
from libzapi.infrastructure.api_clients.ticketing.ticket_field_api_client import TicketFieldApiClient
from libzapi.infrastructure.api_clients.ticketing.ticket_field_api_client import (
TicketFieldApiClient,
)


class TicketFieldsService:
Expand All @@ -13,13 +23,35 @@ def list_all(self) -> Iterable[TicketField]:
return self._client.list()

def get_by_id(self, field_id: int) -> TicketField:
return self._client.get(field_id)
return self._client.get(field_id=field_id)

def create(self, **fields) -> TicketField:
return self._client.create(entity=CreateTicketFieldCmd(**fields))

def update(self, field_id: int, **fields) -> TicketField:
return self._client.update(
field_id=field_id, entity=UpdateTicketFieldCmd(**fields)
)

def delete(self, field_id: int) -> None:
self._client.delete(field_id=field_id)

def reorder(self, field_ids: Iterable[int]) -> None:
self._client.reorder(field_ids=field_ids)

def list_options(self, field_id: int) -> Iterable[dict[str, Any]]:
return self._client.list_options(field_id=field_id)

def create_field(self, entity: TicketField) -> TicketField:
return self._client.create(entity)
def get_option(self, field_id: int, option_id: int) -> dict[str, Any]:
return self._client.get_option(field_id=field_id, option_id=option_id)

def update_field(self, field_id: int, entity: TicketField) -> TicketField:
return self._client.update(field_id, entity)
def upsert_option(
self, field_id: int, name: str, value: str, id: int | None = None
) -> dict[str, Any]:
return self._client.upsert_option(
field_id=field_id,
option=TicketFieldOptionCmd(name=name, value=value, id=id),
)

def delete_field(self, field_id: int) -> None:
self._client.delete(field_id)
def delete_option(self, field_id: int, option_id: int) -> None:
self._client.delete_option(field_id=field_id, option_id=option_id)
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from __future__ import annotations
from typing import Iterable

from typing import Any, Iterable, Iterator

from libzapi.application.commands.ticketing.ticket_field_cmds import (
CreateTicketFieldCmd,
TicketFieldOptionCmd,
UpdateTicketFieldCmd,
)
from libzapi.domain.models.ticketing.ticket_field import TicketField
from libzapi.infrastructure.http.client import HttpClient
from libzapi.infrastructure.http.pagination import yield_items
from libzapi.infrastructure.mappers.ticketing.ticket_field_mapper import to_payload
from libzapi.infrastructure.mappers.ticketing.ticket_field_mapper import (
option_to_payload,
to_payload_create,
to_payload_update,
)
from libzapi.infrastructure.serialization.parse import to_domain
from libzapi.domain.models.ticketing.ticket_field import TicketField


class TicketFieldApiClient:
Expand All @@ -13,28 +24,61 @@ class TicketFieldApiClient:
def __init__(self, http: HttpClient) -> None:
self._http = http

def list(self) -> Iterable[TicketField]:
def list(self) -> Iterator[TicketField]:
for obj in yield_items(
get_json=self._http.get,
first_path="/api/v2/ticket_fields.json",
first_path="/api/v2/ticket_fields",
base_url=self._http.base_url,
items_key="ticket_fields",
):
yield to_domain(data=obj, cls=TicketField)

def get(self, field_id: int) -> TicketField:
data = self._http.get(f"/api/v2/ticket_fields/{field_id}.json")
data = self._http.get(f"/api/v2/ticket_fields/{int(field_id)}")
return to_domain(data=data["ticket_field"], cls=TicketField)

def create(self, entity: TicketField) -> TicketField:
payload = to_payload(entity)
data = self._http.post("/api/v2/ticket_fields.json", payload)
def create(self, entity: CreateTicketFieldCmd) -> TicketField:
payload = to_payload_create(entity)
data = self._http.post("/api/v2/ticket_fields", payload)
return to_domain(data=data["ticket_field"], cls=TicketField)

def update(self, field_id: int, entity: TicketField) -> TicketField:
payload = to_payload(entity)
data = self._http.put(f"/api/v2/ticket_fields/{field_id}.json", payload)
def update(self, field_id: int, entity: UpdateTicketFieldCmd) -> TicketField:
payload = to_payload_update(entity)
data = self._http.put(f"/api/v2/ticket_fields/{int(field_id)}", payload)
return to_domain(data=data["ticket_field"], cls=TicketField)

def delete(self, field_id: int) -> None:
self._http.delete(f"/api/v2/ticket_fields/{field_id}.json")
self._http.delete(f"/api/v2/ticket_fields/{int(field_id)}")

def reorder(self, field_ids: Iterable[int]) -> None:
payload = {"ticket_field_ids": [int(i) for i in field_ids]}
self._http.put("/api/v2/ticket_fields/reorder", payload)

def list_options(self, field_id: int) -> Iterator[dict[str, Any]]:
for obj in yield_items(
get_json=self._http.get,
first_path=f"/api/v2/ticket_fields/{int(field_id)}/options",
base_url=self._http.base_url,
items_key="custom_field_options",
):
yield obj

def get_option(self, field_id: int, option_id: int) -> dict[str, Any]:
data = self._http.get(
f"/api/v2/ticket_fields/{int(field_id)}/options/{int(option_id)}"
)
return data["custom_field_option"]

def upsert_option(
self, field_id: int, option: TicketFieldOptionCmd
) -> dict[str, Any]:
payload = option_to_payload(option)
data = self._http.post(
f"/api/v2/ticket_fields/{int(field_id)}/options", payload
)
return data["custom_field_option"]

def delete_option(self, field_id: int, option_id: int) -> None:
self._http.delete(
f"/api/v2/ticket_fields/{int(field_id)}/options/{int(option_id)}"
)
77 changes: 65 additions & 12 deletions libzapi/infrastructure/mappers/ticketing/ticket_field_mapper.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,65 @@
from libzapi.domain.models.ticketing.ticket_field import TicketField


def to_payload(entity: TicketField) -> dict:
"""Convert domain model back to Zendesk's JSON shape."""
return {
"ticket_field": {
"title": entity.title,
"type": entity.type,
"required": entity.required,
}
}
from __future__ import annotations

from libzapi.application.commands.ticketing.ticket_field_cmds import (
CreateTicketFieldCmd,
TicketFieldOptionCmd,
UpdateTicketFieldCmd,
)


def _add_optionals(body: dict, cmd: CreateTicketFieldCmd | UpdateTicketFieldCmd) -> None:

Check failure on line 10 in libzapi/infrastructure/mappers/ticketing/ticket_field_mapper.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=BCR-CX_libzapi&issues=AZ2yJQsD1bH6TsjJf-ui&open=AZ2yJQsD1bH6TsjJf-ui&pullRequest=88
if cmd.description is not None:
body["description"] = cmd.description
if cmd.active is not None:
body["active"] = cmd.active
if cmd.required is not None:
body["required"] = cmd.required
if cmd.collapsed_for_agents is not None:
body["collapsed_for_agents"] = cmd.collapsed_for_agents
if cmd.regexp_for_validation is not None:
body["regexp_for_validation"] = cmd.regexp_for_validation
if cmd.title_in_portal is not None:
body["title_in_portal"] = cmd.title_in_portal
if cmd.visible_in_portal is not None:
body["visible_in_portal"] = cmd.visible_in_portal
if cmd.editable_in_portal is not None:
body["editable_in_portal"] = cmd.editable_in_portal
if cmd.required_in_portal is not None:
body["required_in_portal"] = cmd.required_in_portal
if cmd.agent_can_edit is not None:
body["agent_can_edit"] = cmd.agent_can_edit
if cmd.tag is not None:
body["tag"] = cmd.tag
if cmd.position is not None:
body["position"] = cmd.position
if cmd.custom_field_options is not None:
body["custom_field_options"] = list(cmd.custom_field_options)
if cmd.sub_type_id is not None:
body["sub_type_id"] = cmd.sub_type_id
if cmd.relationship_target_type is not None:
body["relationship_target_type"] = cmd.relationship_target_type
if cmd.relationship_filter is not None:
body["relationship_filter"] = cmd.relationship_filter
if cmd.agent_description is not None:
body["agent_description"] = cmd.agent_description


def to_payload_create(cmd: CreateTicketFieldCmd) -> dict:
body: dict = {"title": cmd.title, "type": cmd.type}
_add_optionals(body, cmd)
return {"ticket_field": body}


def to_payload_update(cmd: UpdateTicketFieldCmd) -> dict:
body: dict = {}
if cmd.title is not None:
body["title"] = cmd.title
_add_optionals(body, cmd)
return {"ticket_field": body}


def option_to_payload(cmd: TicketFieldOptionCmd) -> dict:
body: dict = {"name": cmd.name, "value": cmd.value}
if cmd.id is not None:
body["id"] = cmd.id
return {"custom_field_option": body}
73 changes: 70 additions & 3 deletions tests/integration/ticketing/test_ticket_field.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,73 @@
import itertools
import uuid

from libzapi import Ticketing


def test_list_ticket_forms(ticketing: Ticketing):
itens = list(ticketing.ticket_fields.list_all())
assert len(itens) > 0, "Expected at least one group from the live API"
def _unique() -> str:
return uuid.uuid4().hex[:10]


def _create_field(ticketing: Ticketing, **overrides):
suffix = _unique()
defaults = dict(
title=f"libzapi field {suffix}",
type="text",
)
defaults.update(overrides)
return ticketing.ticket_fields.create(**defaults)


def test_list_and_get_ticket_field(ticketing: Ticketing):
fields = list(itertools.islice(ticketing.ticket_fields.list_all(), 20))
assert len(fields) > 0
field = ticketing.ticket_fields.get_by_id(fields[0].id)
assert field.title == fields[0].title


def test_create_update_delete_field(ticketing: Ticketing):
field = _create_field(ticketing, description="created by libzapi")
assert field.id > 0
try:
updated = ticketing.ticket_fields.update(
field.id, description="updated by libzapi", active=False
)
assert updated.description == "updated by libzapi"
assert updated.active is False
finally:
ticketing.ticket_fields.delete(field.id)


def test_dropdown_options_lifecycle(ticketing: Ticketing):
field = _create_field(
ticketing,
type="tagger",
custom_field_options=[
{"name": "Alpha", "value": f"alpha_{_unique()}"},
],
)
try:
options = list(ticketing.ticket_fields.list_options(field.id))
assert len(options) >= 1

created = ticketing.ticket_fields.upsert_option(
field_id=field.id, name="Beta", value=f"beta_{_unique()}"
)
assert created["id"]

fetched = ticketing.ticket_fields.get_option(
field_id=field.id, option_id=created["id"]
)
assert fetched["id"] == created["id"]

ticketing.ticket_fields.delete_option(
field_id=field.id, option_id=created["id"]
)
finally:
ticketing.ticket_fields.delete(field.id)


def test_reorder_does_not_raise(ticketing: Ticketing):
fields = list(itertools.islice(ticketing.ticket_fields.list_all(), 5))
ids = [f.id for f in fields]
ticketing.ticket_fields.reorder(ids)
Loading
Loading