| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent c7c139b commit 0dd6ab1
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -314,6 +314,8 @@ def update( | |||
| 314 | 314 | path = self.path | |
| 315 | 315 | else: | |
| 316 | 316 | path = f"{self.path}/{utils.EncodedId(id)}" | |
| 317 | + if "_pg_custom_path" in kwargs: | ||
| 318 | + path = kwargs.pop("_pg_custom_path") | ||
| 317 | 319 | ||
| 318 | 320 | excludes = [] | |
| 319 | 321 | if self._obj_cls is not None and self._obj_cls._id_attr is not None: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ | |||
| 2 | 2 | ||
| 3 | 3 | from typing import Any, TYPE_CHECKING | |
| 4 | 4 | ||
| 5 | + import gitlab.utils | ||
| 5 | 6 | from gitlab import exceptions as exc | |
| 6 | 7 | from gitlab import types | |
| 7 | 8 | from gitlab.base import RESTObject | |
@@ -29,6 +30,63 @@ class GroupEpic(ObjectDeleteMixin, SaveMixin, RESTObject): | |||
| 29 | 30 | resourcelabelevents: GroupEpicResourceLabelEventManager | |
| 30 | 31 | notes: GroupEpicNoteManager | |
| 31 | 32 | ||
| 33 | + def _epic_path(self) -> str: | ||
| 34 | + """Return the API path for this epic using its real group.""" | ||
| 35 | + if not hasattr(self, "group_id") or self.group_id is None: | ||
| 36 | + raise AttributeError( | ||
| 37 | + "Cannot compute epic path: attribute 'group_id' is missing." | ||
| 38 | + ) | ||
| 39 | + encoded_group_id = gitlab.utils.EncodedId(self.group_id) | ||
| 40 | + return f"/groups/{encoded_group_id}/epics/{self.encoded_id}" | ||
| 41 | + | ||
| 42 | + @exc.on_http_error(exc.GitlabUpdateError) | ||
| 43 | + def save(self, **kwargs: Any) -> dict[str, Any] | None: | ||
| 44 | + """Save the changes made to the object to the server. | ||
| 45 | + | ||
| 46 | + The object is updated to match what the server returns. | ||
| 47 | + | ||
| 48 | + This method uses the epic's group_id attribute to construct the correct | ||
| 49 | + API path. This is important when the epic was retrieved from a parent | ||
| 50 | + group but actually belongs to a sub-group. | ||
| 51 | + | ||
| 52 | + Args: | ||
| 53 | + **kwargs: Extra options to send to the server (e.g. sudo) | ||
| 54 | + | ||
| 55 | + Returns: | ||
| 56 | + The new object data (*not* a RESTObject) | ||
| 57 | + | ||
| 58 | + Raises: | ||
| 59 | + GitlabAuthenticationError: If authentication is not correct | ||
| 60 | + GitlabUpdateError: If the server cannot perform the request | ||
| 61 | + """ | ||
| 62 | + # Use the epic's actual group_id to construct the correct path. | ||
| 63 | + path = self._epic_path() | ||
| 64 | + | ||
| 65 | + # Call SaveMixin.save() method | ||
| 66 | + return super().save(_pg_custom_path=path, **kwargs) | ||
| 67 | + | ||
| 68 | + @exc.on_http_error(exc.GitlabDeleteError) | ||
| 69 | + def delete(self, **kwargs: Any) -> None: | ||
| 70 | + """Delete the object from the server. | ||
| 71 | + | ||
| 72 | + This method uses the epic's group_id attribute to construct the correct | ||
| 73 | + API path. This is important when the epic was retrieved from a parent | ||
| 74 | + group but actually belongs to a sub-group. | ||
| 75 | + | ||
| 76 | + Args: | ||
| 77 | + **kwargs: Extra options to send to the server (e.g. sudo) | ||
| 78 | + | ||
| 79 | + Raises: | ||
| 80 | + GitlabAuthenticationError: If authentication is not correct | ||
| 81 | + GitlabDeleteError: If the server cannot perform the request | ||
| 82 | + """ | ||
| 83 | + if TYPE_CHECKING: | ||
| 84 | + assert self.encoded_id is not None | ||
| 85 | + | ||
| 86 | + # Use the epic's actual group_id to construct the correct path. | ||
| 87 | + path = self._epic_path() | ||
| 88 | + self.manager.gitlab.http_delete(path, **kwargs) | ||
| 89 | + | ||
| 32 | 90 | ||
| 33 | 91 | class GroupEpicManager(CRUDMixin[GroupEpic]): | |
| 34 | 92 | _path = "/groups/{group_id}/epics" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,10 @@ | |||
| 1 | + import uuid | ||
| 2 | + | ||
| 1 | 3 | import pytest | |
| 2 | 4 | ||
| 5 | + import gitlab.exceptions | ||
| 6 | + from tests.functional import helpers | ||
| 7 | + | ||
| 3 | 8 | pytestmark = pytest.mark.gitlab_premium | |
| 4 | 9 | ||
| 5 | 10 | ||
@@ -32,3 +37,42 @@ def test_epic_notes(epic): | |||
| 32 | 37 | epic.notes.create({"body": "Test note"}) | |
| 33 | 38 | new_notes = epic.notes.list(get_all=True) | |
| 34 | 39 | assert len(new_notes) == (len(notes) + 1), f"{new_notes} {notes}" | |
| 40 | + | ||
| 41 | + | ||
| 42 | + def test_epic_save_from_parent_group_updates_subgroup_epic(gl, group): | ||
| 43 | + subgroup_id = uuid.uuid4().hex | ||
| 44 | + subgroup = gl.groups.create( | ||
| 45 | + { | ||
| 46 | + "name": f"subgroup-{subgroup_id}", | ||
| 47 | + "path": f"sg-{subgroup_id}", | ||
| 48 | + "parent_id": group.id, | ||
| 49 | + } | ||
| 50 | + ) | ||
| 51 | + | ||
| 52 | + nested_epic = subgroup.epics.create( | ||
| 53 | + {"title": f"Nested epic {subgroup_id}", "description": "Nested epic"} | ||
| 54 | + ) | ||
| 55 | + | ||
| 56 | + try: | ||
| 57 | + fetched_epics = group.epics.list(search=nested_epic.title) | ||
| 58 | + assert fetched_epics, "Expected to discover nested epic via parent group list" | ||
| 59 | + subgroup.epics.get(nested_epic.iid) | ||
| 60 | + | ||
| 61 | + fetched_epic = next( | ||
| 62 | + (epic for epic in fetched_epics if epic.id == nested_epic.id), None | ||
| 63 | + ) | ||
| 64 | + assert ( | ||
| 65 | + fetched_epic is not None | ||
| 66 | + ), "Parent group listing did not include nested epic" | ||
| 67 | + | ||
| 68 | + new_label = f"nested-{subgroup_id}" | ||
| 69 | + fetched_epic.labels = [new_label] | ||
| 70 | + fetched_epic.save() | ||
| 71 | + | ||
| 72 | + refreshed_epic = subgroup.epics.get(nested_epic.iid) | ||
| 73 | + assert new_label in refreshed_epic.labels | ||
| 74 | + finally: | ||
| 75 | + helpers.safe_delete(nested_epic) | ||
| 76 | + with pytest.raises(gitlab.exceptions.GitlabGetError): | ||
| 77 | + subgroup.epics.get(nested_epic.iid) | ||
| 78 | + helpers.safe_delete(subgroup) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,52 @@ | |||
| 1 | + import pytest | ||
| 2 | + import responses | ||
| 3 | + | ||
| 4 | + from gitlab.v4.objects.epics import GroupEpic | ||
| 5 | + | ||
| 6 | + | ||
| 7 | + def _build_epic(manager, iid=3, group_id=2, title="Epic"): | ||
| 8 | + data = {"iid": iid, "group_id": group_id, "title": title} | ||
| 9 | + return GroupEpic(manager, data) | ||
| 10 | + | ||
| 11 | + | ||
| 12 | + def test_group_epic_save_uses_actual_group_path(group): | ||
| 13 | + epic_manager = group.epics | ||
| 14 | + epic = _build_epic(epic_manager, title="Original") | ||
| 15 | + epic.title = "Updated" | ||
| 16 | + | ||
| 17 | + with responses.RequestsMock() as rsps: | ||
| 18 | + rsps.add( | ||
| 19 | + method=responses.PUT, | ||
| 20 | + url="http://localhost/api/v4/groups/2/epics/3", | ||
| 21 | + json={"iid": 3, "group_id": 2, "title": "Updated"}, | ||
| 22 | + content_type="application/json", | ||
| 23 | + status=200, | ||
| 24 | + match=[responses.matchers.json_params_matcher({"title": "Updated"})], | ||
| 25 | + ) | ||
| 26 | + | ||
| 27 | + epic.save() | ||
| 28 | + | ||
| 29 | + assert epic.title == "Updated" | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + def test_group_epic_delete_uses_actual_group_path(group): | ||
| 33 | + epic_manager = group.epics | ||
| 34 | + epic = _build_epic(epic_manager) | ||
| 35 | + | ||
| 36 | + with responses.RequestsMock() as rsps: | ||
| 37 | + rsps.add( | ||
| 38 | + method=responses.DELETE, | ||
| 39 | + url="http://localhost/api/v4/groups/2/epics/3", | ||
| 40 | + status=204, | ||
| 41 | + ) | ||
| 42 | + | ||
| 43 | + epic.delete() | ||
| 44 | + | ||
| 45 | + assert len(epic._updated_attrs) == 0 | ||
| 46 | + | ||
| 47 | + | ||
| 48 | + def test_group_epic_path_requires_group_id(fake_manager): | ||
| 49 | + epic = GroupEpic(manager=fake_manager, attrs={"iid": 5}) | ||
| 50 | + | ||
| 51 | + with pytest.raises(AttributeError): | ||
| 52 | + epic._epic_path() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments