#4302 Add logic to be able to quickly take/drop assignee on a PR
Merged 5 years ago by pingou. Opened 5 years ago by pingou.

file modified
+3
@@ -79,6 +79,7 @@ 

      )

      ENOISSUE = "Issue not found"

      EISSUENOTALLOWED = "You are not allowed to view this issue"

+     EPRNOTALLOWED = "You are not allowed to view this pull-request"

      EPULLREQUESTSDISABLED = (

          "Pull-Request have been deactivated for this " "project"

      )
@@ -565,6 +566,7 @@ 

          fork.api_pull_request_add_comment

      )

      api_pull_request_add_flag_doc = load_doc(fork.api_pull_request_add_flag)

+     api_pull_request_assign_doc = load_doc(fork.api_pull_request_assign)

  

      api_version_doc = load_doc(api_version)

      api_whoami_doc = load_doc(api_whoami)
@@ -630,6 +632,7 @@ 

              api_pull_request_close_doc,

              api_pull_request_add_comment_doc,

              api_pull_request_add_flag_doc,

+             api_pull_request_assign_doc,

          ],

          users=[

              api_users_doc,

file modified
+142 -212
@@ -31,11 +31,13 @@ 

      get_per_page,

  )

  from pagure.config import config as pagure_config

- from pagure.utils import (

-     authenticated,

-     is_repo_committer,

-     is_true,

-     api_authenticated,

+ from pagure.utils import is_repo_committer, is_true

+ from pagure.api.utils import (

+     _get_repo,

+     _check_token,

+     _get_request,

+     _check_pull_request,

+     _check_pull_request_access,

  )

  

  
@@ -146,17 +148,8 @@ 

  

      """

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

  

      status = flask.request.args.get("status", True)

      assignee = flask.request.args.get("assignee", None)
@@ -280,9 +273,8 @@ 

          }

  

      """

-     request = pagure.lib.query.get_request_by_uid(flask.g.session, uid)

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+ 

+     request = _get_request(requestuid=uid)

  

      # we don't really need the repo, but we need to make sure

      # that we're allowed to access it
@@ -378,24 +370,9 @@ 

  

      """

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     request = _get_request(repo, requestid)

  

      jsonout = flask.jsonify(request.to_json(public=True, api=True))

      return jsonout
@@ -452,27 +429,10 @@ 

      """  # noqa

      output = {}

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     if flask.g.token.project and repo != flask.g.token.project:

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo, project_token=False)

+     request = _get_request(repo, requestid)

  

      if not is_repo_committer(repo):

          raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
@@ -561,29 +521,10 @@ 

      """  # noqa

      output = {}

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     if (

-         api_authenticated() and flask.g.token and repo != flask.g.token.project

-     ) or not authenticated():

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo)

+     _get_request(repo, requestid)

  

      if not is_repo_committer(repo):

          raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
@@ -648,27 +589,10 @@ 

      """  # noqa

      output = {}

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     if repo != flask.g.token.project:

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo)

+     request = _get_request(repo, requestid)

  

      if not is_repo_committer(repo):

          raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
@@ -758,29 +682,13 @@ 

          }

  

      """  # noqa

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

  

      output = {}

  

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     if flask.g.token.project and repo != flask.g.token.project:

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo, project_token=False)

+     request = _get_request(repo, requestid)

  

      form = pagure.forms.AddPullRequestCommentForm(csrf_enabled=False)

      if form.validate_on_submit():
@@ -947,29 +855,13 @@ 

          }

  

      """  # noqa

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

  

      output = {}

  

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     if flask.g.token.project and repo != flask.g.token.project:

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo, project_token=False)

+     request = _get_request(repo, requestid)

  

      if "status" in get_request_data():

          form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False)
@@ -1104,26 +996,12 @@ 

          }

  

      """  # noqa

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

  

      output = {}

  

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     request = _get_request(repo, requestid)

  

      output = {"flags": []}

  
@@ -1192,34 +1070,12 @@ 

  

      """  # noqa

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

      output = {}

  

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     if (

-         api_authenticated()

-         and flask.g.token

-         and flask.g.token.project

-         and repo != flask.g.token.project

-     ) or not authenticated():

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo)

+     request = _get_request(repo, requestid)

  

      form = pagure.forms.SubscribtionForm(csrf_enabled=False)

      if form.validate_on_submit():
@@ -1350,15 +1206,9 @@ 

  

      """

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if flask.g.token.project and repo != flask.g.token.project:

-         raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo)

  

      form = pagure.forms.RequestPullForm(csrf_enabled=False)

      if not form.validate_on_submit():
@@ -1511,24 +1361,9 @@ 

  

      """  # noqa

  

-     repo = get_authorized_api_project(

-         flask.g.session, repo, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     if not repo.settings.get("pull_requests", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.EPULLREQUESTSDISABLED

-         )

- 

-     request = pagure.lib.query.search_pull_requests(

-         flask.g.session, project_id=repo.id, requestid=requestid

-     )

- 

-     if not request:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     request = _get_request(repo, requestid)

  

      repopath = None

      parentpath = pagure.utils.get_repo_path(request.project)
@@ -1604,3 +1439,98 @@ 

  

      jsonout = flask.jsonify(output)

      return jsonout

+ 

+ 

+ @API.route("/<repo>/pull-request/<int:requestid>/assign", methods=["POST"])

+ @API.route(

+     "/<namespace>/<repo>/pull-request/<int:requestid>/assign", methods=["POST"]

+ )

+ @API.route(

+     "/fork/<username>/<repo>/pull-request/<int:requestid>/assign",

+     methods=["POST"],

+ )

+ @API.route(

+     "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/assign",

+     methods=["POST"],

+ )

+ @api_login_required(acls=["pull_request_assign", "pull_request_update"])

+ @api_method

+ def api_pull_request_assign(repo, requestid, username=None, namespace=None):

+     """

+     Assign a pull-request

+     ---------------------

+     Assign a pull-request to someone.

+ 

+     ::

+ 

+         POST /api/0/<repo>/pull-request/<issue id>/assign

+         POST /api/0/<namespace>/<repo>/pull-request/<issue id>/assign

+ 

+     ::

+ 

+         POST /api/0/fork/<username>/<repo>/pull-request/<issue id>/assign

+         POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<issue id>/assign

+ 

+     Input

+     ^^^^^

+ 

+     +--------------+----------+---------------+---------------------------+

+     | Key          | Type     | Optionality   | Description               |

+     +==============+==========+===============+===========================+

+     | ``assignee`` | string   | Mandatory     | | The username of the user|

+     |              |          |               |   to assign the PR to.    |

+     +--------------+----------+---------------+---------------------------+

+ 

+     Sample response

+     ^^^^^^^^^^^^^^^

+ 

+     ::

+ 

+         {

+           "message": "pull-request assigned"

+         }

+ 

+     """  # noqa

+     output = {}

+     repo = _get_repo(repo, username, namespace)

+ 

+     _check_pull_request(repo)

+     _check_token(repo)

+ 

+     request = _get_request(repo, requestid)

+     _check_pull_request_access(request, assignee=True)

+ 

+     form = pagure.forms.AssignIssueForm(csrf_enabled=False)

+     if form.validate_on_submit():

+         assignee = form.assignee.data or None

+         # Create our metadata comment object

+         try:

+             # New comment

+             message = pagure.lib.query.add_pull_request_assignee(

+                 flask.g.session,

+                 request=request,

+                 assignee=assignee,

+                 user=flask.g.fas_user.username,

+             )

+             flask.g.session.commit()

+             if message:

+                 pagure.lib.query.add_metadata_update_notif(

+                     session=flask.g.session,

+                     obj=request,

+                     messages=message,

+                     user=flask.g.fas_user.username,

+                 )

+                 output["message"] = message

+             else:

+                 output["message"] = "Nothing to change"

+         except pagure.exceptions.PagureException as err:  # pragma: no cover

+             raise pagure.exceptions.APIError(

+                 400, error_code=APIERROR.ENOCODE, error=str(err)

+             )

+         except SQLAlchemyError as err:  # pragma: no cover

+             flask.g.session.rollback()

+             _log.exception(err)

+             raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

+ 

+     jsonout = flask.jsonify(output)

+     return jsonout

file modified
+8 -130
@@ -25,7 +25,6 @@ 

      api_login_required,

      api_login_optional,

      APIERROR,

-     get_authorized_api_project,

      get_request_data,

      get_page,

      get_per_page,
@@ -34,142 +33,21 @@ 

  from pagure.utils import (

      api_authenticated,

      is_repo_committer,

-     is_repo_user,

      urlpattern,

      is_true,

  )

- 

+ from pagure.api.utils import (

+     _get_repo,

+     _check_token,

+     _get_issue,

+     _check_issue_tracker,

+     _check_ticket_access,

+     _check_private_issue_access,

+ )

  

  _log = logging.getLogger(__name__)

  

  

- def _get_repo(repo_name, username=None, namespace=None):

-     """Check if repository exists and get repository name

-     :param repo_name: name of repository

-     :param username:

-     :param namespace:

-     :raises pagure.exceptions.APIError: when repository doesn't exist or

-         is disabled

-     :return: repository name

-     """

-     repo = get_authorized_api_project(

-         flask.g.session, repo_name, user=username, namespace=namespace

-     )

- 

-     if repo is None:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

- 

-     return repo

- 

- 

- def _check_issue_tracker(repo):

-     """Check if issue tracker is enabled for repository

-     :param repo: repository

-     :raises pagure.exceptions.APIError: when issue tracker is disabled

-     """

-     ticket_namespaces = pagure_config.get("ENABLE_TICKETS_NAMESPACE")

-     if (

-         ticket_namespaces

-         and repo.namespace

-         and repo.namespace not in ticket_namespaces

-     ) or not repo.settings.get("issue_tracker", True):

-         raise pagure.exceptions.APIError(

-             404, error_code=APIERROR.ETRACKERDISABLED

-         )

- 

-     # forbid all POST requests if the issue tracker is made read-only

-     if flask.request.method == "POST" and repo.settings.get(

-         "issue_tracker_read_only", False

-     ):

-         raise pagure.exceptions.APIError(

-             401, error_code=APIERROR.ETRACKERREADONLY

-         )

- 

- 

- def _check_token(repo, project_token=True):

-     """Check if token is valid for the repo

-     :param repo: repository name

-     :param project_token: set True when project token is required,

-         otherwise any token can be used

-     :raises pagure.exceptions.APIError: when token is not valid for repo

-     """

-     if api_authenticated():

-         # if there is a project associated with the token, check it

-         # if there is no project associated, check if it is required

-         if (

-             flask.g.token.project is not None and repo != flask.g.token.project

-         ) or (flask.g.token.project is None and project_token):

-             raise pagure.exceptions.APIError(

-                 401, error_code=APIERROR.EINVALIDTOK

-             )

- 

- 

- def _get_issue(repo, issueid, issueuid=None):

-     """Get issue and check permissions

-     :param repo: repository name

-     :param issueid: issue ID

-     :param issueuid: issue Unique ID

-     :raises pagure.exceptions.APIError: when issues doesn't exists

-     :return: issue

-     """

-     issue = pagure.lib.query.search_issues(

-         flask.g.session, repo, issueid=issueid, issueuid=issueuid

-     )

- 

-     if issue is None or issue.project != repo:

-         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)

- 

-     return issue

- 

- 

- def _check_private_issue_access(issue):

-     """Check if user can access issue. Must be repo committer

-     or author to see private issues.

-     :param issue: issue object

-     :raises pagure.exceptions.APIError: when access denied

-     """

-     if (

-         issue.private

-         and not is_repo_committer(issue.project)

-         and (

-             not api_authenticated()

-             or not issue.user.user == flask.g.fas_user.username

-         )

-     ):

-         raise pagure.exceptions.APIError(

-             403, error_code=APIERROR.EISSUENOTALLOWED

-         )

- 

- 

- def _check_ticket_access(issue, assignee=False, open_access=False):

-     """Check if user can access issue. Must be repo committer

-     or author to see private issues.

-     :param issue: issue object

-     :param assignee: a boolean specifying whether to allow the assignee or not

-         defaults to False

-     :raises pagure.exceptions.APIError: when access denied

-     """

-     # Private tickets require commit access

-     _check_private_issue_access(issue)

- 

-     error = False

-     if not open_access:

-         # Public tickets require ticket access

-         error = not is_repo_user(issue.project)

- 

-     if assignee:

-         if (

-             issue.assignee is not None

-             and issue.assignee.user == flask.g.fas_user.username

-         ):

-             error = False

- 

-     if error:

-         raise pagure.exceptions.APIError(

-             403, error_code=APIERROR.EISSUENOTALLOWED

-         )

- 

- 

  def _check_link_custom_field(field, links):

      """Check if the value provided in the link custom field

      is a link.

file added
+235
@@ -0,0 +1,235 @@ 

+ # -*- coding: utf-8 -*-

+ 

+ """

+  (c) 2015-2019 - Copyright Red Hat Inc

+ 

+  Authors:

+    Pierre-Yves Chibon <pingou@pingoured.fr>

+ 

+ """

+ 

+ from __future__ import print_function, unicode_literals, absolute_import

+ 

+ import flask

+ import logging

+ 

+ 

+ import pagure.exceptions

+ import pagure.lib.query

+ 

+ from pagure.config import config as pagure_config

+ from pagure.api import APIERROR, get_authorized_api_project

+ 

+ from pagure.utils import api_authenticated, is_repo_committer, is_repo_user

+ 

+ 

+ _log = logging.getLogger(__name__)

+ 

+ 

+ def _get_repo(repo_name, username=None, namespace=None):

+     """Check if repository exists and get repository name

+     :param repo_name: name of repository

+     :param username:

+     :param namespace:

+     :raises pagure.exceptions.APIError: when repository doesn't exist or

+         is disabled

+     :return: repository name

+     """

+     repo = get_authorized_api_project(

+         flask.g.session, repo_name, user=username, namespace=namespace

+     )

+ 

+     if repo is None:

+         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

+ 

+     return repo

+ 

+ 

+ def _check_token(repo, project_token=True):

+     """Check if token is valid for the repo

+     :param repo: repository name

+     :param project_token: set True when project token is required,

+         otherwise any token can be used

+     :raises pagure.exceptions.APIError: when token is not valid for repo

+     """

+     if api_authenticated():

+         # if there is a project associated with the token, check it

+         # if there is no project associated, check if it is required

+         if (

+             flask.g.token.project is not None and repo != flask.g.token.project

+         ) or (flask.g.token.project is None and project_token):

+             raise pagure.exceptions.APIError(

+                 401, error_code=APIERROR.EINVALIDTOK

+             )

+ 

+ 

+ def _get_issue(repo, issueid, issueuid=None):

+     """Get issue and check permissions

+     :param repo: repository name

+     :param issueid: issue ID

+     :param issueuid: issue Unique ID

+     :raises pagure.exceptions.APIError: when issues doesn't exists

+     :return: issue

+     """

+     issue = pagure.lib.query.search_issues(

+         flask.g.session, repo, issueid=issueid, issueuid=issueuid

+     )

+ 

+     if issue is None or issue.project != repo:

+         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)

+ 

+     return issue

+ 

+ 

+ def _get_request(repo=None, requestid=None, requestuid=None):

+     """Get pull-request if it exists

+     :param repo: repository name

+     :param requestid: pull-request ID

+     :param requestuid: pull-request Unique ID

+     :raises pagure.exceptions.APIError: when pull-request doesn't exists

+     :return: issue

+     """

+     request = None

+     if repo and requestid:

+         request = pagure.lib.query.search_pull_requests(

+             flask.g.session, project_id=repo.id, requestid=requestid

+         )

+     elif requestuid:

+         request = pagure.lib.query.get_request_by_uid(

+             flask.g.session, requestuid

+         )

+ 

+     if not request or (repo and request.project != repo):

+         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)

+ 

+     return request

+ 

+ 

+ def _check_issue_tracker(repo):

+     """Check if issue tracker is enabled for repository

+     :param repo: repository

+     :raises pagure.exceptions.APIError: when issue tracker is disabled

+     """

+     ticket_namespaces = pagure_config.get("ENABLE_TICKETS_NAMESPACE")

+     if (

+         ticket_namespaces

+         and repo.namespace

+         and repo.namespace not in ticket_namespaces

+     ) or not repo.settings.get("issue_tracker", True):

+         raise pagure.exceptions.APIError(

+             404, error_code=APIERROR.ETRACKERDISABLED

+         )

+ 

+     # forbid all POST requests if the issue tracker is made read-only

+     if flask.request.method == "POST" and repo.settings.get(

+         "issue_tracker_read_only", False

+     ):

+         raise pagure.exceptions.APIError(

+             401, error_code=APIERROR.ETRACKERREADONLY

+         )

+ 

+ 

+ def _check_pull_request(repo):

+     """Check if pull-requests are enabled for repository

+     :param repo: repository

+     :raises pagure.exceptions.APIError: when issue tracker is disabled

+     """

+     if not repo.settings.get("pull_requests", True):

+         raise pagure.exceptions.APIError(

+             404, error_code=APIERROR.EPULLREQUESTSDISABLED

+         )

+ 

+ 

+ def _check_ticket_access(issue, assignee=False, open_access=False):

+     """Check if user can access issue. Must be repo committer

+     or author to see private issues.

+     :param issue: issue object

+     :param assignee: a boolean specifying whether to allow the assignee or not

+         defaults to False

+     :raises pagure.exceptions.APIError: when access denied

+     """

+     # Private tickets require commit access

+     _check_private_issue_access(issue)

+ 

+     error = False

+     if not open_access:

+         # Public tickets require ticket access

+         error = not is_repo_user(issue.project)

+ 

+     if assignee:

+         if (

+             issue.assignee is not None

+             and issue.assignee.user == flask.g.fas_user.username

+         ):

+             error = False

+ 

+     if error:

+         raise pagure.exceptions.APIError(

+             403, error_code=APIERROR.EISSUENOTALLOWED

+         )

+ 

+ 

+ def _check_private_issue_access(issue):

+     """Check if user can access issue. Must be repo committer

+     or author to see private issues.

+     :param issue: issue object

+     :raises pagure.exceptions.APIError: when access denied

+     """

+     if (

+         issue.private

+         and not is_repo_committer(issue.project)

+         and (

+             not api_authenticated()

+             or not issue.user.user == flask.g.fas_user.username

+         )

+     ):

+         raise pagure.exceptions.APIError(

+             403, error_code=APIERROR.EISSUENOTALLOWED

+         )

+ 

+ 

+ def _check_pull_request_access(request, assignee=False):

+     """Check if user can access Pull-Request. Must be repo committer

+     or author to see private pull-requests.

+     :param request: PullRequest object

+     :param assignee: a boolean specifying whether to allow the assignee or not

+         defaults to False

+     :raises pagure.exceptions.APIError: when access denied

+     """

+     # Private PRs require commit access

+     _check_private_pull_request_access(request)

+ 

+     error = False

+     # Public tickets require ticket access

+     error = not is_repo_user(request.project)

+ 

+     if assignee:

+         if (

+             request.assignee is not None

+             and request.assignee.user == flask.g.fas_user.username

+         ):

+             error = False

+ 

+     if error:

+         raise pagure.exceptions.APIError(

+             403, error_code=APIERROR.EPRNOTALLOWED

+         )

+ 

+ 

+ def _check_private_pull_request_access(request):

+     """Check if user can access PR. Must be repo committer

+     or author to see private PR.

+     :param request: PullRequest object

+     :raises pagure.exceptions.APIError: when access denied

+     """

+     if (

+         request.private

+         and not is_repo_committer(request.project)

+         and (

+             not api_authenticated()

+             or not request.user.user == flask.g.fas_user.username

+         )

+     ):

+         raise pagure.exceptions.APIError(

+             403, error_code=APIERROR.EPRNOTALLOWED

+         )

@@ -345,6 +345,10 @@ 

      "pull_request_subscribe": (

          "Subscribe the user with this token to a pull-request"

      ),

+     "pull_request_assign": "Assign someone to a pull-request",

+     "pull_request_update": (

+         "Update a pull-request (title, description, assignee...)"

+     ),

      "update_watch_status": "Update the watch status on a project",

      "pull_request_rebase": "Rebase a pull-request",

  }

file modified
+1 -1
@@ -646,7 +646,7 @@ 

              ),

          )

  

-         return "Request reset"

+         return "Request assignee reset"

      elif assignee is None and request.assignee is None:

          return

  

file modified
+1 -1
@@ -975,7 +975,7 @@ 

    $.post (_url, _data ).done(

      function(data) {

        var _user_url = '\n<div class="ml-2"><div class="mt-1">{{g.fas_user.username| avatar(size=24) | safe}} '

-         + '<a href="{{ url_for("ui_ns.view_issues", repo=repo.name, username=username) }}'

+         + '<a href="{{ url_for("ui_ns.view_issues", repo=repo.name, username=username, namespace=repo.namespace) }}'

          + '?assignee={{ g.fas_user.username }}">'

          + '{{ g.fas_user.username }}</a>'

          + ' &mdash; <a href="javascript:void(0)" id="drop-btn" title="drop the assignment of this issue">Drop</a></div></div>';

@@ -509,13 +509,39 @@ 

                </fieldset>

          {% endif %}

          <fieldset class="form-group issue-metadata-display ml-1">

-           <label class="mb-0"><strong>Assignee</strong></label>

-           <div class="ml-2" title="{{ pull_request.assignee.html_title if pull_request.assignee else '' }}">

+           <label class="mb-1 pl-1"> <i class="fa fa-fw fa-user-plus"></i> <strong>Assignee</strong></label>

+           <div id="assignee_plain">

+             <div class="ml-2" title="{{ pull_request.assignee.html_title if pull_request.assignee else '' }}">

              {% if pull_request.assignee.username %}

-               <div class="mt-1">{{pull_request.assignee.username| avatar(size=24) | safe}} {{ pull_request.assignee.username }}</div>

+                 <div class="mt-1">{{pull_request.assignee.username| avatar(size=24) | safe}}

+                   <a href="{{ url_for(

+                     'ui_ns.request_pulls',

+                     repo=repo.name,

+                     username=username,

+                     namespace=repo.namespace,

+                     assignee=pull_request.assignee.username)

+                     }}" title="{{ pull_request.assignee.html_title }}">

+                     {{ pull_request.assignee.username }}

+                   </a>

+                   {% if g.authenticated and (pull_request.assignee.username == g.fas_user.username) %}

+                   &mdash; <a href="javascript:void(0)" id="drop-btn"

+                       title="drop the assignment of this pull-request">

+                     Drop

+                   </a>

+                 {% endif %}

+               </div>

              {% else %}

-               <span class="text-muted">None</span>

+               <div class="text-muted">

+                 <span class="text-muted">None</span>

+                   {% if g.authenticated and (g.repo_user or g.fas_user.username == pull_request.user.user) and pull_request.status|lower == 'open'

+                     and (not pull_request.assignee or pull_request.assignee.username != g.fas_user.username)

+                     and not repo.settings.get('pull_request_tracker_read_only', False) %}

+                     &mdash; <a href="javascript:void(0)" id="take-btn"

+                     title="assign this pull_request to you"> Take </a>

+                   {% endif %}

+                 </div>

              {% endif %}

+             </div>

            </div>

          </fieldset>

  
@@ -523,7 +549,7 @@ 

              g.repo_user

              or g.fas_user.username == pull_request.user.user) %}

          <fieldset class="form-group issue-metadata-form hidden">

-           <label for="tag"><strong>Tags</strong></label>

+           <label class="mb-1"><i class="fa fa-fw fa-tag"></i> <strong>Tags</strong></label>

             <input id="tag" type="text" placeholder="tag1, tag2" name="tag"

                title="comma separated list of tags"

                value="{{ pull_request.tags_text | join(',') }}" />
@@ -1522,6 +1548,74 @@ 

  $(document).on("mouseleave", "td.cell2", function() {

    $(this).find("a.open_changed_file_icon_wrap").css('visibility', 'hidden');

  });

+ 

+ {% if g.authenticated and (g.repo_user or pull_request.user.user == g.fas_user.username or open_access) %}

+ function take_issue(){

+   var _url = "{{ url_for('api_ns.api_pull_request_assign',

+             repo=repo.name, namespace=repo.namespace, username=username,

+             requestid=requestid) }}";

+   var _data = {assignee: "{{ g.fas_user.username }}"};

+   $.post (_url, _data ).done(

+     function(data) {

+       var _user_url = '\n<div class="ml-2"><div class="mt-1">{{g.fas_user.username| avatar(size=24) | safe}} '

+         + '<a href="{{ url_for("ui_ns.request_pulls", repo=repo.name, username=username, namespace=repo.namespace) }}'

+         + '?assignee={{ g.fas_user.username }}">'

+         + '{{ g.fas_user.username }}</a>'

+         + ' &mdash; <a href="javascript:void(0)" id="drop-btn" title="drop the assignment of this pull-request">Drop</a></div></div>';

+       $('#assignee_plain').html(_user_url);

+       $('#assignee').val("{{ g.fas_user.username }}");

+       setup_btn_take_drop();

+     }

+   ).fail(function() {

+     alert( "An error occured, could not assign this pull-request to you." );

+   })

+   return false;

+ }

+ {% endif %}

+ 

+ {% if g.authenticated and (

+     g.repo_user

+     or pull_request.user.user == g.fas_user.username

+     or pull_request.assignee.user == g.fas_user.username) %}

+ function drop_issue(){

+   var _url = "{{ url_for('api_ns.api_pull_request_assign',

+             repo=repo.name, namespace=repo.namespace, username=username,

+             requestid=requestid) }}";

+   var _data = {assignee: ""};

+   $.post( _url, _data ).done(

+     function(data) {

+       var _user_url = '<div class="ml-2">\n<span class="text-muted">None</span>'

+         + ' &mdash; <a href="javascript:void(0)" id="take-btn" title="assign this pull-request to you">Take</a></div>';

+       $('#assignee_plain').html(_user_url);

+       $('#assignee').val("");

+       setup_btn_take_drop();

+     }

+   ).fail(function() {

+     alert( "An error occured, could not drop the current assignee." );

+   })

+   return false;

+ }

+ {% endif %}

+ 

+ function setup_btn_take_drop(){

+   {% if g.authenticated and g.repo_user %}

+   $("#take-btn").click(take_issue)

+   {% endif %}

+   {% if g.authenticated and (

+     g.repo_user

+     or pull_request.user.user == g.fas_user.username

+     or pull_request.assignee.user == g.fas_user.username) %}

+   $("#drop-btn").click(drop_issue);

+   {% endif %}

+ }

+ 

+ {% if g.authenticated and (

+     g.repo_user

+     or pull_request.user.user == g.fas_user.username

+     or pull_request.assignee.user == g.fas_user.username) %}

+ setup_btn_take_drop();

+ {% endif %}

+ 

  </script>

  

  

file modified
+36 -13
@@ -236,22 +236,45 @@ 

          output = self.app.get('/api/0/-/error_codes')

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data), 34)

+         self.assertEqual(len(data), 35)

          self.assertEqual(

              sorted(data.keys()),

              [

-                 u'EDATETIME', u'EDBERROR', u'EGITERROR',

-                 u'EINVALIDISSUEFIELD', u'EINVALIDISSUEFIELD_LINK',

-                 u'EINVALIDPERPAGEVALUE', u'EINVALIDPRIORITY', u'EINVALIDREQ',

-                 u'EINVALIDTOK', u'EISSUENOTALLOWED',

-                 u'EMODIFYPROJECTNOTALLOWED', u'ENEWPROJECTDISABLED',

-                 u'ENOCODE', u'ENOCOMMENT', u'ENOCOMMIT', u'ENOGROUP',

-                 u'ENOISSUE', u'ENOPRCLOSE', u'ENOPROJECT', u'ENOPROJECTS',

-                 u'ENOPRSTATS', u'ENOREQ', u'ENOSIGNEDOFF', u'ENOTASSIGNED',

-                 u'ENOTASSIGNEE', u'ENOTHIGHENOUGH', u'ENOTMAINADMIN',

-                 u'ENOUSER', u'EPRCONFLICTS', u'EPRSCORE',

-                 u'EPULLREQUESTSDISABLED', u'ETIMESTAMP', u'ETRACKERDISABLED',

-                 u'ETRACKERREADONLY'

+                 'EDATETIME',

+                 'EDBERROR',

+                 'EGITERROR',

+                 'EINVALIDISSUEFIELD',

+                 'EINVALIDISSUEFIELD_LINK',

+                 'EINVALIDPERPAGEVALUE',

+                 'EINVALIDPRIORITY',

+                 'EINVALIDREQ',

+                 'EINVALIDTOK',

+                 'EISSUENOTALLOWED',

+                 'EMODIFYPROJECTNOTALLOWED',

+                 'ENEWPROJECTDISABLED',

+                 'ENOCODE',

+                 'ENOCOMMENT',

+                 'ENOCOMMIT',

+                 'ENOGROUP',

+                 'ENOISSUE',

+                 'ENOPRCLOSE',

+                 'ENOPROJECT',

+                 'ENOPROJECTS',

+                 'ENOPRSTATS',

+                 'ENOREQ',

+                 'ENOSIGNEDOFF',

+                 'ENOTASSIGNED',

+                 'ENOTASSIGNEE',

+                 'ENOTHIGHENOUGH',

+                 'ENOTMAINADMIN',

+                 'ENOUSER',

+                 'EPRCONFLICTS',

+                 'EPRNOTALLOWED',

+                 'EPRSCORE',

+                 'EPULLREQUESTSDISABLED',

+                 'ETIMESTAMP',

+                 'ETRACKERDISABLED',

+                 'ETRACKERREADONLY',

              ]

          )

  

@@ -0,0 +1,265 @@ 

+ # -*- coding: utf-8 -*-

+ 

+ """

+  (c) 2019 - Copyright Red Hat Inc

+ 

+  Authors:

+    Pierre-Yves Chibon <pingou@pingoured.fr>

+ 

+ """

+ 

+ from __future__ import unicode_literals, absolute_import

+ 

+ import arrow

+ import copy

+ import datetime

+ import unittest

+ import shutil

+ import sys

+ import time

+ import os

+ 

+ import flask

+ import json

+ import munch

+ from mock import patch, MagicMock

+ from sqlalchemy.exc import SQLAlchemyError

+ 

+ sys.path.insert(0, os.path.join(os.path.dirname(

+     os.path.abspath(__file__)), '..'))

+ 

+ import pagure.lib.query

+ import tests

+ 

+ 

+ class PagureFlaskApiForkAssigntests(tests.SimplePagureTest):

+     """ Tests for the flask API of pagure for assigning a PR """

+ 

+     maxDiff = None

+ 

+     @patch('pagure.lib.git.update_git', MagicMock(return_value=True))

+     @patch('pagure.lib.notify.send_email', MagicMock(return_value=True))

+     def setUp(self):

+         """ Set up the environnment, ran before every tests. """

+         super(PagureFlaskApiForkAssigntests, self).setUp()

+ 

+         tests.create_projects(self.session)

+         tests.add_content_git_repo(

+             os.path.join(self.path, "repos", "test.git"))

+ 

+         # Fork

+         project = pagure.lib.query.get_authorized_project(

+             self.session, 'test')

+         task = pagure.lib.query.fork_project(

+             session=self.session,

+             user='pingou',

+             repo=project,

+         )

+         self.session.commit()

+         self.assertEqual(

+             task.get(),

+             {'endpoint': 'ui_ns.view_repo',

+              'repo': 'test',

+              'namespace': None,

+              'username': 'pingou'})

+ 

+         tests.add_readme_git_repo(

+             os.path.join(self.path, "repos", "forks", "pingou", "test.git"))

+         project = pagure.lib.query.get_authorized_project(

+             self.session, 'test')

+         fork = pagure.lib.query.get_authorized_project(

+             self.session,

+             'test',

+             user='pingou',

+         )

+ 

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         req = pagure.lib.query.new_pull_request(

+             session=self.session,

+             repo_from=fork,

+             branch_from='master',

+             repo_to=project,

+             branch_to='master',

+             title='test pull-request',

+             user='pingou',

+         )

+         self.session.commit()

+         self.assertEqual(req.id, 1)

+         self.assertEqual(req.title, 'test pull-request')

+ 

+         # Assert the PR is open

+         self.session = pagure.lib.query.create_session(self.dbpath)

+         project = pagure.lib.query.get_authorized_project(

+             self.session, 'test')

+         self.assertEqual(len(project.requests), 1)

+         self.assertEqual(project.requests[0].status, "Open")

+         # Check how the PR renders in the API and the UI

+         output = self.app.get('/api/0/test/pull-request/1')

+         self.assertEqual(output.status_code, 200)

+         output = self.app.get('/test/pull-request/1')

+         self.assertEqual(output.status_code, 200)

+ 

+     def test_api_assign_pr_invalid_project_namespace(self):

+         """ Test api_pull_request_assign method when the project doesn't exist.

+         """

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         # Valid token, wrong project

+         output = self.app.post(

+             '/api/0/somenamespace/test3/pull-request/1/assign', headers=headers)

+         self.assertEqual(output.status_code, 401)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'error': 'Invalid or expired token. Please visit '

+                       'http://localhost.localdomain/settings#api-keys to get or renew your '

+                       'API token.',

+              'error_code': 'EINVALIDTOK'}

+ 

+         )

+ 

+     def test_api_assign_pr_invalid_project(self):

+         """ Test api_pull_request_assign method when the project doesn't exist.

+         """

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         # Invalid project

+         output = self.app.post('/api/0/foo/pull-request/1/assign', headers=headers)

+         self.assertEqual(output.status_code, 404)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {

+               "error": "Project not found",

+               "error_code": "ENOPROJECT",

+             }

+         )

+ 

+     def test_api_assign_pr_invalid_project_token(self):

+         """ Test api_pull_request_assign method when the token doesn't correspond

+         to the project.

+         """

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         # Valid token, wrong project

+         output = self.app.post('/api/0/test2/pull-request/1/assign', headers=headers)

+         self.assertEqual(output.status_code, 401)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertEqual(sorted(data.keys()), ['error', 'error_code'])

+         self.assertEqual(

+             pagure.api.APIERROR.EINVALIDTOK.value, data['error'])

+         self.assertEqual(

+             pagure.api.APIERROR.EINVALIDTOK.name, data['error_code'])

+ 

+     def test_api_assign_pr_invalid_pr(self):

+         """ Test api_pull_request_assign method when asking for an invalid PR

+         """

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         # No input

+         output = self.app.post('/api/0/test/pull-request/404/assign', headers=headers)

+         self.assertEqual(output.status_code, 404)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'error': 'Pull-Request not found', 'error_code': 'ENOREQ'}

+         )

+ 

+     def test_api_assign_pr_no_input(self):

+         """ Test api_pull_request_assign method when no input is specified

+         """

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         # No input

+         output = self.app.post('/api/0/test/pull-request/1/assign', headers=headers)

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'message': 'Nothing to change'}

+         )

+ 

+     def test_api_assign_pr_assigned(self):

+         """ Test api_pull_request_assign method when with valid input

+         """

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         data = {

+             'assignee': 'pingou',

+         }

+ 

+         # Valid request

+         output = self.app.post(

+             '/api/0/test/pull-request/1/assign', data=data, headers=headers)

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'message': 'Request assigned'}

+         )

+ 

+     def test_api_assign_pr_unassigned(self):

+         """ Test api_pull_request_assign method when unassigning

+         """

+         self.test_api_assign_pr_assigned()

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+         data = {}

+ 

+         # Un-assign

+         output = self.app.post(

+             '/api/0/test/pull-request/1/assign', data=data, headers=headers)

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'message': 'Request assignee reset'}

+         )

+ 

+     def test_api_assign_pr_unassigned_twice(self):

+         """ Test api_pull_request_assign method when unassigning

+         """

+         self.test_api_assign_pr_unassigned()

+         headers = {'Authorization': 'token aaabbbcccddd'}

+         data = {'assignee': None}

+ 

+         # Un-assign

+         output = self.app.post(

+             '/api/0/test/pull-request/1/assign', data=data, headers=headers)

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'message': 'Nothing to change'}

+         )

+ 

+     def test_api_assign_pr_unassigned_empty_string(self):

+         """ Test api_pull_request_assign method when unassigning with an

+         empty string

+         """

+         self.test_api_assign_pr_assigned()

+ 

+         headers = {'Authorization': 'token aaabbbcccddd'}

+ 

+         # Un-assign

+         data = {'assignee': ''}

+         output = self.app.post(

+             '/api/0/test/pull-request/1/assign', data=data, headers=headers)

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {'message': 'Request assignee reset'}

+         )

+ 

+ 

+ if __name__ == '__main__':

+     unittest.main(verbosity=2)

file modified
+3 -1
@@ -2998,7 +2998,7 @@ 

              assignee=None,

              user='foo',

          )

-         self.assertEqual(msg, 'Request reset')

+         self.assertEqual(msg, 'Request assignee reset')

  

          # Try resetting again

          msg = pagure.lib.query.add_pull_request_assignee(
@@ -5534,6 +5534,7 @@ 

                  'issue_update_custom_fields',

                  'issue_update_milestone',

                  'modify_project',

+                 'pull_request_assign',

                  'pull_request_close',

                  'pull_request_comment',

                  'pull_request_create',
@@ -5541,6 +5542,7 @@ 

                  'pull_request_merge',

                  'pull_request_rebase',

                  'pull_request_subscribe',

+                 'pull_request_update',

                  'update_watch_status',

              ]

          )

no initial comment

4 new commits added

  • Add missing namespace on the link to see the user's issues
  • Add a button to take/drop a pull-request (assignee field)
  • Add a new API endpoint to assign pull-request to someone
  • Move utility methods to used in different places of the API to a module
5 years ago

4 new commits added

  • Add missing namespace on the link to see the user's issues
  • Add a button to take/drop a pull-request (assignee field)
  • Add a new API endpoint to assign pull-request to someone
  • Move utility methods to used in different places of the API to a module
5 years ago

4 new commits added

  • Add missing namespace on the link to see the user's issues
  • Add a button to take/drop a pull-request (assignee field)
  • Add a new API endpoint to assign pull-request to someone
  • Move utility methods to used in different places of the API to a module
5 years ago

4 new commits added

  • Add missing namespace on the link to see the user's issues
  • Add a button to take/drop a pull-request (assignee field)
  • Add a new API endpoint to assign pull-request to someone
  • Move utility methods to used in different places of the API to a module
5 years ago

Pull-Request has been merged by pingou

5 years ago