Blob Blame Raw
# -*- coding: utf-8 -*-

"""
 (c) 2015 - Copyright Red Hat Inc

 Authors:
   Pierre-Yves Chibon <pingou@pingoured.fr>

"""

import flask

from sqlalchemy.exc import SQLAlchemyError

import pagure
import pagure.exceptions
import pagure.lib
from pagure import APP, SESSION, is_repo_admin, api_authenticated
from pagure.api import (
    API, api_method, api_login_required, api_login_optional, APIERROR
)


@API.route('/<repo>/new_issue', methods=['POST'])
@API.route('/fork/<username>/<repo>/new_issue', methods=['POST'])
@api_login_required(acls=['issue_create'])
@api_method
def api_new_issue(repo, username=None):
    """
    Create a new issue
    ------------------
    Open a new issue on a project.

    ::

        POST /api/0/<repo>/new_issue

    ::

        POST /api/0/fork/<username>/<repo>/new_issue

    Input
    ^^^^^

    +---------------+-----------+---------------+--------------------------------------------------------------+
    | Key           | Type      | Optionality   | Description                                                  |
    +===============+===========+===============+==============================================================+
    | ``title``     | string    | Mandatory     | The title of the issue                                       |
    +---------------+-----------+---------------+--------------------------------------------------------------+
    | ``content``   | string    | Mandatory     | The description of the issue                                 |
    +---------------+-----------+---------------+--------------------------------------------------------------+
    | ``private``   | boolean   | Optional      | Include this key if you want a private issue to be created   |
    +---------------+-----------+---------------+--------------------------------------------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "message": "Issue created"
        }

    """
    repo = pagure.lib.get_project(SESSION, repo, user=username)
    output = {}

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if not repo.settings.get('issue_tracker', True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ETRACKERDISABLED)

    if repo != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    form = pagure.forms.IssueFormSimplied(csrf_enabled=False)
    if form.validate_on_submit():
        title = form.title.data
        content = form.issue_content.data
        private = form.private.data

        try:
            issue = pagure.lib.new_issue(
                SESSION,
                repo=repo,
                title=title,
                content=content,
                private=private or False,
                user=flask.g.fas_user.username,
                ticketfolder=APP.config['TICKETS_FOLDER'],
            )
            SESSION.flush()
            # If there is a file attached, attach it.
            filestream = flask.request.files.get('filestream')
            if filestream and '<!!image>' in issue.content:
                new_filename = pagure.lib.git.add_file_to_git(
                    repo=repo,
                    issue=issue,
                    ticketfolder=APP.config['TICKETS_FOLDER'],
                    user=flask.g.fas_user,
                    filename=filestream.filename,
                    filestream=filestream.stream,
                )
                # Replace the <!!image> tag in the comment with the link
                # to the actual image
                filelocation = flask.url_for(
                    'view_issue_raw_file',
                    repo=repo.name,
                    username=username,
                    filename=new_filename,
                )
                new_filename = new_filename.split('-', 1)[1]
                url = '[![%s](%s)](%s)' % (
                    new_filename, filelocation, filelocation)
                issue.content = issue.content.replace('<!!image>', url)
                SESSION.add(issue)
                SESSION.flush()

            SESSION.commit()
            output['message'] = 'Issue created'
        except SQLAlchemyError, err:  # pragma: no cover
            SESSION.rollback()
            APP.logger.exception(err)
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

    else:
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    jsonout = flask.jsonify(output)
    return jsonout


@API.route('/<repo>/issues')
@API.route('/fork/<username>/<repo>/issues')
@api_login_optional()
@api_method
def api_view_issues(repo, username=None):
    """
    List project's issues
    ---------------------
    List issues of a project.

    ::

        GET /api/0/<repo>/issues

    ::

        GET /api/0/fork/<username>/<repo>/issues

    Parameters
    ^^^^^^^^^^

    +----------------+----------+---------------+--------------------------------------------------------------------------------------------------------------------------------+
    | Key            | Type     | Optionality   | Description                                                                                                                    |
    +================+==========+===============+================================================================================================================================+
    | ``status``     | string   | Optional      | Filters the status of issues. Default: ``Open``                                                                                |
    +----------------+----------+---------------+--------------------------------------------------------------------------------------------------------------------------------+
    | ``tags``       | string   | Optional      | A list of tags you wish to filter. If you want to filter for issues not having a tag, add an exclamation mark in front of it   |
    +----------------+----------+---------------+--------------------------------------------------------------------------------------------------------------------------------+
    | ``assignee``   | string   | Optional      | Filter the issues by assignee                                                                                                  |
    +----------------+----------+---------------+--------------------------------------------------------------------------------------------------------------------------------+
    | ``author``     | string   | Optional      | Filter the issues by creator                                                                                                   |
    +----------------+----------+---------------+--------------------------------------------------------------------------------------------------------------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "args": {
            "assignee": null,
            "author": null,
            "status": "Closed",
            "tags": [
              "0.1"
            ]
          },
          "issues": [
            {
              "assignee": null,
              "blocks": [],
              "comments": [],
              "content": "asd",
              "date_created": "1427442217",
              "depends": [],
              "id": 4,
              "private": false,
              "status": "Fixed",
              "tags": [
                "0.1"
              ],
              "title": "bug",
              "user": {
                "fullname": "PY.C",
                "name": "pingou"
              }
            }
          ]
        }

    """

    repo = pagure.lib.get_project(SESSION, repo, user=username)

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if not repo.settings.get('issue_tracker', True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ETRACKERDISABLED)

    status = flask.request.args.get('status', None)
    tags = flask.request.args.getlist('tags')
    tags = [tag.strip() for tag in tags if tag.strip()]
    assignee = flask.request.args.get('assignee', None)
    author = flask.request.args.get('author', None)

    # Hide private tickets
    private = False
    # If user is authenticated, show him/her his/her private tickets
    if api_authenticated():
        if repo != flask.g.token.project:
            raise pagure.exceptions.APIError(
                401, error_code=APIERROR.EINVALIDTOK)
        private = flask.g.fas_user.username
    # If user is repo admin, show all tickets included the private ones
    if is_repo_admin(repo):
        private = None

    if status is not None:
        if status.lower() == 'closed':
            issues = pagure.lib.search_issues(
                SESSION,
                repo,
                closed=True,
                tags=tags,
                assignee=assignee,
                author=author,
                private=private,
            )
        else:
            issues = pagure.lib.search_issues(
                SESSION,
                repo,
                status=status,
                tags=tags,
                assignee=assignee,
                author=author,
                private=private,
            )
    else:
        issues = pagure.lib.search_issues(
            SESSION, repo, status='Open', tags=tags, assignee=assignee,
            author=author, private=private)

    jsonout = flask.jsonify({
        'issues': [issue.to_json(public=True) for issue in issues],
        'args': {
            'status': status,
            'tags': tags,
            'assignee': assignee,
            'author': author,
        }
    })
    return jsonout


@API.route('/<repo>/issue/<int:issueid>')
@API.route('/fork/<username>/<repo>/issue/<int:issueid>')
@api_login_optional()
@api_method
def api_view_issue(repo, issueid, username=None):
    """
    Issue information
    -----------------
    Retrieve information of a specific issue.

    ::

        GET /api/0/<repo>/issue/<issue id>

    ::

        GET /api/0/fork/<username>/<repo>/issue/<issue id>

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "assignee": null,
          "blocks": [],
          "comments": [],
          "content": "This issue needs attention",
          "date_created": "1431414800",
          "depends": [],
          "id": 1,
          "private": false,
          "status": "Open",
          "tags": [],
          "title": "test issue",
          "user": {
            "fullname": "PY C",
            "name": "pingou"
          }
        }

    """

    repo = pagure.lib.get_project(SESSION, repo, user=username)

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if not repo.settings.get('issue_tracker', True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ETRACKERDISABLED)

    issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)

    if issue is None or issue.project != repo:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)

    if api_authenticated():
        if repo != flask.g.token.project:
            raise pagure.exceptions.APIError(
                401, error_code=APIERROR.EINVALIDTOK)

    if issue.private and not is_repo_admin(repo) \
            and (not api_authenticated() or
                 not issue.user.user == flask.g.fas_user.username):
        raise pagure.exceptions.APIError(
            403, error_code=APIERROR.EISSUENOTALLOWED)

    jsonout = flask.jsonify(issue.to_json(public=True))
    return jsonout


@API.route('/<repo>/issue/<int:issueid>/status', methods=['POST'])
@API.route('/fork/<username>/<repo>/<int:issueid>/status', methods=['POST'])
@api_login_required(acls=['issue_change_status'])
@api_method
def api_change_status_issue(repo, issueid, username=None):
    """
    Change issue status
    -------------------
    Change the status of an issue.

    ::

        POST /api/0/<repo>/issue/<issue id>/status

    ::

        POST /api/0/fork/<username>/<repo>/issue/<issue id>/status

    Input
    ^^^^^

    +--------------+----------+---------------+-------------------------------+
    | Key          | Type     | Optionality   | Description                   |
    +==============+==========+===============+===============================+
    | ``status``   | string   | Mandatory     | The new status of the issue   |
    +--------------+----------+---------------+-------------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "message": "Successfully edited issue #1"
        }

    """
    repo = pagure.lib.get_project(SESSION, repo, user=username)
    output = {}

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if not repo.settings.get('issue_tracker', True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ETRACKERDISABLED)

    if repo != flask.g.token.project:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)

    if issue is None or issue.project != repo:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)

    if issue.private and not is_repo_admin(repo) \
            and (not api_authenticated() or
                 not issue.user.user == flask.g.fas_user.username):
        raise pagure.exceptions.APIError(
            403, error_code=APIERROR.EISSUENOTALLOWED)

    status = pagure.lib.get_issue_statuses(SESSION)
    form = pagure.forms.StatusForm(status=status, csrf_enabled=False)
    if form.validate_on_submit():
        new_status = form.status.data
        try:
            # Update status
            message = pagure.lib.edit_issue(
                SESSION,
                issue=issue,
                status=new_status,
                user=flask.g.fas_user.username,
                ticketfolder=APP.config['TICKETS_FOLDER'],
            )
            SESSION.commit()
            if message:
                output['message'] = message
            else:
                output['message'] = 'No changes'
        except pagure.exceptions.PagureException, err:
            raise pagure.exceptions.APIError(
                400, error_code=APIERROR.ENOCODE, error=str(err))
        except SQLAlchemyError, err:  # pragma: no cover
            SESSION.rollback()
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

    else:
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    jsonout = flask.jsonify(output)
    return jsonout


@API.route('/<repo>/issue/<int:issueid>/comment', methods=['POST'])
@API.route('/fork/<username>/<repo>/<int:issueid>/comment', methods=['POST'])
@api_login_required(acls=['issue_comment'])
@api_method
def api_comment_issue(repo, issueid, username=None):
    """
    Comment to an issue
    -------------------
    Add a comment to an issue.

    ::

        POST /api/0/<repo>/issue/<issue id>/comment

    ::

        POST /api/0/fork/<username>/<repo>/issue/<issue id>/comment

    Input
    ^^^^^

    +---------------+----------+---------------+-----------------------------------+
    | Key           | Type     | Optionality   | Description                       |
    +===============+==========+===============+===================================+
    | ``comment``   | string   | Mandatory     | The comment to add to the issue   |
    +---------------+----------+---------------+-----------------------------------+

    Sample response
    ^^^^^^^^^^^^^^^

    ::

        {
          "message": "Comment added"
        }

    """
    repo = pagure.lib.get_project(SESSION, repo, user=username)
    output = {}

    if repo is None:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

    if not repo.settings.get('issue_tracker', True):
        raise pagure.exceptions.APIError(
            404, error_code=APIERROR.ETRACKERDISABLED)

    if repo.fullname != flask.g.token.project.fullname:
        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)

    issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)

    if issue is None or issue.project != repo:
        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)

    if issue.private and not is_repo_admin(repo) \
            and (not api_authenticated() or
                 not issue.user.user == flask.g.fas_user.username):
        raise pagure.exceptions.APIError(
            403, error_code=APIERROR.EISSUENOTALLOWED)

    form = pagure.forms.CommentForm(csrf_enabled=False)
    if form.validate_on_submit():
        comment = form.comment.data
        try:
            # New comment
            message = pagure.lib.add_issue_comment(
                SESSION,
                issue=issue,
                comment=comment,
                user=flask.g.fas_user.username,
                ticketfolder=APP.config['TICKETS_FOLDER'],
            )
            SESSION.commit()
            output['message'] = message
        except SQLAlchemyError, err:  # pragma: no cover
            SESSION.rollback()
            APP.logger.exception(err)
            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

    else:
        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

    jsonout = flask.jsonify(output)
    return jsonout