Unverified Commit 0d41da3c authored by gerry-pratama's avatar gerry-pratama Committed by GitHub
Browse files

feat(api): add project templates (#3057)

* feat(api): Added project template classes to templates.py
* feat(api): Added project template managers to Project in project.py
* docs(merge_requests): Add example of creating mr with description template
* test(templates): Added unit tests for templates
* docs(templates): added section for project templates
parent 735efff8
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -95,6 +95,7 @@ Get a single MR::
    mr = project.mergerequests.get(mr_iid)

Get MR reviewer details::

    mr = project.mergerequests.get(mr_iid)
    reviewers = mr.reviewer_details.list()

@@ -105,6 +106,13 @@ Create a MR::
                                       'title': 'merge cool feature',
                                       'labels': ['label1', 'label2']})

    # Use a project MR description template
    mr_description_template = project.merge_request_templates.get("Default")
    mr = project.mergerequests.create({'source_branch': 'cool_feature',
                                       'target_branch': 'main',
                                       'title': 'merge cool feature',
                                       'description': mr_description_template.content})

Update a MR::

    mr.description = 'New description'
+71 −1
Original line number Diff line number Diff line
@@ -99,7 +99,7 @@ Reference
  + :class:`gitlab.v4.objects.DockerfileManager`
  + :attr:`gitlab.Gitlab.gitlabciymls`

* GitLab API: Not documented.
* GitLab API: https://docs.gitlab.com/ce/api/templates/dockerfiles.html

Examples
--------
@@ -112,3 +112,73 @@ Get a Dockerfile template::

    dockerfile = gl.dockerfiles.get('Python')
    print(dockerfile.content)

Project templates
=========================

These templates are project-specific versions of the templates above, as
well as issue and merge request templates.

Reference
---------

* v4 API:

  + :class:`gitlab.v4.objects.ProjectLicenseTemplate`
  + :class:`gitlab.v4.objects.ProjectLicenseTemplateManager`
  + :attr:`gitlab.v4.objects.Project.license_templates`
  + :class:`gitlab.v4.objects.ProjectGitignoreTemplate`
  + :class:`gitlab.v4.objects.ProjectGitignoreTemplateManager`
  + :attr:`gitlab.v4.objects.Project.gitignore_templates`
  + :class:`gitlab.v4.objects.ProjectGitlabciymlTemplate`
  + :class:`gitlab.v4.objects.ProjectGitlabciymlTemplateManager`
  + :attr:`gitlab.v4.objects.Project.gitlabciyml_templates`
  + :class:`gitlab.v4.objects.ProjectDockerfileTemplate`
  + :class:`gitlab.v4.objects.ProjectDockerfileTemplateManager`
  + :attr:`gitlab.v4.objects.Project.dockerfile_templates`
  + :class:`gitlab.v4.objects.ProjectIssueTemplate`
  + :class:`gitlab.v4.objects.ProjectIssueTemplateManager`
  + :attr:`gitlab.v4.objects.Project.issue_templates`
  + :class:`gitlab.v4.objects.ProjectMergeRequestTemplate`
  + :class:`gitlab.v4.objects.ProjectMergeRequestTemplateManager`
  + :attr:`gitlab.v4.objects.Project.merge_request_templates`

* GitLab API: https://docs.gitlab.com/ce/api/project_templates.html

Examples
--------

List known project templates::

    license_templates = project.license_templates.list()
    gitignore_templates = project.gitignore_templates.list()
    gitlabciyml_templates = project.gitlabciyml_templates.list()
    dockerfile_templates = project.dockerfile_templates.list()
    issue_templates = project.issue_templates.list()
    merge_request_templates = project.merge_request_templates.list()

Get project templates::
  
      license_template = project.license_templates.get('apache-2.0')
      gitignore_template = project.gitignore_templates.get('Python')
      gitlabciyml_template = project.gitlabciyml_templates.get('Pelican')
      dockerfile_template = project.dockerfile_templates.get('Python')
      issue_template = project.issue_templates.get('Default')
      merge_request_template = project.merge_request_templates.get('Default')

      print(license_template.content)
      print(gitignore_template.content)
      print(gitlabciyml_template.content)
      print(dockerfile_template.content)
      print(issue_template.content)
      print(merge_request_template.content)

Create an issue or merge request using a description template::

      issue = project.issues.create({'title': 'I have a bug',
                                     'description': issue_template.content})
      mr = project.mergerequests.create({'source_branch': 'cool_feature',
                                        'target_branch': 'main',
                                        'title': 'merge cool feature',
                                        'description': merge_request_template.content})
                          
 No newline at end of file
+14 −0
Original line number Diff line number Diff line
@@ -100,6 +100,14 @@ from .statistics import ( # noqa: F401
    ProjectIssuesStatisticsManager,
)
from .tags import ProjectProtectedTagManager, ProjectTagManager  # noqa: F401
from .templates import (  # noqa: F401
    ProjectDockerfileTemplateManager,
    ProjectGitignoreTemplateManager,
    ProjectGitlabciymlTemplateManager,
    ProjectIssueTemplateManager,
    ProjectLicenseTemplateManager,
    ProjectMergeRequestTemplateManager,
)
from .triggers import ProjectTriggerManager  # noqa: F401
from .users import ProjectUserManager  # noqa: F401
from .variables import ProjectVariableManager  # noqa: F401
@@ -189,27 +197,33 @@ class Project(
    customattributes: ProjectCustomAttributeManager
    deployments: ProjectDeploymentManager
    deploytokens: ProjectDeployTokenManager
    dockerfile_templates: ProjectDockerfileTemplateManager
    environments: ProjectEnvironmentManager
    events: ProjectEventManager
    exports: ProjectExportManager
    files: ProjectFileManager
    forks: "ProjectForkManager"
    generic_packages: GenericPackageManager
    gitignore_templates: ProjectGitignoreTemplateManager
    gitlabciyml_templates: ProjectGitlabciymlTemplateManager
    groups: ProjectGroupManager
    hooks: ProjectHookManager
    imports: ProjectImportManager
    integrations: ProjectIntegrationManager
    invitations: ProjectInvitationManager
    issues: ProjectIssueManager
    issue_templates: ProjectIssueTemplateManager
    issues_statistics: ProjectIssuesStatisticsManager
    iterations: ProjectIterationManager
    jobs: ProjectJobManager
    job_token_scope: ProjectJobTokenScopeManager
    keys: ProjectKeyManager
    labels: ProjectLabelManager
    license_templates: ProjectLicenseTemplateManager
    members: ProjectMemberManager
    members_all: ProjectMemberAllManager
    mergerequests: ProjectMergeRequestManager
    merge_request_templates: ProjectMergeRequestTemplateManager
    merge_trains: ProjectMergeTrainManager
    milestones: ProjectMilestoneManager
    notes: ProjectNoteManager
+104 −0
Original line number Diff line number Diff line
@@ -12,6 +12,18 @@ __all__ = [
    "GitlabciymlManager",
    "License",
    "LicenseManager",
    "ProjectDockerfileTemplate",
    "ProjectDockerfileTemplateManager",
    "ProjectGitignoreTemplate",
    "ProjectGitignoreTemplateManager",
    "ProjectGitlabciymlTemplate",
    "ProjectGitlabciymlTemplateManager",
    "ProjectIssueTemplate",
    "ProjectIssueTemplateManager",
    "ProjectLicenseTemplate",
    "ProjectLicenseTemplateManager",
    "ProjectMergeRequestTemplate",
    "ProjectMergeRequestTemplateManager",
]


@@ -65,3 +77,95 @@ class LicenseManager(RetrieveMixin, RESTManager):

    def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> License:
        return cast(License, super().get(id=id, lazy=lazy, **kwargs))


class ProjectDockerfileTemplate(RESTObject):
    _id_attr = "name"


class ProjectDockerfileTemplateManager(RetrieveMixin, RESTManager):
    _path = "/projects/{project_id}/templates/dockerfiles"
    _obj_cls = ProjectDockerfileTemplate
    _from_parent_attrs = {"project_id": "id"}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectDockerfileTemplate:
        return cast(ProjectDockerfileTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectGitignoreTemplate(RESTObject):
    _id_attr = "name"


class ProjectGitignoreTemplateManager(RetrieveMixin, RESTManager):
    _path = "/projects/{project_id}/templates/gitignores"
    _obj_cls = ProjectGitignoreTemplate
    _from_parent_attrs = {"project_id": "id"}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectGitignoreTemplate:
        return cast(ProjectGitignoreTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectGitlabciymlTemplate(RESTObject):
    _id_attr = "name"


class ProjectGitlabciymlTemplateManager(RetrieveMixin, RESTManager):
    _path = "/projects/{project_id}/templates/gitlab_ci_ymls"
    _obj_cls = ProjectGitlabciymlTemplate
    _from_parent_attrs = {"project_id": "id"}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectGitlabciymlTemplate:
        return cast(ProjectGitlabciymlTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectLicenseTemplate(RESTObject):
    _id_attr = "key"


class ProjectLicenseTemplateManager(RetrieveMixin, RESTManager):
    _path = "/projects/{project_id}/templates/licenses"
    _obj_cls = ProjectLicenseTemplate
    _from_parent_attrs = {"project_id": "id"}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectLicenseTemplate:
        return cast(ProjectLicenseTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectIssueTemplate(RESTObject):
    _id_attr = "name"


class ProjectIssueTemplateManager(RetrieveMixin, RESTManager):
    _path = "/projects/{project_id}/templates/issues"
    _obj_cls = ProjectIssueTemplate
    _from_parent_attrs = {"project_id": "id"}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectIssueTemplate:
        return cast(ProjectIssueTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectMergeRequestTemplate(RESTObject):
    _id_attr = "name"


class ProjectMergeRequestTemplateManager(RetrieveMixin, RESTManager):
    _path = "/projects/{project_id}/templates/merge_requests"
    _obj_cls = ProjectMergeRequestTemplate
    _from_parent_attrs = {"project_id": "id"}

    def get(
        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
    ) -> ProjectMergeRequestTemplate:
        return cast(
            ProjectMergeRequestTemplate, super().get(id=id, lazy=lazy, **kwargs)
        )
+106 −0
Original line number Diff line number Diff line
"""
Gitlab API:
https://docs.gitlab.com/ce/api/templates/dockerfiles.html
https://docs.gitlab.com/ce/api/templates/gitignores.html
https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html
https://docs.gitlab.com/ce/api/templates/licenses.html
https://docs.gitlab.com/ce/api/project_templates.html
"""

import pytest
import responses

from gitlab.v4.objects import (
    Dockerfile,
    Gitignore,
    Gitlabciyml,
    License,
    ProjectDockerfileTemplate,
    ProjectGitignoreTemplate,
    ProjectGitlabciymlTemplate,
    ProjectIssueTemplate,
    ProjectLicenseTemplate,
    ProjectMergeRequestTemplate,
)


@pytest.mark.parametrize(
    "tmpl, tmpl_mgr, tmpl_path",
    [
        (Dockerfile, "dockerfiles", "dockerfiles"),
        (Gitignore, "gitignores", "gitignores"),
        (Gitlabciyml, "gitlabciymls", "gitlab_ci_ymls"),
        (License, "licenses", "licenses"),
    ],
    ids=[
        "dockerfile",
        "gitignore",
        "gitlabciyml",
        "license",
    ],
)
def test_get_template(gl, tmpl, tmpl_mgr, tmpl_path):
    tmpl_id = "sample"
    tmpl_content = {"name": tmpl_id, "content": "Sample template content"}

    # License templates have 'key' as the id attribute, so ensure
    # this is included in the response content
    if tmpl == License:
        tmpl_id = "smpl"
        tmpl_content.update({"key": tmpl_id})

    path = f"templates/{tmpl_path}/{tmpl_id}"
    with responses.RequestsMock() as rsps:
        rsps.add(
            method=responses.GET,
            url=f"http://localhost/api/v4/{path}",
            json=tmpl_content,
        )

        template = getattr(gl, tmpl_mgr).get(tmpl_id)

    assert isinstance(template, tmpl)
    assert getattr(template, template._id_attr) == tmpl_id


@pytest.mark.parametrize(
    "tmpl, tmpl_mgr, tmpl_path",
    [
        (ProjectDockerfileTemplate, "dockerfile_templates", "dockerfiles"),
        (ProjectGitignoreTemplate, "gitignore_templates", "gitignores"),
        (ProjectGitlabciymlTemplate, "gitlabciyml_templates", "gitlab_ci_ymls"),
        (ProjectLicenseTemplate, "license_templates", "licenses"),
        (ProjectIssueTemplate, "issue_templates", "issues"),
        (ProjectMergeRequestTemplate, "merge_request_templates", "merge_requests"),
    ],
    ids=[
        "dockerfile",
        "gitignore",
        "gitlabciyml",
        "license",
        "issue",
        "mergerequest",
    ],
)
def test_get_project_template(project, tmpl, tmpl_mgr, tmpl_path):
    tmpl_id = "sample"
    tmpl_content = {"name": tmpl_id, "content": "Sample template content"}

    # ProjectLicenseTemplate templates have 'key' as the id attribute, so ensure
    # this is included in the response content
    if tmpl == ProjectLicenseTemplate:
        tmpl_id = "smpl"
        tmpl_content.update({"key": tmpl_id})

    path = f"projects/{project.id}/templates/{tmpl_path}/{tmpl_id}"
    with responses.RequestsMock() as rsps:
        rsps.add(
            method=responses.GET,
            url=f"http://localhost/api/v4/{path}",
            json=tmpl_content,
        )

        template = getattr(project, tmpl_mgr).get(tmpl_id)

    assert isinstance(template, tmpl)
    assert getattr(template, template._id_attr) == tmpl_id