# -*- coding: utf-8 -*- """ (c) 2015 - Copyright Red Hat Inc Authors: Pierre-Yves Chibon """ 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('//new_issue', methods=['POST']) @API.route('///new_issue', methods=['POST']) @API.route('/fork///new_issue', methods=['POST']) @API.route('/fork////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//new_issue POST /api/0///new_issue :: POST /api/0/fork///new_issue POST /api/0/fork////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 '' 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 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('', 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('///issues') @API.route('/fork///issues') @API.route('//issues') @API.route('/fork////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//issues GET /api/0///issues :: GET /api/0/fork///issues GET /api/0/fork////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('//issue/') @API.route('///issue/') @API.route('/fork///issue/') @API.route('/fork////issue/') @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//issue/ GET /api/0///issue/ :: GET /api/0/fork///issue/ GET /api/0/fork////issue/ 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('//issue//comment/') @API.route('///issue//comment/') @API.route('/fork///issue//comment/') @API.route( '/fork////issue//' 'comment/') @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//issue//comment/ GET /api/0///issue//comment/ :: GET /api/0/fork///issue//comment/ GET /api/0/fork////issue//comment/ 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('//issue//status', methods=['POST']) @API.route('///issue//status', methods=['POST']) @API.route( '/fork///issue//status', methods=['POST']) @API.route( '/fork////issue//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//issue//status POST /api/0///issue//status :: POST /api/0/fork///issue//status POST /api/0/fork////issue//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('//issue//comment', methods=['POST']) @API.route('///issue//comment', methods=['POST']) @API.route( '/fork///issue//comment', methods=['POST']) @API.route( '/fork////issue//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//issue//comment POST /api/0///issue//comment :: POST /api/0/fork///issue//comment POST /api/0/fork////issue//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('//issue//assign', methods=['POST']) @API.route('///issue//assign', methods=['POST']) @API.route( '/fork///issue//assign', methods=['POST']) @API.route( '/fork////issue//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//issue//assign POST /api/0///issue//assign :: POST /api/0/fork///issue//assign POST /api/0/fork////issue//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