diff --git a/changes/162.feature b/changes/162.feature new file mode 100644 index 0000000..f1f38ac --- /dev/null +++ b/changes/162.feature @@ -0,0 +1 @@ +Adds SwimLane/SwimLanes models and support to add/list in Project. diff --git a/docs/usage.rst b/docs/usage.rst index 2424dea..e7f5a11 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -144,6 +144,14 @@ To add a task to your user story just run new_project.task_statuses[0].id ) +****************************************************** +Create a swimlane +****************************************************** + +.. code:: python + + newlane = new_project.add_swimlane('New Swimlane') + ****************************************************** Create an issue ****************************************************** diff --git a/taiga/client.py b/taiga/client.py index 912674e..c18d336 100644 --- a/taiga/client.py +++ b/taiga/client.py @@ -22,6 +22,7 @@ Projects, Roles, Severities, + SwimLanes, TaskAttachments, TaskAttributes, Tasks, @@ -78,6 +79,7 @@ def _init_resources(self): self.user_stories = UserStories(self.raw_request) self.user_story_attachments = UserStoryAttachments(self.raw_request) self.users = Users(self.raw_request) + self.swimlanes = SwimLanes(self.raw_request) self.issues = Issues(self.raw_request) self.issue_attachments = IssueAttachments(self.raw_request) self.tasks = Tasks(self.raw_request) diff --git a/taiga/models/__init__.py b/taiga/models/__init__.py index 4dc0fe4..6df380b 100644 --- a/taiga/models/__init__.py +++ b/taiga/models/__init__.py @@ -30,6 +30,8 @@ Roles, Severities, Severity, + SwimLane, + SwimLanes, Task, TaskAttachment, TaskAttachments, @@ -87,6 +89,8 @@ "UserStoryStatuses", "Severity", "Severities", + "SwimLane", + "SwimLanes", "Priority", "Priorities", "IssueStatus", diff --git a/taiga/models/models.py b/taiga/models/models.py index d05b968..a09c496 100644 --- a/taiga/models/models.py +++ b/taiga/models/models.py @@ -547,6 +547,42 @@ def create(self, project, name, **attrs): return self._new_resource(payload=attrs) +class SwimLane(MoveOnDestroyMixinObject, InstanceResource): + """ + Taiga Swimlane model + + :param name: The name of :class:`SwimLane` + :param order: the order of :class:`SwimLane` + :param project: the project of :class:`SwimLane` + :param statuses: the statuses of :class:`SwimLane` + """ + + repr_attribute = "name" + + endpoint = "swimlanes" + + allowed_params = ["name", "order", "project", "statuses"] + + parser = { + "statuses": UserStoryStatuses, + } + + +class SwimLanes(MoveOnDestroyMixinList, ListResource): + instance = SwimLane + + def create(self, project, name, **attrs): + """ + Create a new :class:`SwimLane`. + + :param project: :class:`Project` id + :param name: name of :class:`SwimLane` + :param attrs: optional attributes of :class:`SwimLane` + """ + attrs.update({"project": project, "name": name}) + return self._new_resource(payload=attrs) + + class Point(MoveOnDestroyMixinObject, InstanceResource): """ Taiga Point model @@ -1179,6 +1215,7 @@ class Project(InstanceResource): "points": Points, "us_statuses": UserStoryStatuses, "milestones": Milestones, + "swimlanes": SwimLanes, } def get_item_by_ref(self, ref): @@ -1347,6 +1384,21 @@ def list_user_stories(self, **queryparams): """ return UserStories(self.requester).list(project=self.id, **queryparams) + def add_swimlane(self, name, **attrs): + """ + Adds a :class:`SwimLane` and returns a :class:`SwimLane` resource. + + :param name: name of :class:`SwimLane` + :param attrs: other :class:`SwimLane` attributes + """ + return SwimLanes(self.requester).create(self.id, name, **attrs) + + def list_swimlanes(self, **queryparams): + """ + Returns the :class:`SwimLane` list of the project. + """ + return SwimLanes(self.requester).list(project=self.id, **queryparams) + def add_issue(self, subject, priority, status, issue_type, severity, **attrs): """ Adds a Issue and returns a :class:`Issue` resource. diff --git a/tests/resources/project_details_success.json b/tests/resources/project_details_success.json index ec0a679..3f9035a 100644 --- a/tests/resources/project_details_success.json +++ b/tests/resources/project_details_success.json @@ -646,6 +646,18 @@ "computable": false } ], + "swimlanes": [ + { + "id": 1, + "name": "SwimLane 1", + "order": 0 + }, + { + "id": 2, + "name": "SwimLane 2", + "order": 1 + } + ], "members": [ { "id": 10, diff --git a/tests/test_projects.py b/tests/test_projects.py index cd52be0..2777ec1 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -3,7 +3,7 @@ from unittest.mock import patch from taiga import TaigaAPI -from taiga.models import Point, Project, Projects, Severity, User, UserStoryStatus +from taiga.models import Point, Project, Projects, Severity, SwimLane, User, UserStoryStatus from taiga.requestmaker import RequestMaker from .tools import MockResponse, create_mock_json @@ -21,6 +21,7 @@ def test_single_project_parsing(self, mock_requestmaker_get): self.assertEqual(len(project.members), 11) self.assertTrue(isinstance(project.members[0], User)) self.assertTrue(isinstance(project.points[0], Point)) + self.assertTrue(isinstance(project.swimlanes[0], SwimLane)) self.assertTrue(isinstance(project.us_statuses[0], UserStoryStatus)) self.assertTrue(isinstance(project.severities[0], Severity)) @@ -287,6 +288,20 @@ def test_list_task_statuses(self, mock_list_task_statuses): project.list_task_statuses() mock_list_task_statuses.assert_called_with(project=1) + @patch("taiga.models.SwimLanes.create") + def test_add_swimlane(self, mock_new_swimlane): + rm = RequestMaker("/api/v1", "fakehost", "faketoken") + project = Project(rm, id=1) + project.add_swimlane("SwimLane 1") + mock_new_swimlane.assert_called_with(1, "SwimLane 1") + + @patch("taiga.models.SwimLanes.list") + def test_list_swimlanes(self, mock_list_swimlanes): + rm = RequestMaker("/api/v1", "fakehost", "faketoken") + project = Project(rm, id=1) + project.list_swimlanes() + mock_list_swimlanes.assert_called_with(project=1) + @patch("taiga.models.Points.create") def test_add_point(self, mock_new_point): rm = RequestMaker("/api/v1", "fakehost", "faketoken") diff --git a/tests/test_swimlanes.py b/tests/test_swimlanes.py new file mode 100644 index 0000000..a83f823 --- /dev/null +++ b/tests/test_swimlanes.py @@ -0,0 +1,40 @@ +import unittest +from unittest.mock import patch + +from taiga.models import SwimLane, SwimLanes +from taiga.requestmaker import RequestMaker + +from .tools import MockResponse + + +class TestSwimLanes(unittest.TestCase): + @patch("taiga.models.base.ListResource._new_resource") + def test_create_swimlane(self, mock_new_resource): + rm = RequestMaker("/api/v1", "fakehost", "faketoken") + mock_new_resource.return_value = SwimLane(rm) + SwimLanes(rm).create(1, "SwimLane 1") + mock_new_resource.assert_called_with(payload={"project": 1, "name": "SwimLane 1"}) + + @patch("taiga.requestmaker.requests.delete") + def test_delete_swimlanes(self, requests_delete): + rm = RequestMaker(api_path="/api/v1", host="host", token="f4k3") + requests_delete.return_value = MockResponse(204, "") + SwimLanes(rm).delete(1, 2) + requests_delete.assert_called_with( + "host/api/v1/swimlanes/1", + headers={"Content-type": "application/json", "Authorization": "Bearer f4k3", "x-lazy-pagination": "True"}, + params={"moveTo": 2}, + verify=True, + ) + + @patch("taiga.requestmaker.requests.delete") + def test_delete_swimlane(self, requests_delete): + rm = RequestMaker(api_path="/api/v1", host="host", token="f4k3") + requests_delete.return_value = MockResponse(204, "") + SwimLane(rm, id=1).delete(2) + requests_delete.assert_called_with( + "host/api/v1/swimlanes/1", + headers={"Content-type": "application/json", "Authorization": "Bearer f4k3", "x-lazy-pagination": "True"}, + params={"moveTo": 2}, + verify=True, + )