# -*- coding: utf-8 -*-
"""
(c) 2014-2017 - Copyright Red Hat Inc
Authors:
Pierre-Yves Chibon <pingou@pingoured.fr>
"""
# pylint: disable=too-many-lines
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from __future__ import unicode_literals, absolute_import
import datetime
import logging
import os
from collections import defaultdict, OrderedDict
from math import ceil
import flask
import pygit2
import werkzeug.datastructures
from sqlalchemy.exc import SQLAlchemyError
from binaryornot.helpers import is_binary_string
import pagure.doc_utils
import pagure.exceptions
import pagure.lib.query
import pagure.lib.mimetype
from pagure.decorators import has_issue_tracker, is_repo_admin
import pagure.forms
from pagure.config import config as pagure_config
from pagure.ui import UI_NS
from pagure.utils import (
__get_file_in_tree,
authenticated,
login_required,
urlpattern,
is_true,
)
_log = logging.getLogger(__name__)
@UI_NS.route("/<repo>/issue/<int:issueid>/update/", methods=["GET", "POST"])
@UI_NS.route("/<repo>/issue/<int:issueid>/update", methods=["GET", "POST"])
@UI_NS.route(
"/<namespace>/<repo>/issue/<int:issueid>/update/", methods=["GET", "POST"]
)
@UI_NS.route(
"/<namespace>/<repo>/issue/<int:issueid>/update", methods=["GET", "POST"]
)
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/update/",
methods=["GET", "POST"],
)
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/update",
methods=["GET", "POST"],
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/update/",
methods=["GET", "POST"],
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/update",
methods=["GET", "POST"],
)
@login_required
@has_issue_tracker
def update_issue(repo, issueid, username=None, namespace=None):
""" Add comment or update metadata of 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(
"ui_ns.view_issue",
username=username,
repo=repo.name,
namespace=repo.namespace,
issueid=issueid,
)
)
issue = pagure.lib.query.search_issues(
flask.g.session, repo, issueid=issueid
)
if issue is None or issue.project != repo:
flask.abort(404, description="Issue not found")
if (
issue.private
and not flask.g.repo_committer
and (
not authenticated()
or not issue.user.user == flask.g.fas_user.username
)
):
flask.abort(
403,
description="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.query.get_issue_statuses(flask.g.session)
form = pagure.forms.UpdateIssueForm(
status=status,
priorities=repo.priorities,
milestones=repo.milestones,
close_status=repo.close_status,
)
is_author = flask.g.fas_user.username == issue.user.user
is_contributor = flask.g.repo_user
is_open_access = repo.settings.get("open_metadata_access_to_all", False)
if form.validate_on_submit():
if flask.request.form.get("drop_comment"):
commentid = flask.request.form.get("drop_comment")
comment = pagure.lib.query.get_issue_comment(
flask.g.session, issue.uid, commentid
)
if comment is None or comment.issue.project != repo:
flask.abort(404, description="Comment not found")
if (
not is_author or comment.parent.status != "Open"
) and not flask.g.repo_committer:
flask.abort(
403,
description="You are not allowed to remove this "
"comment from this issue",
)
issue.last_updated = datetime.datetime.utcnow()
flask.g.session.add(issue)
flask.g.session.delete(comment)
pagure.lib.git.update_git(issue, repo=issue.project)
try:
flask.g.session.commit()
if not is_js:
flask.flash("Comment removed")
except SQLAlchemyError as err: # pragma: no cover
is_js = False
flask.g.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(
"ui_ns.view_issue",
username=username,
repo=repo.name,
namespace=repo.namespace,
issueid=issueid,
)
)
comment = form.comment.data
depends = []
for depend in form.depending.data.split(","):
if depend.strip():
try:
depends.append(int(depend.strip()))
except ValueError:
pass
blocks = []
for block in form.blocking.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 close_status not in repo.close_status:
close_status = None
new_priority = None
try:
new_priority = int(form.priority.data)
except (ValueError, TypeError):
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 AttributeError:
pass
try:
messages = set()
# The status field can be updated by both the admin and the
# person who opened the ticket.
# Update status
if is_contributor or is_author:
if new_status in status:
msgs = pagure.lib.query.edit_issue(
flask.g.session,
issue=issue,
status=new_status,
close_status=close_status,
milestone=issue.milestone,
private=issue.private,
user=flask.g.fas_user.username,
)
flask.g.session.commit()
if msgs:
messages = messages.union(set(msgs))
# All the other meta-data can be changed only by admins
# while other field will be missing for non-admin and thus
# reset if we let them
if is_contributor or is_open_access:
# Adjust (add/remove) tags
msgs = pagure.lib.query.update_tags(
flask.g.session,
issue,
tags,
username=flask.g.fas_user.username,
)
messages = messages.union(set(msgs))
# 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
# Assign or update assignee of the ticket
message = pagure.lib.query.add_issue_assignee(
flask.g.session,
issue=issue,
assignee=assignee or None,
user=flask.g.fas_user.username,
)
flask.g.session.commit()
if message and message != "Nothing to change":
messages.add(message)
# Adjust priority if needed
if "%s" % new_priority not in repo.priorities:
new_priority = None
# Update core metadata
msgs = pagure.lib.query.edit_issue(
flask.g.session,
repo=repo,
issue=issue,
milestone=new_milestone,
priority=new_priority,
private=form.private.data,
user=flask.g.fas_user.username,
)
if msgs:
messages = messages.union(set(msgs))
# Update the custom keys/fields
for key in repo.issue_keys:
value = flask.request.form.get(key.name)
if value:
if key.key_type == "link":
links = value.split(",")
for link in links:
link = link.replace(" ", "")
if not urlpattern.match(link):
flask.abort(
400,
description='Meta-data "link" field '
"(%s) has invalid url (%s) "
% (key.name, link),
)
msg = pagure.lib.query.set_custom_key_value(
flask.g.session, issue, key, value
)
if key.key_notify and msg is not None:
# Custom field changed that is set for notifications
pagure.lib.notify.notify_meta_change_issue(
issue, flask.g.fas_user, msg
)
if msg:
messages.add(msg)
# Update ticket this one depends on
msgs = pagure.lib.query.update_dependency_issue(
flask.g.session,
repo,
issue,
depends,
username=flask.g.fas_user.username,
)
messages = messages.union(set(msgs))
# Update ticket(s) depending on this one
msgs = pagure.lib.query.update_blocked_issue(
flask.g.session,
repo,
issue,
blocks,
username=flask.g.fas_user.username,
)
messages = messages.union(set(msgs))
flask.g.session.commit()
# New comment
if comment:
message = pagure.lib.query.add_issue_comment(
flask.g.session,
issue=issue,
comment=comment,
user=flask.g.fas_user.username,
)
if not is_js:
if message:
messages.add(message)
flask.g.session.commit()
if not is_js:
for message in messages:
flask.flash(message)
# Add the comment for field updates:
if messages:
not_needed = set(["Comment added", "Updated comment"])
pagure.lib.query.add_metadata_update_notif(
session=flask.g.session,
obj=issue,
messages=messages - not_needed,
user=flask.g.fas_user.username,
)
messages.add("Metadata fields updated")
except pagure.exceptions.PagureException as err:
is_js = False
flask.g.session.rollback()
flask.flash("%s" % err, "error")
except SQLAlchemyError as err: # pragma: no cover
is_js = False
flask.g.session.rollback()
_log.exception(err)
flask.flash("%s" % err, "error")
else:
if is_js:
return "notok: %s" % form.errors
if is_js:
return "ok"
else:
return flask.redirect(
flask.url_for(
"ui_ns.view_issue",
repo=repo.name,
username=username,
namespace=namespace,
issueid=issueid,
)
)
_REACTION_URL_SNIPPET = "issue/<int:issueid>/comment/<int:commentid>/react"
@UI_NS.route("/<repo>/%s/" % _REACTION_URL_SNIPPET, methods=["POST"])
@UI_NS.route("/<repo>/%s" % _REACTION_URL_SNIPPET, methods=["POST"])
@UI_NS.route(
"/<namespace>/<repo>/%s/" % _REACTION_URL_SNIPPET, methods=["POST"]
)
@UI_NS.route(
"/<namespace>/<repo>/%s" % _REACTION_URL_SNIPPET, methods=["POST"]
)
@UI_NS.route(
"/fork/<username>/<repo>/%s/" % _REACTION_URL_SNIPPET, methods=["POST"]
)
@UI_NS.route(
"/fork/<username>/<repo>/%s" % _REACTION_URL_SNIPPET, methods=["POST"]
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/%s/" % _REACTION_URL_SNIPPET,
methods=["POST"],
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/%s" % _REACTION_URL_SNIPPET,
methods=["POST"],
)
@login_required
@has_issue_tracker
def issue_comment_add_reaction(
repo, issueid, commentid, username=None, namespace=None
):
"""Add a reaction to a comment of an issue"""
repo = flask.g.repo
issue = pagure.lib.query.search_issues(
flask.g.session, repo, issueid=issueid
)
if not issue or issue.project != repo:
flask.abort(404, description="Comment not found")
form = pagure.forms.ConfirmationForm()
if not form.validate_on_submit():
flask.abort(400, description="CSRF token not valid")
if (
issue.private
and not flask.g.repo_committer
and (
not authenticated()
or not issue.user.user == flask.g.fas_user.username
)
):
flask.abort(404, description="No such issue")
comment = pagure.lib.query.get_issue_comment(
flask.g.session, issue.uid, commentid
)
if "reaction" not in flask.request.form:
flask.abort(400, description="Reaction not found")
reactions = comment.reactions
r = flask.request.form["reaction"]
if not r:
flask.abort(400, description="Empty reaction is not acceptable")
if flask.g.fas_user.username in reactions.get(r, []):
flask.abort(409, description="Already posted this one")
reactions.setdefault(r, []).append(flask.g.fas_user.username)
comment.reactions = reactions
flask.g.session.add(comment)
try:
flask.g.session.commit()
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.error(err)
return "error"
return "ok"
@UI_NS.route("/<repo>/issues/")
@UI_NS.route("/<repo>/issues")
@UI_NS.route("/<namespace>/<repo>/issues/")
@UI_NS.route("/<namespace>/<repo>/issues")
@UI_NS.route("/fork/<username>/<repo>/issues/")
@UI_NS.route("/fork/<username>/<repo>/issues")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/issues/")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/issues")
@has_issue_tracker
def view_issues(repo, username=None, namespace=None):
""" List all issues associated to a repo
"""
status = flask.request.args.get("status", "Open")
status = flask.request.args.get("close_status") or status
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") or None
milestones = flask.request.args.getlist("milestone", None)
order = flask.request.args.get("order", "desc")
order_key = flask.request.args.get("order_key", "date_created")
# 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]
try:
priority = int(priority)
except (ValueError, TypeError):
priority = None
fields = {
"status": status,
"priority": priority,
"tags": tags,
"assignee": assignee,
"author": author,
"milestones": milestones,
"search_content": None,
}
no_stone = None
if "none" in milestones:
no_stone = True
milestones.remove("none")
search_string = search_pattern
extra_fields, search_pattern = pagure.lib.query.tokenize_search_string(
search_pattern
)
if "content" in extra_fields:
extra_fields["search_content"] = extra_fields["content"]
del extra_fields["content"]
for field in fields:
if field in extra_fields:
fields[field] = extra_fields[field]
del extra_fields[field]
custom_search.update(extra_fields)
repo = flask.g.repo
# 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 committer, show all tickets including the private ones
if flask.g.repo_committer:
private = None
total_closed = pagure.lib.query.search_issues(
flask.g.session, repo, status="Closed", private=private, count=True
)
total_open = pagure.lib.query.search_issues(
flask.g.session, repo, status="Open", private=private, count=True
)
status = fields["status"]
del fields["status"]
if status.lower() in ["all"]:
status = None
oth_issues_cnt = None
total_issues_cnt = pagure.lib.query.search_issues(
flask.g.session, repo, private=private, count=True, **fields
)
close_status_cnt = None
if status is not None:
if status.lower() not in ["open", "closed", "true"]:
if status.lower() not in (s.lower() for s in repo.close_status):
flask.abort(404, description="No status of that name")
status = status.capitalize()
status_count = "Closed"
other_status_count = "Open"
close_status_cnt = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
search_pattern=search_pattern,
custom_search=custom_search,
no_milestones=no_stone,
count=True,
status=status,
**fields
)
else:
if status.lower() in ["open", "true"]:
status = "Open"
status_count = "Open"
other_status_count = "Closed"
else:
status = "Closed"
status_count = "Closed"
other_status_count = "Open"
issues = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
offset=flask.g.offset,
limit=flask.g.limit,
search_pattern=search_pattern,
custom_search=custom_search,
no_milestones=no_stone,
order=order,
order_key=order_key,
status=status,
**fields
)
issues_cnt = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
search_pattern=search_pattern,
custom_search=custom_search,
no_milestones=no_stone,
count=True,
status=status_count,
**fields
)
oth_issues_cnt = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
search_pattern=search_pattern,
custom_search=custom_search,
no_milestones=no_stone,
count=True,
status=other_status_count,
**fields
)
else:
issues = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
offset=flask.g.offset,
limit=flask.g.limit,
search_pattern=search_pattern,
custom_search=custom_search,
order=order,
order_key=order_key,
**fields
)
issues_cnt = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
search_pattern=search_pattern,
custom_search=custom_search,
count=True,
**fields
)
oth_issues_cnt = pagure.lib.query.search_issues(
flask.g.session,
repo,
private=private,
search_pattern=search_pattern,
custom_search=custom_search,
no_milestones=no_stone,
count=True,
status="Open",
**fields
)
tag_list = pagure.lib.query.get_tags_of_project(flask.g.session, repo)
total_page = 1
if close_status_cnt:
total_page = int(ceil(close_status_cnt / float(flask.g.limit)))
elif issues_cnt:
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,
issues=issues,
issues_cnt=issues_cnt,
total_issues_cnt=total_issues_cnt,
oth_issues_cnt=oth_issues_cnt,
close_status_cnt=close_status_cnt,
total_page=total_page,
add_report_form=pagure.forms.AddReportForm(),
search_pattern=search_string,
order=order,
order_key=order_key,
close_status=flask.request.args.get("close_status"),
status=status,
total_open=total_open,
total_closed=total_closed,
no_milestones=no_stone,
**fields
)
@UI_NS.route("/<repo>/roadmap/")
@UI_NS.route("/<repo>/roadmap")
@UI_NS.route("/<namespace>/<repo>/roadmap/")
@UI_NS.route("/<namespace>/<repo>/roadmap")
@UI_NS.route("/fork/<username>/<repo>/roadmap/")
@UI_NS.route("/fork/<username>/<repo>/roadmap")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/roadmap/")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/roadmap")
@has_issue_tracker
def view_roadmap(repo, username=None, namespace=None):
""" List all issues associated to a repo as roadmap
"""
milestones_status_arg = flask.request.args.get("status", "active")
milestones_keyword_arg = flask.request.args.get("keyword", None)
milestones_onlyincomplete_arg = flask.request.args.get(
"onlyincomplete", False
)
if milestones_onlyincomplete_arg == "True":
milestones_onlyincomplete_arg = True
else:
milestones_onlyincomplete_arg = False
repo = flask.g.repo
# 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 committer, show all tickets including the private ones
if flask.g.repo_committer:
private = None
milestones_list = []
milestones_totals = defaultdict(int)
milestones_totals["active"] = 0
milestones_totals["inactive"] = 0
for key in repo.milestones_keys:
if milestones_keyword_arg and milestones_keyword_arg not in key:
continue
if key not in repo.milestones:
continue
if repo.milestones[key]["active"]:
milestones_totals["active"] += 1
if (
milestones_status_arg == "active"
or milestones_status_arg == "all"
):
milestones_list.append(key)
else:
milestones_totals["inactive"] += 1
if (
milestones_status_arg == "inactive"
or milestones_status_arg == "all"
):
milestones_list.append(key)
issues = pagure.lib.query.search_issues(
flask.g.session,
repo,
milestones=milestones_list,
private=private,
status=None,
)
# Change from a list of issues to a dict of milestone/issues
milestone_issues = OrderedDict()
if milestones_list:
for milestone in milestones_list:
milestone_issues[milestone] = defaultdict(int)
for issue in issues:
if issue.milestone:
milestone_issues[issue.milestone][issue.status] += 1
milestone_issues[issue.milestone]["Total"] += 1
if milestones_onlyincomplete_arg:
for m in milestone_issues:
if milestone_issues[m]["Total"] == 0:
continue
elif milestone_issues[m]["Total"] == milestone_issues[m]["Closed"]:
del milestone_issues[m]
if repo.milestones[m]["active"]:
milestones_totals["active"] -= 1
else:
milestones_totals["inactive"] -= 1
return flask.render_template(
"repo_roadmap.html",
select="roadmap",
milestones_status_select=milestones_status_arg,
repo=repo,
username=username,
milestones=milestone_issues,
milestones_totals=milestones_totals,
keyword=milestones_keyword_arg,
onlyincomplete=milestones_onlyincomplete_arg,
)
@UI_NS.route("/<repo>/roadmap/<path:milestone>")
@UI_NS.route("/<repo>/roadmap/<path:milestone>/")
@UI_NS.route("/<namespace>/<repo>/roadmap/<path:milestone>/")
@UI_NS.route("/<namespace>/<repo>/roadmap/<path:milestone>")
@UI_NS.route("/fork/<username>/<repo>/roadmap/<path:milestone>/")
@UI_NS.route("/fork/<username>/<repo>/roadmap/<path:milestone>")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/roadmap/<path:milestone>/")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/roadmap/<path:milestone>")
@has_issue_tracker
def view_milestone(repo, username=None, namespace=None, milestone=None):
""" List all issues associated to a repo as roadmap
"""
repo = flask.g.repo
# 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 committer, show all tickets including the private ones
if flask.g.repo_committer:
private = None
open_issues = pagure.lib.query.search_issues(
flask.g.session,
repo,
milestones=[milestone],
private=private,
status="Open",
)
closed_issues = pagure.lib.query.search_issues(
flask.g.session,
repo,
milestones=[milestone],
private=private,
status="Closed",
)
return flask.render_template(
"repo_milestone.html",
select="roadmap",
repo=repo,
username=username,
milestone=milestone,
open_issues=open_issues,
closed_issues=closed_issues,
total_open=len(open_issues),
total_closed=len(closed_issues),
)
@UI_NS.route("/<repo>/new_issue/", methods=("GET", "POST"))
@UI_NS.route("/<repo>/new_issue", methods=("GET", "POST"))
@UI_NS.route("/<namespace>/<repo>/new_issue/", methods=("GET", "POST"))
@UI_NS.route("/<namespace>/<repo>/new_issue", methods=("GET", "POST"))
@UI_NS.route("/fork/<username>/<repo>/new_issue/", methods=("GET", "POST"))
@UI_NS.route("/fork/<username>/<repo>/new_issue", methods=("GET", "POST"))
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/new_issue/", methods=("GET", "POST")
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/new_issue", methods=("GET", "POST")
)
@login_required
@has_issue_tracker
def new_issue(repo, username=None, namespace=None):
""" Create a new issue
"""
template = flask.request.args.get("template") or "default"
repo = flask.g.repo
open_access = repo.settings.get("open_metadata_access_to_all", False)
milestones = []
for m in repo.milestones_keys or repo.milestones:
if m in repo.milestones and repo.milestones[m]["active"]:
milestones.append(m)
form = pagure.forms.IssueFormSimplied(
priorities=repo.priorities, milestones=milestones
)
if form.validate_on_submit():
title = form.title.data
content = form.issue_content.data
private = form.private.data
try:
user_obj = pagure.lib.query.get_user(
flask.g.session, flask.g.fas_user.username
)
except pagure.exceptions.PagureException:
flask.abort(
404,
description="No such user found in the database: %s"
% (flask.g.fas_user.username),
)
try:
priority = None
if repo.default_priority:
for key, val in repo.priorities.items():
if repo.default_priority == val:
priority = key
assignee = None
milestone = None
tags = None
if flask.g.repo_user or open_access:
assignee = (
flask.request.form.get("assignee", "").strip() or None
)
milestone = form.milestone.data or None
priority = form.priority.data or priority
tags = [
tag.strip()
for tag in flask.request.form.get("tag", "").split(",")
if tag.strip()
]
issue = pagure.lib.query.new_issue(
flask.g.session,
repo=repo,
title=title,
content=content,
private=private or False,
user=flask.g.fas_user.username,
assignee=assignee,
milestone=milestone,
priority=priority,
tags=tags,
)
flask.g.session.commit()
# If there is a file attached, attach it.
form = pagure.forms.UploadFileForm()
if form.validate_on_submit():
streams = flask.request.files.getlist("filestream")
n_img = issue.content.count("<!!image>")
if n_img == len(streams):
for filestream in streams:
new_filename = pagure.lib.query.add_attachment(
repo=repo,
issue=issue,
attachmentfolder=pagure_config[
"ATTACHMENTS_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(
"ui_ns.view_issue_raw_file",
repo=repo.name,
username=username,
namespace=repo.namespace,
filename="files/%s" % new_filename,
)
new_filename = new_filename.split("-", 1)[1]
url = "[![%s](%s)](%s)" % (
new_filename,
filelocation,
filelocation,
)
issue.content = issue.content.replace(
"<!!image>", url, 1
)
flask.g.session.add(issue)
flask.g.session.commit()
return flask.redirect(
flask.url_for(
"ui_ns.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
flask.g.session.rollback()
flask.flash(str(err), "error")
types = None
default = None
ticketrepopath = repo.repopath("tickets")
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.rsplit(".md", 1)[0] for f in files]
default_file = None
if types and template in types:
# Get the template
default_file = __get_file_in_tree(
ticketrepo,
commit.tree,
["templates", "%s.md" % template],
bail_on_tree=True,
)
if default_file:
default, _ = pagure.doc_utils.convert_readme(
default_file.data, "md"
)
tag_list = pagure.lib.query.get_tags_of_project(flask.g.session, repo)
if flask.request.method == "GET":
form.private.data = repo.settings.get(
"issues_default_to_private", False
)
form.title.data = flask.request.args.get("title")
form.issue_content.data = flask.request.args.get("content")
default_priority = None
if repo.default_priority:
for key, val in repo.priorities.items():
if repo.default_priority == val:
default_priority = key
form.priority.data = flask.request.form.get(
"priority", "%s" % default_priority
)
return flask.render_template(
"new_issue.html",
select="issues",
form=form,
repo=repo,
username=username,
types=types,
default=default,
tag_list=tag_list,
open_access=open_access,
)
@UI_NS.route("/<repo>/issue/<int:issueid>/")
@UI_NS.route("/<repo>/issue/<int:issueid>")
@UI_NS.route("/<namespace>/<repo>/issue/<int:issueid>/")
@UI_NS.route("/<namespace>/<repo>/issue/<int:issueid>")
@UI_NS.route("/fork/<username>/<repo>/issue/<int:issueid>/")
@UI_NS.route("/fork/<username>/<repo>/issue/<int:issueid>")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/issue/<int:issueid>")
@has_issue_tracker
def view_issue(repo, issueid, username=None, namespace=None):
""" List all issues associated to a repo
"""
repo = flask.g.repo
issue = pagure.lib.query.search_issues(
flask.g.session, repo, issueid=issueid
)
if issue is None or issue.project != repo:
flask.abort(404, description="Issue not found")
if issue.private:
assignee = issue.assignee.user if issue.assignee else None
if not authenticated() or (
not flask.g.repo_committer
and issue.user.user != flask.g.fas_user.username
and assignee != flask.g.fas_user.username
):
flask.abort(404, description="Issue not found")
status = pagure.lib.query.get_issue_statuses(flask.g.session)
milestones = []
for m in repo.milestones_keys or repo.milestones:
if m in repo.milestones and repo.milestones[m]["active"]:
milestones.append(m)
form = pagure.forms.UpdateIssueForm(
status=status,
priorities=repo.priorities,
milestones=milestones or None,
close_status=repo.close_status,
)
form.status.data = issue.status
form.priority.data = "%s" % issue.priority
# issue.priority is an int that we need to convert to string as the form
# relies on string
form.milestone.data = 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.query.get_tags_of_project(flask.g.session, repo)
knowns_keys = {}
for key in issue.other_fields:
knowns_keys[key.key.name] = key
open_access = repo.settings.get("open_metadata_access_to_all", False)
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,
open_access=open_access,
subscribers=pagure.lib.query.get_watch_list(flask.g.session, issue),
attachments=issue.attachments,
)
@UI_NS.route("/<repo>/issue/<int:issueid>/drop", methods=["POST"])
@UI_NS.route("/<namespace>/<repo>/issue/<int:issueid>/drop", methods=["POST"])
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/drop", methods=["POST"]
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/drop",
methods=["POST"],
)
@has_issue_tracker
def delete_issue(repo, issueid, username=None, namespace=None):
""" Delete the specified issue
"""
repo = flask.g.repo
issue = pagure.lib.query.search_issues(
flask.g.session, repo, issueid=issueid
)
if issue is None or issue.project != repo:
flask.abort(404, description="Issue not found")
if not flask.g.repo_committer:
flask.abort(
403,
description="You are not allowed to remove tickets of "
"this project",
)
form = pagure.forms.ConfirmationForm()
if form.validate_on_submit():
try:
pagure.lib.query.drop_issue(
flask.g.session, issue, user=flask.g.fas_user.username
)
flask.g.session.commit()
flask.flash("Issue deleted")
return flask.redirect(
flask.url_for(
"ui_ns.view_issues",
username=username,
repo=repo.name,
namespace=namespace,
)
)
except SQLAlchemyError as err: # pragma: no cover
flask.g.session.rollback()
_log.exception(err)
flask.flash("Could not delete the issue", "error")
return flask.redirect(
flask.url_for(
"ui_ns.view_issue",
username=username,
repo=repo.name,
namespace=repo.namespace,
issueid=issueid,
)
)
@UI_NS.route("/<repo>/issue/<int:issueid>/edit/", methods=("GET", "POST"))
@UI_NS.route("/<repo>/issue/<int:issueid>/edit", methods=("GET", "POST"))
@UI_NS.route(
"/<namespace>/<repo>/issue/<int:issueid>/edit/", methods=("GET", "POST")
)
@UI_NS.route(
"/<namespace>/<repo>/issue/<int:issueid>/edit", methods=("GET", "POST")
)
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/edit/",
methods=("GET", "POST"),
)
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/edit", methods=("GET", "POST")
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/edit/",
methods=("GET", "POST"),
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/edit",
methods=("GET", "POST"),
)
@login_required
@has_issue_tracker
def edit_issue(repo, issueid, username=None, namespace=None):
""" Edit the specified issue
"""
repo = flask.g.repo
issue = pagure.lib.query.search_issues(
flask.g.session, repo, issueid=issueid
)
if issue is None or issue.project != repo:
flask.abort(404, description="Issue not found")
if not (
flask.g.repo_committer
or flask.g.fas_user.username == issue.user.username
):
flask.abort(
403,
description="You are not allowed to edit issues for this project",
)
status = pagure.lib.query.get_issue_statuses(flask.g.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.query.get_user(
flask.g.session, flask.g.fas_user.username
)
except pagure.exceptions.PagureException:
flask.abort(
404,
description="No such user found in the database: %s"
% (flask.g.fas_user.username),
)
try:
messages = pagure.lib.query.edit_issue(
flask.g.session,
issue=issue,
title=title,
content=content,
status=status,
user=flask.g.fas_user.username,
private=private,
)
flask.g.session.commit()
if messages:
pagure.lib.query.add_metadata_update_notif(
session=flask.g.session,
obj=issue,
messages=messages,
user=flask.g.fas_user.username,
)
# 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.query.add_attachment(
repo=repo,
issue=issue,
attachmentfolder=pagure_config["ATTACHMENTS_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](%s)](%s)" % (
new_filename,
filelocation,
filelocation,
)
issue.content = issue.content.replace("<!!image>", url)
flask.g.session.add(issue)
flask.g.session.commit()
if messages:
for message in messages:
flask.flash(message)
url = flask.url_for(
"ui_ns.view_issue",
username=username,
namespace=namespace,
repo=repo.name,
issueid=issueid,
)
return flask.redirect(url)
except pagure.exceptions.PagureException as err:
flask.g.session.rollback()
flask.flash(str(err), "error")
except SQLAlchemyError as err: # pragma: no cover
flask.g.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,
)
@UI_NS.route("/<repo>/issue/<int:issueid>/upload", methods=["POST"])
@UI_NS.route(
"/<namespace>/<repo>/issue/<int:issueid>/upload", methods=["POST"]
)
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/upload", methods=["POST"]
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/upload",
methods=["POST"],
)
@login_required
@has_issue_tracker
def upload_issue(repo, issueid, username=None, namespace=None):
""" Upload a file to a ticket.
"""
repo = flask.g.repo
issue = pagure.lib.query.search_issues(
flask.g.session, repo, issueid=issueid
)
if issue is None or issue.project != repo:
flask.abort(404, description="Issue not found")
try:
user_obj = pagure.lib.query.get_user(
flask.g.session, flask.g.fas_user.username
)
except pagure.exceptions.PagureException:
flask.abort(
404,
description="No such user found in the database: %s"
% (flask.g.fas_user.username),
)
form = pagure.forms.UploadFileForm()
if form.validate_on_submit():
filenames = []
for filestream in flask.request.files.getlist("filestream"):
new_filename = pagure.lib.query.add_attachment(
repo=repo,
issue=issue,
attachmentfolder=pagure_config["ATTACHMENTS_FOLDER"],
user=user_obj,
filename=filestream.filename,
filestream=filestream.stream,
)
filenames.append(new_filename)
return flask.jsonify(
{
"output": "ok",
"filenames": [
filename.split("-", 1)[1] for filename in filenames
],
"filelocations": [
flask.url_for(
"ui_ns.view_issue_raw_file",
repo=repo.name,
username=username,
namespace=repo.namespace,
filename="files/%s" % nfilename,
)
for nfilename in filenames
],
}
)
else:
return flask.jsonify({"output": "notok"})
@UI_NS.route("/<repo>/issue/raw/<path:filename>")
@UI_NS.route("/<namespace>/<repo>/issue/raw/<path:filename>")
@UI_NS.route("/fork/<username>/<repo>/issue/raw/<path:filename>")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/issue/raw/<path:filename>")
@has_issue_tracker
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.
"""
raw = is_true(flask.request.args.get("raw"))
repo = flask.g.repo
attachdir = os.path.join(
pagure_config["ATTACHMENTS_FOLDER"], repo.fullname
)
attachpath = os.path.join(attachdir, filename)
if not os.path.exists(attachpath):
if not os.path.exists(attachdir):
os.makedirs(attachdir)
# Try to copy from git repo to attachments folder
reponame = repo.repopath("tickets")
repo_obj = pygit2.Repository(reponame)
if repo_obj.is_empty:
flask.abort(404, description="Empty repo cannot have a file")
branch = repo_obj.lookup_branch("master")
commit = branch.peel()
content = __get_file_in_tree(
repo_obj, commit.tree, ["files", filename], bail_on_tree=True
)
if not content or isinstance(content, pygit2.Tree):
flask.abort(404, description="File not found")
data = repo_obj[content.oid].data
if not data:
flask.abort(404, description="No content found")
_log.info(
"Migrating file %s for project %s to attachments",
filename,
repo.fullname,
)
with open(attachpath, "wb") as stream:
stream.write(data)
data = None
# At this moment, attachpath exists and points to the file
with open(attachpath, "rb") as f:
data = f.read()
if (
not raw
and (filename.endswith(".patch") or filename.endswith(".diff"))
and not is_binary_string(data)
):
# We have a patch file attached to this issue, render the diff in html
orig_filename = filename.partition("-")[2]
return flask.render_template(
"patchfile.html",
select="issues",
repo=repo,
username=username,
diff=data.decode("utf-8"),
patchfile=orig_filename,
)
return (data, 200, pagure.lib.mimetype.get_type_headers(filename, data))
@UI_NS.route(
"/<repo>/issue/<int:issueid>/comment/<int:commentid>/edit",
methods=("GET", "POST"),
)
@UI_NS.route(
"/<namespace>/<repo>/issue/<int:issueid>/comment/<int:commentid>/" "edit",
methods=("GET", "POST"),
)
@UI_NS.route(
"/fork/<username>/<repo>/issue/<int:issueid>/comment"
"/<int:commentid>/edit",
methods=("GET", "POST"),
)
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issue/<int:issueid>/comment"
"/<int:commentid>/edit",
methods=("GET", "POST"),
)
@login_required
@has_issue_tracker
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
issue = pagure.lib.query.search_issues(
flask.g.session, project, issueid=issueid
)
if issue is None or issue.project != project:
flask.abort(404, description="Issue not found")
comment = pagure.lib.query.get_issue_comment(
flask.g.session, issue.uid, commentid
)
if comment is None or comment.parent.project != project:
flask.abort(404, description="Comment not found")
if (
flask.g.fas_user.username != comment.user.username
or comment.parent.status != "Open"
) and not flask.g.repo_user:
flask.abort(
403, description="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.query.edit_comment(
flask.g.session,
parent=issue,
comment=comment,
user=flask.g.fas_user.username,
updated_comment=updated_comment,
)
flask.g.session.commit()
if not is_js:
flask.flash(message)
except SQLAlchemyError as err: # pragma: no cover
flask.g.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(
"ui_ns.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,
)
@UI_NS.route("/<repo>/issues/reports", methods=["POST"])
@UI_NS.route("/<namespace>/<repo>/issues/reports", methods=["POST"])
@UI_NS.route("/fork/<username>/<repo>/issues/reports", methods=["POST"])
@UI_NS.route(
"/fork/<username>/<namespace>/<repo>/issues/reports", methods=["POST"]
)
@login_required
@is_repo_admin
def save_reports(repo, username=None, namespace=None):
""" Marked for watching or Unwatching
"""
return_point = flask.url_for(
"ui_ns.view_issues", repo=repo, username=username, namespace=namespace
)
if pagure.utils.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.query.save_report(
flask.g.session,
flask.g.repo,
name=name,
url=flask.request.referrer,
username=flask.g.fas_user.username,
)
flask.g.session.commit()
flask.flash(msg)
except pagure.exceptions.PagureException as msg:
flask.flash(msg, "error")
return flask.redirect(return_point)
@UI_NS.route("/<repo>/report/<report>")
@UI_NS.route("/<namespace>/<repo>/report/<report>")
@UI_NS.route("/fork/<username>/<repo>/report/<report>")
@UI_NS.route("/fork/<username>/<namespace>/<repo>/report/<report>")
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, description="No such report found")
flask.request.args = werkzeug.datastructures.ImmutableMultiDict(
reports[report]
)
return view_issues(repo=repo, username=username, namespace=namespace)