# -*- coding: utf-8 -*-
"""
(c) 2014-2016 - Copyright Red Hat Inc
Authors:
Pierre-Yves Chibon <pingou@pingoured.fr>
"""
# pylint: disable=no-member
# pylint: disable=too-many-lines
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
import flask
import os
from collections import defaultdict
from math import ceil
import pygit2
import werkzeug.datastructures
from sqlalchemy.exc import SQLAlchemyError
import kitchen.text.converters as ktc
import mimetypes
import pagure.doc_utils
import pagure.exceptions
import pagure.lib
import pagure.lib.encoding_utils
import pagure.forms
from pagure import (APP, SESSION, LOG, __get_file_in_tree,
login_required, authenticated)
# URLs
@APP.route(
'/<repo>/issue/<int:issueid>/update/',
methods=['GET', 'POST'])
@APP.route(
'/<repo>/issue/<int:issueid>/update',
methods=['GET', 'POST'])
@APP.route(
'/<namespace>/<repo>/issue/<int:issueid>/update/',
methods=['GET', 'POST'])
@APP.route(
'/<namespace>/<repo>/issue/<int:issueid>/update',
methods=['GET', 'POST'])
@APP.route(
'/fork/<username>/<repo>/issue/<int:issueid>/update/',
methods=['GET', 'POST'])
@APP.route(
'/fork/<username>/<repo>/issue/<int:issueid>/update',
methods=['GET', 'POST'])
@APP.route(
'/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/update/',
methods=['GET', 'POST'])
@APP.route(
'/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/update',
methods=['GET', 'POST'])
@login_required
def update_issue(repo, issueid, username=None, namespace=None):
''' Add a comment to an issue. '''
is_js = flask.request.args.get('js', False)
repo = flask.g.repo
if flask.request.method == 'GET':
if not is_js:
flask.flash('Invalid method: GET', 'error')
return flask.redirect(flask.url_for(
'view_issue', username=username, repo=repo.name,
namespace=repo.namespace, issueid=issueid))
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
if issue is None or issue.project != repo:
flask.abort(404, 'Issue not found')
if issue.private and not flask.g.repo_admin \
and (not authenticated() or
not issue.user.user == flask.g.fas_user.username):
flask.abort(
403, 'This issue is private and you are not allowed to view it')
if flask.request.form.get('edit_comment'):
commentid = flask.request.form.get('edit_comment')
form = pagure.forms.EditCommentForm()
if form.validate_on_submit():
return edit_comment_issue(
repo.name, issueid, commentid, username=username)
status = pagure.lib.get_issue_statuses(SESSION)
form = pagure.forms.UpdateIssueForm(
status=status,
priorities=repo.priorities,
milestones=repo.milestones,
close_status=repo.close_status,
)
if form.validate_on_submit():
repo_admin = flask.g.repo_admin
if flask.request.form.get('drop_comment'):
commentid = flask.request.form.get('drop_comment')
comment = pagure.lib.get_issue_comment(
SESSION, issue.uid, commentid)
if comment is None or comment.issue.project != repo:
flask.abort(404, 'Comment not found')
if (flask.g.fas_user.username != comment.user.username
or comment.parent.status != 'Open') \
and not flask.g.repo_admin:
flask.abort(
403,
'You are not allowed to remove this comment from '
'this issue')
SESSION.delete(comment)
try:
SESSION.commit()
if not is_js:
flask.flash('Comment removed')
except SQLAlchemyError as err: # pragma: no cover
is_js = False
SESSION.rollback()
LOG.error(err)
if not is_js:
flask.flash(
'Could not remove the comment: %s' % commentid,
'error')
if is_js:
return 'ok'
else:
return flask.redirect(flask.url_for(
'view_issue', username=username, repo=repo.name,
namespace=repo.namespace, issueid=issueid))
comment = form.comment.data
depends = []
for depend in form.depends.data.split(','):
if depend.strip():
try:
depends.append(int(depend.strip()))
except ValueError:
pass
blocks = []
for block in form.blocks.data.split(','):
if block.strip():
try:
blocks.append(int(block.strip()))
except ValueError:
pass
assignee = form.assignee.data.strip() or None
new_status = form.status.data.strip() or None
close_status = form.close_status.data or None
if new_status != 'Closed':
close_status = None
if close_status not in repo.close_status:
close_status = None
new_priority = None
try:
new_priority = int(form.priority.data)
except:
pass
tags = [
tag.strip()
for tag in form.tag.data.split(',')
if tag.strip()]
new_milestone = None
try:
if repo.milestones:
new_milestone = form.milestone.data.strip() or None
except:
pass
try:
messages = set()
# New comment
if 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()
if message and not is_js:
messages.add(message)
if repo_admin:
# Adjust (add/remove) tags
messages.union(set(pagure.lib.update_tags(
SESSION, issue, tags,
username=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER']
)))
# The meta-data can only be changed by admins, which means they
# will be missing for non-admin and thus reset if we let them
if repo_admin:
# Assign or update assignee of the ticket
message = pagure.lib.add_issue_assignee(
SESSION,
issue=issue,
assignee=assignee or None,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
if message:
messages.add(message)
# Update status
if new_status in status:
message = pagure.lib.edit_issue(
SESSION,
issue=issue,
status=new_status,
close_status=close_status,
private=issue.private,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
if message:
messages.add(message)
# Update priority
if str(new_priority) in repo.priorities:
message = pagure.lib.edit_issue(
SESSION,
issue=issue,
priority=new_priority,
private=issue.private,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
if message:
messages.add(message)
# Update milestone and privacy setting
message = pagure.lib.edit_issue(
SESSION,
issue=issue,
milestone=new_milestone,
private=form.private.data,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
if message:
messages.add(message)
# Update the custom keys/fields
for key in repo.issue_keys:
value = flask.request.form.get(key.name)
if value:
messages.add(
pagure.lib.set_custom_key_value(
SESSION, issue, key, value)
)
# Update ticket this one depends on
messages.union(set(pagure.lib.update_dependency_issue(
SESSION, repo, issue, depends,
username=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)))
# Update ticket(s) depending on this one
messages.union(set(pagure.lib.update_blocked_issue(
SESSION, repo, issue, blocks,
username=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)))
if not is_js:
for message in messages:
flask.flash(message)
except pagure.exceptions.PagureException as err:
is_js = False
SESSION.rollback()
if not is_js:
flask.flash(err.message, 'error')
except SQLAlchemyError as err: # pragma: no cover
is_js = False
SESSION.rollback()
APP.logger.exception(err)
if not is_js:
flask.flash(str(err), 'error')
if is_js:
return 'ok'
else:
return flask.redirect(flask.url_for(
'view_issue',
repo=repo.name,
username=username,
namespace=namespace,
issueid=issueid)
)
@APP.route('/<repo>/tag/<tag>/edit/', methods=('GET', 'POST'))
@APP.route('/<repo>/tag/<tag>/edit', methods=('GET', 'POST'))
@APP.route('/<namespace>/<repo>/tag/<tag>/edit/', methods=('GET', 'POST'))
@APP.route('/<namespace>/<repo>/tag/<tag>/edit', methods=('GET', 'POST'))
@APP.route(
'/fork/<username>/<repo>/tag/<tag>/edit/',
methods=('GET', 'POST'))
@APP.route(
'/fork/<username>/<repo>/tag/<tag>/edit',
methods=('GET', 'POST'))
@APP.route(
'/fork/<username>/<namespace>/<repo>/tag/<tag>/edit/',
methods=('GET', 'POST'))
@APP.route(
'/fork/<username>/<namespace>/<repo>/tag/<tag>/edit',
methods=('GET', 'POST'))
@login_required
def edit_tag(repo, tag, username=None, namespace=None):
""" Edit the specified tag associated with the issues of a project.
"""
repo = flask.g.repo
if not flask.g.repo_admin:
flask.abort(
403,
'You are not allowed to edit tags associated with the issues of \
this project')
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
tags = pagure.lib.get_tags_of_project(SESSION, repo)
if not tags or tag not in [t.tag for t in tags]:
flask.abort(404, 'Tag %s not found in this project' % tag)
form = pagure.forms.AddIssueTagForm()
if form.validate_on_submit():
new_tag = form.tag.data
msgs = pagure.lib.edit_issue_tags(
SESSION, repo, tag, new_tag,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER']
)
try:
SESSION.commit()
for msg in msgs:
flask.flash(msg)
except SQLAlchemyError as err: # pragma: no cover
SESSION.rollback()
LOG.error(err)
flask.flash('Could not edit tag: %s' % tag, 'error')
return flask.redirect(flask.url_for(
'.view_settings', repo=repo.name, username=username,
namespace=repo.namespace))
return flask.render_template(
'edit_tag.html',
form=form,
username=username,
repo=repo,
edit_tag=tag,
)
@APP.route('/<repo>/droptag/', methods=['POST'])
@APP.route('/<namespace>/<repo>/droptag/', methods=['POST'])
@APP.route('/fork/<username>/<repo>/droptag/', methods=['POST'])
@APP.route('/fork/<username>/<namespace>/<repo>/droptag/', methods=['POST'])
@login_required
def remove_tag(repo, username=None, namespace=None):
""" Remove the specified tag, associated with the issues, from the project.
"""
repo = flask.g.repo
if not flask.g.repo_admin:
flask.abort(
403,
'You are not allowed to remove tags associated with the issues \
of this project')
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
form = pagure.forms.AddIssueTagForm()
if form.validate_on_submit():
tags = form.tag.data
tags = [tag.strip() for tag in tags.split(',')]
msgs = pagure.lib.remove_tags(
SESSION, repo, tags,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER']
)
try:
SESSION.commit()
for msg in msgs:
flask.flash(msg)
except SQLAlchemyError as err: # pragma: no cover
SESSION.rollback()
LOG.error(err)
flask.flash(
'Could not remove tag: %s' % ','.join(tags), 'error')
return flask.redirect(flask.url_for(
'.view_settings', repo=repo.name, username=username,
namespace=repo.namespace)
)
@APP.route('/<repo>/issues/')
@APP.route('/<repo>/issues')
@APP.route('/<namespace>/<repo>/issues/')
@APP.route('/<namespace>/<repo>/issues')
@APP.route('/fork/<username>/<repo>/issues/')
@APP.route('/fork/<username>/<repo>/issues')
@APP.route('/fork/<username>/<namespace>/<repo>/issues/')
@APP.route('/fork/<username>/<namespace>/<repo>/issues')
def view_issues(repo, username=None, namespace=None):
""" List all issues associated to a repo
"""
status = flask.request.args.get('status', 'Open')
priority = flask.request.args.get('priority', 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)
search_pattern = flask.request.args.get('search_pattern', None)
# Custom fields
custom_keys = flask.request.args.getlist('ckeys')
custom_values = flask.request.args.getlist('cvalue')
custom_search = {}
if len(custom_keys) == len(custom_values):
for idx, key in enumerate(custom_keys):
custom_search[key] = custom_values[idx]
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
try:
priority = int(priority)
except:
priority = None
# Hide private tickets
private = False
# If user is authenticated, show him/her his/her private tickets
if authenticated():
private = flask.g.fas_user.username
# If user is repo admin, show all tickets included the private ones
if flask.g.repo_admin:
private = None
if str(status).lower() in ['all']:
status = None
oth_issues = None
if status is not None:
issues = pagure.lib.search_issues(
SESSION,
repo,
closed=True if status.lower() != 'open' else False,
status=status.capitalize() if status.lower() != 'closed' else None,
tags=tags,
assignee=assignee,
author=author,
private=private,
priority=priority,
offset=flask.g.offset,
limit=flask.g.limit,
search_pattern=search_pattern,
custom_search=custom_search,
)
issues_cnt = pagure.lib.search_issues(
SESSION,
repo,
closed=True if status.lower() != 'open' else False,
status=status.capitalize() if status.lower() != 'closed' else None,
tags=tags,
assignee=assignee,
author=author,
private=private,
priority=priority,
search_pattern=search_pattern,
custom_search=custom_search,
count=True
)
oth_issues = pagure.lib.search_issues(
SESSION,
repo,
closed=True if status.lower() != 'open' else False,
tags=tags,
assignee=assignee,
author=author,
private=private,
priority=priority,
count=True,
search_pattern=search_pattern,
custom_search=custom_search,
)
else:
issues = pagure.lib.search_issues(
SESSION, repo, tags=tags, assignee=assignee,
author=author, private=private, priority=priority,
offset=flask.g.offset, limit=flask.g.limit,
search_pattern=search_pattern,
custom_search=custom_search,
)
issues_cnt = pagure.lib.search_issues(
SESSION, repo, tags=tags, assignee=assignee,
author=author, private=private, priority=priority, count=True)
tag_list = pagure.lib.get_tags_of_project(SESSION, repo)
total_page = int(ceil(issues_cnt / float(flask.g.limit)))
return flask.render_template(
'issues.html',
select='issues',
repo=repo,
username=username,
tag_list=tag_list,
status=status,
issues=issues,
issues_cnt=issues_cnt,
oth_issues=oth_issues,
tags=tags,
assignee=assignee,
author=author,
priority=priority,
total_page=total_page,
add_report_form=pagure.forms.AddReportForm(),
search_pattern=search_pattern,
)
@APP.route('/<repo>/roadmap/')
@APP.route('/<repo>/roadmap')
@APP.route('/<namespace>/<repo>/roadmap/')
@APP.route('/<namespace>/<repo>/roadmap')
@APP.route('/fork/<username>/<repo>/roadmap/')
@APP.route('/fork/<username>/<repo>/roadmap')
@APP.route('/fork/<username>/<namespace>/<repo>/roadmap/')
@APP.route('/fork/<username>/<namespace>/<repo>/roadmap')
def view_roadmap(repo, username=None, namespace=None):
""" List all issues associated to a repo as roadmap
"""
status = flask.request.args.get('status', 'Open')
if status.lower() == 'all':
status = None
milestone = flask.request.args.getlist('milestone', None)
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
# Hide private tickets
private = False
# If user is authenticated, show him/her his/her private tickets
if authenticated():
private = flask.g.fas_user.username
# If user is repo admin, show all tickets included the private ones
if flask.g.repo_admin:
private = None
milestones = milestone or list(repo.milestones.keys())
issues = pagure.lib.search_issues(
SESSION,
repo,
milestones=milestones,
private=private,
)
# Change from a list of issues to a dict of milestone/issues
milestone_issues = defaultdict(list)
for cnt in range(len(issues)):
saved = False
for mlstone in sorted(milestones):
if mlstone == issues[cnt].milestone:
milestone_issues[mlstone].append(issues[cnt])
saved = True
break
if saved:
continue
if status:
for key in milestone_issues.keys():
active = False
for issue in milestone_issues[key]:
if issue.status == 'Open':
active = True
break
if not active:
del milestone_issues[key]
if milestone:
for mlstone in milestone:
if mlstone not in milestone_issues:
milestone_issues[mlstone] = []
tag_list = pagure.lib.get_tags_of_project(SESSION, repo)
reponame = pagure.get_repo_path(repo)
repo_obj = pygit2.Repository(reponame)
milestones_ordered = sorted(list(milestone_issues.keys()))
if 'unplanned' in milestones_ordered:
index = milestones_ordered.index('unplanned')
cnt = len(milestones_ordered)
milestones_ordered.insert(cnt, milestones_ordered.pop(index))
return flask.render_template(
'roadmap.html',
select='issues',
repo=repo,
username=username,
tag_list=tag_list,
status=status,
milestones=milestones_ordered,
issues=milestone_issues,
tags=milestone,
)
@APP.route('/<repo>/new_issue/', methods=('GET', 'POST'))
@APP.route('/<repo>/new_issue', methods=('GET', 'POST'))
@APP.route('/<namespace>/<repo>/new_issue/', methods=('GET', 'POST'))
@APP.route('/<namespace>/<repo>/new_issue', methods=('GET', 'POST'))
@APP.route('/fork/<username>/<repo>/new_issue/', methods=('GET', 'POST'))
@APP.route('/fork/<username>/<repo>/new_issue', methods=('GET', 'POST'))
@APP.route(
'/fork/<username>/<namespace>/<repo>/new_issue/',
methods=('GET', 'POST'))
@APP.route(
'/fork/<username>/<namespace>/<repo>/new_issue',
methods=('GET', 'POST'))
@login_required
def new_issue(repo, username=None, namespace=None):
""" Create a new issue
"""
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
form = pagure.forms.IssueFormSimplied()
if form.validate_on_submit():
title = form.title.data
content = form.issue_content.data
private = form.private.data
try:
user_obj = pagure.lib.get_user(
SESSION, flask.g.fas_user.username)
except pagure.exceptions.PagureException:
flask.abort(
404,
'No such user found in the database: %s' % (
flask.g.fas_user.username))
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.commit()
# 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)' % (
new_filename, filelocation, filelocation)
issue.content = issue.content.replace('<!!image>', url)
SESSION.add(issue)
SESSION.commit()
return flask.redirect(flask.url_for(
'.view_issue', username=username, repo=repo.name,
namespace=namespace, issueid=issue.id))
except pagure.exceptions.PagureException as err:
flask.flash(str(err), 'error')
except SQLAlchemyError as err: # pragma: no cover
SESSION.rollback()
flask.flash(str(err), 'error')
types = None
default = None
ticketrepopath = os.path.join(APP.config['TICKETS_FOLDER'], repo.path)
if os.path.exists(ticketrepopath):
ticketrepo = pygit2.Repository(ticketrepopath)
if not ticketrepo.is_empty and not ticketrepo.head_is_unborn:
commit = ticketrepo[ticketrepo.head.target]
# Get the different ticket types
files = __get_file_in_tree(
ticketrepo, commit.tree, ['templates'],
bail_on_tree=True)
if files:
types = [f.name.rstrip('.md') for f in files]
# Get the default template
default_file = __get_file_in_tree(
ticketrepo, commit.tree, ['templates', 'default.md'],
bail_on_tree=True)
if default_file:
default, _ = pagure.doc_utils.convert_readme(
default_file.data, 'md')
if flask.request.method == 'GET':
form.private.data = repo.settings.get(
'issues_default_to_private', False)
return flask.render_template(
'new_issue.html',
select='issues',
form=form,
repo=repo,
username=username,
types=types,
default=default,
)
@APP.route('/<repo>/issue/<int:issueid>/')
@APP.route('/<repo>/issue/<int:issueid>')
@APP.route('/<namespace>/<repo>/issue/<int:issueid>/')
@APP.route('/<namespace>/<repo>/issue/<int:issueid>')
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>/')
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>')
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/')
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>')
def view_issue(repo, issueid, username=None, namespace=None):
""" List all issues associated to a repo
"""
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
if issue is None or issue.project != repo:
flask.abort(404, 'Issue not found')
if issue.private and not flask.g.repo_admin \
and (not authenticated() or
not issue.user.user == flask.g.fas_user.username):
flask.abort(
403, 'This issue is private and you are not allowed to view it')
status = pagure.lib.get_issue_statuses(SESSION)
form = pagure.forms.UpdateIssueForm(
status=status,
priorities=repo.priorities,
milestones=repo.milestones,
close_status=repo.close_status,
)
form.status.data = issue.status
form.priority.data = str(issue.priority)
form.milestone.data = str(issue.milestone)
form.private.data = issue.private
form.close_status.data = ''
if issue.close_status:
form.close_status.data = issue.close_status
tag_list = pagure.lib.get_tags_of_project(SESSION, repo)
knowns_keys = {}
for key in issue.other_fields:
knowns_keys[key.key.name] = key
return flask.render_template(
'issue.html',
select='issues',
repo=repo,
username=username,
tag_list=tag_list,
issue=issue,
issueid=issueid,
form=form,
knowns_keys=knowns_keys,
)
@APP.route('/<repo>/issue/<int:issueid>/drop', methods=['POST'])
@APP.route('/<namespace>/<repo>/issue/<int:issueid>/drop', methods=['POST'])
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>/drop',
methods=['POST'])
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/drop',
methods=['POST'])
def delete_issue(repo, issueid, username=None, namespace=None):
""" Delete the specified issue
"""
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
if issue is None or issue.project != repo:
flask.abort(404, 'Issue not found')
if not flask.g.repo_admin:
flask.abort(
403,
'You are not allowed to remove tickets of this project')
form = pagure.forms.ConfirmationForm()
if form.validate_on_submit():
try:
pagure.lib.drop_issue(
SESSION, issue,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
flask.flash('Issue deleted')
return flask.redirect(flask.url_for(
'view_issues', username=username, repo=repo.name,
namespace=namespace))
except SQLAlchemyError as err: # pragma: no cover
SESSION.rollback()
APP.logger.exception(err)
flask.flash('Could not delete the issue', 'error')
return flask.redirect(flask.url_for(
'view_issue', username=username, repo=repo.name,
namespace=repo.namespace, issueid=issueid))
@APP.route('/<repo>/issue/<int:issueid>/edit/', methods=('GET', 'POST'))
@APP.route('/<repo>/issue/<int:issueid>/edit', methods=('GET', 'POST'))
@APP.route(
'/<namespace>/<repo>/issue/<int:issueid>/edit/',
methods=('GET', 'POST'))
@APP.route(
'/<namespace>/<repo>/issue/<int:issueid>/edit',
methods=('GET', 'POST'))
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>/edit/',
methods=('GET', 'POST'))
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>/edit',
methods=('GET', 'POST'))
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/edit/',
methods=('GET', 'POST'))
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/edit',
methods=('GET', 'POST'))
@login_required
def edit_issue(repo, issueid, username=None, namespace=None):
""" Edit the specified issue
"""
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
if issue is None or issue.project != repo:
flask.abort(404, 'Issue not found')
if not (flask.g.repo_admin
or flask.g.fas_user.username == issue.user.username):
flask.abort(
403, 'You are not allowed to edit issues for this project')
status = pagure.lib.get_issue_statuses(SESSION)
form = pagure.forms.IssueForm(status=status)
if form.validate_on_submit():
title = form.title.data
content = form.issue_content.data
status = form.status.data
private = form.private.data
try:
user_obj = pagure.lib.get_user(
SESSION, flask.g.fas_user.username)
except pagure.exceptions.PagureException:
flask.abort(
404, 'No such user found in the database: %s' % (
flask.g.fas_user.username))
try:
message = pagure.lib.edit_issue(
SESSION,
issue=issue,
title=title,
content=content,
status=status,
user=flask.g.fas_user.username,
ticketfolder=APP.config['TICKETS_FOLDER'],
private=private,
)
SESSION.commit()
# 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,
namespace=repo.namespace,
username=username,
filename=new_filename,
)
new_filename = new_filename.split('-', 1)[1]
url = '[](%s)' % (
new_filename, filelocation, filelocation)
issue.content = issue.content.replace('<!!image>', url)
SESSION.add(issue)
SESSION.commit()
flask.flash(message)
url = flask.url_for(
'view_issue', username=username, namespace=namespace,
repo=repo.name, issueid=issueid)
return flask.redirect(url)
except pagure.exceptions.PagureException as err:
flask.flash(str(err), 'error')
except SQLAlchemyError as err: # pragma: no cover
SESSION.rollback()
flask.flash(str(err), 'error')
elif flask.request.method == 'GET':
form.title.data = issue.title
form.issue_content.data = issue.content
form.status.data = issue.status
form.private.data = issue.private
return flask.render_template(
'new_issue.html',
select='issues',
type='edit',
form=form,
repo=repo,
username=username,
issue=issue,
issueid=issueid,
)
@APP.route('/<namespace>/<repo>/issue/<int:issueid>/upload', methods=['POST'])
@APP.route('/<repo>/issue/<int:issueid>/upload', methods=['POST'])
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>/upload',
methods=['POST'])
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/upload',
methods=['POST'])
@login_required
def upload_issue(repo, issueid, username=None, namespace=None):
''' Upload a file to a ticket.
'''
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
if issue is None or issue.project != repo:
flask.abort(404, 'Issue not found')
try:
user_obj = pagure.lib.get_user(
SESSION, flask.g.fas_user.username)
except pagure.exceptions.PagureException:
flask.abort(
404, 'No such user found in the database: %s' % (
flask.g.fas_user.username))
form = pagure.forms.UploadFileForm()
if form.validate_on_submit():
filestream = flask.request.files['filestream']
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,
)
return flask.jsonify({
'output': 'ok',
'filename': new_filename.split('-', 1)[1],
'filelocation': flask.url_for(
'view_issue_raw_file',
repo=repo.name,
username=username,
namespace=repo.namespace,
filename=new_filename,
)
})
else:
return flask.jsonify({'output': 'notok'})
@APP.route('/<repo>/issue/raw/<path:filename>')
@APP.route('/<namespace>/<repo>/issue/raw/<path:filename>')
@APP.route('/fork/<username>/<repo>/issue/raw/<path:filename>')
@APP.route('/fork/<username>/<namespace>/<repo>/issue/raw/<path:filename>')
def view_issue_raw_file(
repo, filename=None, username=None, namespace=None):
""" Displays the raw content of a file of a commit for the specified
ticket repo.
"""
repo = flask.g.repo
if not repo.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
reponame = os.path.join(APP.config['TICKETS_FOLDER'], repo.path)
repo_obj = pygit2.Repository(reponame)
if repo_obj.is_empty:
flask.abort(404, 'Empty repo cannot have a file')
branch = repo_obj.lookup_branch('master')
commit = branch.get_object()
mimetype = None
encoding = None
content = __get_file_in_tree(
repo_obj, commit.tree, filename.split('/'), bail_on_tree=True)
if not content or isinstance(content, pygit2.Tree):
flask.abort(404, 'File not found')
mimetype, encoding = mimetypes.guess_type(filename)
data = repo_obj[content.oid].data
if not data:
flask.abort(404, 'No content found')
if not mimetype and data[:2] == '#!':
mimetype = 'text/plain'
headers = {}
if not mimetype:
if '\0' in data:
mimetype = 'application/octet-stream'
else:
mimetype = 'text/plain'
elif 'html' in mimetype:
mimetype = 'application/octet-stream'
headers['Content-Disposition'] = 'attachment'
if mimetype.startswith('text/') and not encoding:
encoding = pagure.lib.encoding_utils.guess_encoding(
ktc.to_bytes(data))
if encoding:
mimetype += '; charset={encoding}'.format(encoding=encoding)
headers['Content-Type'] = mimetype
return (data, 200, headers)
@APP.route('/<repo>/issue/<int:issueid>/comment/<int:commentid>/edit',
methods=('GET', 'POST'))
@APP.route('/<namespace>/<repo>/issue/<int:issueid>/comment/<int:commentid>/edit',
methods=('GET', 'POST'))
@APP.route('/fork/<username>/<repo>/issue/<int:issueid>/comment'
'/<int:commentid>/edit', methods=('GET', 'POST'))
@APP.route('/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/comment'
'/<int:commentid>/edit', methods=('GET', 'POST'))
@login_required
def edit_comment_issue(
repo, issueid, commentid, username=None, namespace=None):
"""Edit comment of an issue
"""
is_js = flask.request.args.get('js', False)
project = flask.g.repo
if not project.settings.get('issue_tracker', True):
flask.abort(404, 'No issue tracker found for this project')
issue = pagure.lib.search_issues(SESSION, project, issueid=issueid)
if issue is None or issue.project != project:
flask.abort(404, 'Issue not found')
comment = pagure.lib.get_issue_comment(
SESSION, issue.uid, commentid)
if comment is None or comment.parent.project != project:
flask.abort(404, 'Comment not found')
if (flask.g.fas_user.username != comment.user.username
or comment.parent.status != 'Open') \
and not flask.g.repo_admin:
flask.abort(403, 'You are not allowed to edit this comment')
form = pagure.forms.EditCommentForm()
if form.validate_on_submit():
updated_comment = form.update_comment.data
try:
message = pagure.lib.edit_comment(
SESSION,
parent=issue,
comment=comment,
user=flask.g.fas_user.username,
updated_comment=updated_comment,
folder=APP.config['TICKETS_FOLDER'],
)
SESSION.commit()
if not is_js:
flask.flash(message)
except SQLAlchemyError, err: # pragma: no cover
SESSION.rollback()
LOG.error(err)
if is_js:
return 'error'
flask.flash(
'Could not edit the comment: %s' % commentid, 'error')
if is_js:
return 'ok'
return flask.redirect(flask.url_for(
'view_issue', username=username, namespace=namespace,
repo=project.name, issueid=issueid))
if is_js and flask.request.method == 'POST':
return 'failed'
return flask.render_template(
'comment_update.html',
select='issues',
requestid=issueid,
repo=project,
username=username,
form=form,
comment=comment,
is_js=is_js,
)
@APP.route('/<repo>/issues/reports', methods=['POST'])
@APP.route('/<namespace>/<repo>/issues/reports', methods=['POST'])
@APP.route('/fork/<username>/<repo>/issues/reports', methods=['POST'])
@APP.route(
'/fork/<username>/<namespace>/<repo>/issues/reports', methods=['POST'])
@login_required
def save_reports(repo, username=None, namespace=None):
""" Marked for watching or Unwatching
"""
if not flask.g.repo_admin:
flask.abort(
403,
'You are not allowed to create reports for this project')
return_point = flask.url_for(
'view_issues', repo=repo, username=username, namespace=namespace)
if pagure.is_safe_url(flask.request.referrer):
return_point = flask.request.referrer
form = pagure.forms.AddReportForm()
if not form.validate_on_submit():
flask.abort(400)
name = form.report_name.data
try:
msg = pagure.lib.save_report(
SESSION,
flask.g.repo,
name=name,
url=flask.request.referrer,
username=flask.g.fas_user.username)
SESSION.commit()
flask.flash(msg)
except pagure.exceptions.PagureException as msg:
flask.flash(msg, 'error')
return flask.redirect(return_point)
@APP.route('/<repo>/report/<report>')
@APP.route('/<namespace>/<repo>/report/<report>')
@APP.route('/fork/<username>/<repo>/report/<report>')
@APP.route('/fork/<username>/<namespace>/<repo>/report/<report>')
@login_required
def view_report(repo, report, username=None, namespace=None):
""" Show the specified report.
"""
reports = flask.g.repo.reports
if report not in reports:
flask.abort(404, 'No such report found')
flask.request.args = werkzeug.datastructures.ImmutableMultiDict(
reports[report])
return view_issues(repo=repo, username=username, namespace=namespace)