pagure-new/pagure/api/issue.py

782 lines
25 KiB
Python

# -*- 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('/<namespace>/<repo>/new_issue', methods=['POST'])
@API.route('/fork/<username>/<repo>/new_issue', methods=['POST'])
@API.route('/fork/<username>/<namespace>/<repo>/new_issue', methods=['POST'])
@api_login_required(acls=['issue_create'])
@api_method
def api_new_issue(repo, username=None, namespace=None):
"""
Create a new issue
------------------
Open a new issue on a project.
::
POST /api/0/<repo>/new_issue
POST /api/0/<namespace>/<repo>/new_issue
::
POST /api/0/fork/<username>/<repo>/new_issue
POST /api/0/fork/<username>/<namespace>/<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, namespace=namespace)
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)
user_obj = pagure.lib.get_user(
SESSION, flask.g.fas_user.username)
if not user_obj:
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOUSER)
form = pagure.forms.IssueFormSimplied(csrf_enabled=False)
if form.validate_on_submit():
title = form.title.data
content = form.issue_content.data
private = str(form.private.data).lower() in ['true', '1']
try:
issue = pagure.lib.new_issue(
SESSION,
repo=repo,
title=title,
content=content,
private=private,
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=user_obj,
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 as 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('/<namespace>/<repo>/issues')
@API.route('/fork/<username>/<repo>/issues')
@API.route('/<repo>/issues')
@API.route('/fork/<username>/<namespace>/<repo>/issues')
@api_login_optional()
@api_method
def api_view_issues(repo, username=None, namespace=None):
"""
List project's issues
---------------------
List issues of a project.
::
GET /api/0/<repo>/issues
GET /api/0/<namespace>/<repo>/issues
::
GET /api/0/fork/<username>/<repo>/issues
GET /api/0/fork/<username>/<namespace>/<repo>/issues
Parameters
^^^^^^^^^^
+---------------+---------+--------------+---------------------------+
| Key | Type | Optionality | Description |
+===============+=========+==============+===========================+
| ``status`` | string | Optional | | Filters the status of |
| | | | issues. Fetches all the |
| | | | issues if status is |
| | | | ``all``. 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"
]
},
"total_issues": 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, namespace=namespace)
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:
params = {
'session': SESSION,
'repo': repo,
'tags': tags,
'assignee': assignee,
'author': author,
'private': private
}
if status.lower() == 'closed':
params.update({'closed': True})
elif status.lower() != 'all':
params.update({'status': status})
issues = pagure.lib.search_issues(**params)
else:
issues = pagure.lib.search_issues(
SESSION, repo, status='Open', tags=tags, assignee=assignee,
author=author, private=private)
jsonout = flask.jsonify({
'total_issues': len(issues),
'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/<issueid>')
@API.route('/<namespace>/<repo>/issue/<issueid>')
@API.route('/fork/<username>/<repo>/issue/<issueid>')
@API.route('/fork/<username>/<namespace>/<repo>/issue/<issueid>')
@api_login_optional()
@api_method
def api_view_issue(repo, issueid, username=None, namespace=None):
"""
Issue information
-----------------
Retrieve information of a specific issue.
::
GET /api/0/<repo>/issue/<issue id>
GET /api/0/<namespace>/<repo>/issue/<issue id>
::
GET /api/0/fork/<username>/<repo>/issue/<issue id>
GET /api/0/fork/<username>/<namespace>/<repo>/issue/<issue id>
The identifier provided can be either the unique identifier or the
regular identifier used in the UI (for example ``24`` in
``/forks/user/test/issue/24``)
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"
}
}
"""
comments = flask.request.args.get('comments', True)
if str(comments).lower() in ['0', 'False']:
comments = False
repo = pagure.lib.get_project(
SESSION, repo, user=username, namespace=namespace)
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_id = issue_uid = None
try:
issue_id = int(issueid)
except:
issue_uid = issueid
issue = pagure.lib.search_issues(
SESSION, repo, issueid=issue_id, issueuid=issue_uid)
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, with_comments=comments))
return jsonout
@API.route('/<repo>/issue/<issueid>/comment/<int:commentid>')
@API.route('/<namespace>/<repo>/issue/<issueid>/comment/<int:commentid>')
@API.route('/fork/<username>/<repo>/issue/<issueid>/comment/<int:commentid>')
@API.route(
'/fork/<username>/<namespace>/<repo>/issue/<issueid>/'
'comment/<int:commentid>')
@api_login_optional()
@api_method
def api_view_issue_comment(
repo, issueid, commentid, username=None, namespace=None):
"""
Comment of an issue
--------------------
Retrieve a specific comment of an issue.
::
GET /api/0/<repo>/issue/<issue id>/comment/<comment id>
GET /api/0/<namespace>/<repo>/issue/<issue id>/comment/<comment id>
::
GET /api/0/fork/<username>/<repo>/issue/<issue id>/comment/<comment id>
GET /api/0/fork/<username>/<namespace>/<repo>/issue/<issue id>/comment/<comment id>
The identifier provided can be either the unique identifier or the
regular identifier used in the UI (for example ``24`` in
``/forks/user/test/issue/24``)
Sample response
^^^^^^^^^^^^^^^
::
{
"avatar_url": "https://seccdn.libravatar.org/avatar/...",
"comment": "9",
"comment_date": "2015-07-01 15:08",
"date_created": "1435756127",
"id": 464,
"parent": null,
"user": {
"fullname": "P.-Y.C.",
"name": "pingou"
}
}
"""
repo = pagure.lib.get_project(
SESSION, repo, user=username, namespace=namespace)
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_id = issue_uid = None
try:
issue_id = int(issueid)
except:
issue_uid = issueid
issue = pagure.lib.search_issues(
SESSION, repo, issueid=issue_id, issueuid=issue_uid)
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(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)
comment = pagure.lib.get_issue_comment(SESSION, issue.uid, commentid)
if not comment:
raise pagure.exceptions.APIError(
404, error_code=APIERROR.ENOCOMMENT)
output = comment.to_json(public=True)
output['avatar_url'] = pagure.lib.avatar_url_from_openid(
comment.user.default_email, size=16)
output['comment_date'] = comment.date_created.strftime(
'%Y-%m-%d %H:%M:%S')
jsonout = flask.jsonify(output)
return jsonout
@API.route('/<repo>/issue/<int:issueid>/status', methods=['POST'])
@API.route('/<namespace>/<repo>/issue/<int:issueid>/status', methods=['POST'])
@API.route(
'/fork/<username>/<repo>/issue/<int:issueid>/status', methods=['POST'])
@API.route(
'/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/status',
methods=['POST'])
@api_login_required(acls=['issue_change_status'])
@api_method
def api_change_status_issue(repo, issueid, username=None, namespace=None):
"""
Change issue status
-------------------
Change the status of an issue.
::
POST /api/0/<repo>/issue/<issue id>/status
POST /api/0/<namespace>/<repo>/issue/<issue id>/status
::
POST /api/0/fork/<username>/<repo>/issue/<issue id>/status
POST /api/0/fork/<username>/<namespace>/<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, namespace=namespace)
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 api_authenticated():
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 as err:
raise pagure.exceptions.APIError(
400, error_code=APIERROR.ENOCODE, error=str(err))
except SQLAlchemyError as 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('/<namespace>/<repo>/issue/<int:issueid>/comment', methods=['POST'])
@API.route(
'/fork/<username>/<repo>/issue/<int:issueid>/comment', methods=['POST'])
@API.route(
'/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/comment',
methods=['POST'])
@api_login_required(acls=['issue_comment'])
@api_method
def api_comment_issue(repo, issueid, username=None, namespace=None):
"""
Comment to an issue
-------------------
Add a comment to an issue.
::
POST /api/0/<repo>/issue/<issue id>/comment
POST /api/0/<namespace>/<repo>/issue/<issue id>/comment
::
POST /api/0/fork/<username>/<repo>/issue/<issue id>/comment
POST /api/0/fork/<username>/<namespace>/<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, namespace=namespace)
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 api_authenticated():
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)
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 as 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>/issue/<int:issueid>/assign', methods=['POST'])
@API.route('/<namespace>/<repo>/issue/<int:issueid>/assign', methods=['POST'])
@API.route(
'/fork/<username>/<repo>/issue/<int:issueid>/assign', methods=['POST'])
@API.route(
'/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/assign',
methods=['POST'])
@api_login_required(acls=['issue_assign'])
@api_method
def api_assign_issue(repo, issueid, username=None, namespace=None):
"""
Assign an issue
---------------
Assign an issue to someone.
::
POST /api/0/<repo>/issue/<issue id>/assign
POST /api/0/<namespace>/<repo>/issue/<issue id>/assign
::
POST /api/0/fork/<username>/<repo>/issue/<issue id>/assign
POST /api/0/fork/<username>/<namespace>/<repo>/issue/<issue id>/assign
Input
^^^^^
+--------------+----------+---------------+---------------------------+
| Key | Type | Optionality | Description |
+==============+==========+===============+===========================+
| ``assignee`` | string | Mandatory | | The username of the user|
| | | | to assign the issue to. |
+--------------+----------+---------------+---------------------------+
Sample response
^^^^^^^^^^^^^^^
::
{
"message": "Issue assigned"
}
"""
repo = pagure.lib.get_project(
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('issue_tracker', True):
raise pagure.exceptions.APIError(
404, error_code=APIERROR.ETRACKERDISABLED)
if api_authenticated():
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)
form = pagure.forms.AssignIssueForm(csrf_enabled=False)
if form.validate_on_submit():
assignee = form.assignee.data
try:
# New comment
message = pagure.lib.add_issue_assignee(
SESSION,
issue=issue,
assignee=assignee,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
output['message'] = message
except SQLAlchemyError as 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