# -*- coding: utf-8 -*-
"""
(c) 2014-2018 - Copyright Red Hat Inc
Authors:
Pierre-Yves Chibon <pingou@pingoured.fr>
"""
# pylint: disable=too-many-branches
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
from __future__ import unicode_literals, absolute_import
import textwrap
import logging
from os.path import splitext
import arrow
import flask
import six
import bleach
from six.moves.urllib.parse import urlparse, parse_qsl
from jinja2 import escape
import pagure.exceptions
import pagure.lib.query
import pagure.forms
from pagure.config import config as pagure_config
from pagure.ui import UI_NS
from pagure.utils import authenticated, is_repo_committer, is_true
_log = logging.getLogger(__name__)
# Jinja filters
@UI_NS.app_template_filter("hasattr")
def jinja_hasattr(obj, string):
""" Template filter checking if the provided object at the provided
string as attribute
"""
return hasattr(obj, string)
@UI_NS.app_template_filter("render")
def jinja_render(tmpl, **kwargs):
""" Render the given template with the provided arguments
"""
return flask.render_template_string(tmpl, **kwargs)
@UI_NS.app_template_filter("humanize")
def humanize_date(date):
""" Template filter returning the last commit date of the provided repo.
"""
if date:
return arrow.get(date).humanize()
@UI_NS.app_template_filter("format_ts")
@UI_NS.app_template_filter("format_datetime")
def format_ts(string):
""" Template filter transforming a timestamp, datetime or anything
else arrow.get() can handle to a human-readable date
"""
# We *could* enhance this by allowing users to specify preferred
# timezone, localized time format etc. and customizing this display
# to user's preferences. But we don't have that, so for now, we
# always use UTC timezone, and we don't use localized forms like
# %b or %d because they will be 'localized' for the *server*.
# This format should be pretty 'locale-neutral'.
arr = arrow.get(string)
return arr.strftime("%Y-%m-%d %H:%M:%S %Z")
@UI_NS.app_template_filter("linkify")
def linkify_text(text):
""" escape all html tags with bleach, then use bleach to linkify
"""
if text:
cleaned = bleach.clean(text, tags=[], attributes=[])
return bleach.linkify(cleaned)
else:
return ""
@UI_NS.app_template_filter("syntax_alias")
def get_syntax_alias(filename):
""" return an alias based on the filename that is used to
override the automatic syntax highlighting dectection
by highlight.js
"""
override_rules = pagure_config.get(
"SYNTAX_ALIAS_OVERRIDES", {".spec": "rpm-specfile", ".patch": "diff"}
)
fn, fn_ext = splitext(filename)
output = ""
if fn_ext == "":
output = "lang-plaintext"
elif fn_ext in override_rules:
output = "lang-" + override_rules[fn_ext]
return output
@UI_NS.app_template_filter("format_loc")
def format_loc(
loc,
commit=None,
filename=None,
tree_id=None,
prequest=None,
index=None,
isprdiff=False,
):
""" Template filter putting the provided lines of code into a table
"""
if loc is None:
return
output = ['<div class="highlight">', '<table class="code_table">']
commit_hash = commit
if hasattr(commit_hash, "hex"):
commit_hash = commit_hash.hex
comments = {}
if prequest and not isinstance(prequest, flask.wrappers.Request):
for com in prequest.comments:
if (
commit
and com.commit_id == commit_hash
and com.filename == filename
):
if com.line in comments:
comments[com.line].append(com)
else:
comments[com.line] = [com]
for key in comments:
comments[key] = sorted(comments[key], key=lambda obj: obj.date_created)
if not index:
index = ""
cnt = 1
for line in loc.split("\n"):
if filename and commit:
if isinstance(filename, str) and six.PY2:
filename = filename.decode("UTF-8")
if isprdiff and (
line.startswith("@@")
or line.startswith("+")
or line.startswith("-")
):
if line.startswith("@@"):
output.append(
'<tr class="stretch-table-column bg-light"\
id="c-%(commit)s-%(cnt_lbl)s">'
% ({"cnt_lbl": cnt, "commit": commit})
)
elif line.startswith("+"):
output.append(
'<tr class="stretch-table-column alert-success" \
id="c-%(commit)s-%(cnt_lbl)s">'
% ({"cnt_lbl": cnt, "commit": commit})
)
elif line.startswith("-"):
output.append(
'<tr class="stretch-table-column alert-danger" \
id="c-%(commit)s-%(cnt_lbl)s">'
% ({"cnt_lbl": cnt, "commit": commit})
)
else:
output.append(
'<tr id="c-%(commit)s-%(cnt_lbl)s">'
% ({"cnt_lbl": cnt, "commit": commit})
)
output.append(
'<td class="cell1">'
'<a id="%(cnt)s" href="#%(cnt)s" data-line-number='
'"%(cnt_lbl)s" data-file-number='
'"%(line)s"></a></td>'
'<td class="prc border-right" data-row="%(cnt_lbl)s"'
' data-filename="%(filename)s" data-commit="%(commit)s"'
' data-tree="%(tree_id)s">'
"<p>"
'<span class="fa fa-comment prc_img hidden"'
'alt="Add comment" title="Add comment"></span>'
"</p>"
"</td>"
% (
{
"cnt": "_%s__%s" % (index, cnt),
"cnt_lbl": cnt,
"line": index,
"filename": filename,
"commit": commit,
"tree_id": tree_id,
}
)
)
else:
output.append(
'<tr><td class="cell1">'
'<a id="%(cnt)s" href="#%(cnt)s" data-line-number='
'"%(cnt_lbl)s"></a></td>'
% ({"cnt": "%s_%s" % (index, cnt), "cnt_lbl": cnt})
)
cnt += 1
if not line:
output.append(line)
continue
if line.startswith("@@"):
if prequest and prequest.project_from:
rangeline = (
line.partition("@@ ")[2]
if line.partition("@@ ")[1] == "@@ "
else None
)
if rangeline:
rangeline = rangeline.split(" @@")[0]
linenumber = rangeline.split("+")[1].split(",")[0]
line = line + ' <a href="%s#_%s" target="_blank" ' % (
flask.url_for(
"ui_ns.view_file",
repo=prequest.project_from.name,
username=prequest.project_from.user.username
if prequest.project_from.is_fork
else None,
namespace=prequest.project_from.namespace,
identifier=prequest.branch_from,
filename=filename,
),
linenumber,
)
line = (
line
+ 'class="open_changed_file_icon_wrap">'
+ '<span class="fa fa-file-code-o fa-fw" '
+ 'alt="Open changed file" '
+ 'title="Open changed file"></span></a>'
)
if isprdiff and (
line.startswith("@@")
or line.startswith("+")
or line.startswith("-")
):
if line.startswith("@@"):
output.append(
'<td class="cell2 stretch-table-column">\
<pre class="text-muted"><code>%s</code></pre></td>'
% line
)
elif line.startswith("+"):
output.append(
'<td class="cell2 stretch-table-column">\
<pre class="alert-success"><code>%s</code></pre></td>'
% escape(line)
)
elif line.startswith("-"):
output.append(
'<td class="cell2 stretch-table-column">\
<pre class="alert-danger"><code>%s</code></pre></td>'
% escape(line)
)
else:
output.append(
'<td class="cell2"><pre><code>%s</code></pre></td>'
% (escape(line))
)
output.append("</tr>")
tpl_edit = (
'<a href="%(edit_url)s" '
'class="btn btn-outline-primary border-0" '
'data-comment="%(commentid)s" '
'data-objid="%(requestid)s">'
'<i class="fa fa-pencil"></i>'
"</a>"
)
tpl_edited = (
'<small class="text-muted" title="%(edit_date)s"> '
"Edited %(human_edit_date)s by %(user)s </small>"
)
tpl_delete = (
'<button class="btn btn-outline-primary border-0 '
'delete_comment_btn" title="Remove comment" name="drop_comment"'
'value="%(commentid)s" type="submit">'
'<i class="fa fa-trash"></i>'
"</button>"
)
if cnt - 1 in comments:
for comment in comments[cnt - 1]:
templ_delete = ""
templ_edit = ""
templ_edited = ""
if authenticated() and (
(
is_true(comment.parent.status, ["true", "open"])
and comment.user.user == flask.g.fas_user.username
)
or is_repo_committer(comment.parent.project)
):
templ_delete = tpl_delete % ({"commentid": comment.id})
templ_edit = tpl_edit % (
{
"edit_url": flask.url_for(
"ui_ns.pull_request_edit_comment",
repo=comment.parent.project.name,
namespace=comment.parent.project.namespace,
requestid=comment.parent.id,
commentid=comment.id,
username=comment.parent.user.user
if comment.parent.project.is_fork
else None,
),
"requestid": comment.parent.id,
"commentid": comment.id,
}
)
if comment.edited_on:
templ_edited = tpl_edited % (
{
"edit_date": format_ts(comment.edited_on),
"human_edit_date": humanize_date(
comment.edited_on
),
"user": comment.editor.user,
}
)
output.append(
'<tr class="inline-pr-comment">'
'<td colspan="3" class="p-3 border">'
'<div class="card clearfix">'
'<div class="card-header bg-light d-flex '
'align-items-center px-3 py-2">'
"<div>"
'<div id="comment-%(commentid)s">'
'<img class="avatar circle" src="%(avatar_url)s"/>'
'<a href="%(url)s" title="%(user_html)s">'
"%(user)s</a> commented "
'<a class="headerlink" title="Permalink '
'to this headline" href="#comment-%(commentid)s">'
'<span title="%(date)s">%(human_date)s</span>'
"</a></div>"
"</div>"
'<div class="mr-auto">'
"%(templ_edit)s"
"%(templ_delete)s"
"</div>"
"</div>"
'<div class="card-block">'
"<small></small>"
'<section class="issue_comment">'
'<div class="comment_body">'
"%(comment)s"
"</div>"
"</section>"
"</div></div>"
"</td></tr>"
% (
{
"url": flask.url_for(
"ui_ns.view_user", username=comment.user.user
),
"templ_delete": templ_delete,
"templ_edit": templ_edit,
"templ_edited": templ_edited,
"user": comment.user.user,
"user_html": comment.user.html_title,
"avatar_url": avatar_url(
comment.user.default_email, 16
),
"date": format_ts(comment.date_created),
"human_date": humanize_date(comment.date_created),
"comment": markdown_filter(comment.comment),
"commentid": comment.id,
}
)
)
output.append("</table></div>")
return "\n".join(output)
@UI_NS.app_template_filter("blame_loc")
def blame_loc(loc, repo, username, blame):
""" Template filter putting the provided lines of code into a table
This method blame lines of code (loc) takes as input a text (lines of
code) concerning a given repo, with its repo and a pygit2.Blame object
and convert it into a html table displayed to the user with the git
blame information (user, commit, commit date).
:arg loc: a unicode object of the lines of code to display (in this case,
most likely the content of a file).
:arg repo: the name of the repo in which this file is.
:arg username: the user name of the user whose repo this is, if the repo
is not a *fork*, this value is ``None``.
:arg blame: a pygit2.Blame object allowing us to link a given line of
code to a commit.
"""
if loc is None:
return
if not isinstance(loc, six.text_type):
raise ValueError('"loc" must be a unicode string, not %s' % type(loc))
output = ['<div class="highlight">', '<table class="code_table">']
for idx, line in enumerate(loc.split("\n")):
if line == "</pre></div>":
break
try:
diff = blame.for_line(idx + 1)
except IndexError:
# Happens at the end of the file, since we are using idx + 1
continue
output.append(
'<tr><td class="cell1">'
'<a id="%(cnt)s" href="#%(cnt)s" data-line-number='
'"%(cnt)s"></a></td>' % ({"cnt": idx + 1})
)
committer = None
try:
committer = diff.orig_committer
except ValueError:
pass
output.append(
'<td class="cell_user">%s</td>'
% (
author_to_user(committer, with_name=False)
if committer
else " "
)
)
output.append(
'<td class="cell_commit"><a href="%s">%s</a></td>'
% (
flask.url_for(
"ui_ns.view_commit",
repo=repo.name,
username=username,
namespace=repo.namespace,
commitid=diff.final_commit_id,
),
shorted_commit(diff.final_commit_id),
)
)
output.append(
'<td class="cell2"><pre><code>%s</code></pre></td>' % no_js(line)
)
output.append("</tr>")
output.append("</table></div>")
return "\n".join(output)
@UI_NS.app_template_filter("wraps")
def text_wraps(text, size=10):
""" Template filter to wrap text at a specified size
"""
if text:
parts = textwrap.wrap(text, size)
if len(parts) > 1:
parts = "%s..." % parts[0]
else:
parts = parts[0]
return parts
@UI_NS.app_template_filter("avatar")
def avatar(packager, size=64, css_class=None):
""" Template filter that returns html for avatar of any given Username.
"""
if not isinstance(packager, six.text_type):
packager = packager.decode("utf-8")
if "@" not in packager:
user = pagure.lib.query.search_user(flask.g.session, username=packager)
if user:
packager = user.default_email
class_string = "avatar circle"
if css_class:
class_string = class_string + " " + css_class
output = '<img class="%s" src="%s"/>' % (
class_string,
avatar_url(packager, size),
)
return output
@UI_NS.app_template_filter("avatar_url")
def avatar_url(email, size=64):
""" Template filter that returns html for avatar of any given Email.
"""
return pagure.lib.query.avatar_url_from_email(email, size)
@UI_NS.app_template_filter("short")
def shorted_commit(cid):
"""Gets short version of the commit id"""
return ("%s" % cid)[: pagure_config["SHORT_LENGTH"]]
@UI_NS.app_template_filter("markdown")
def markdown_filter(text):
""" Template filter converting a string into html content using the
markdown library.
"""
return pagure.lib.query.text2markdown(text)
@UI_NS.app_template_filter("patch_to_diff")
def patch_to_diff(patch):
"""Render a hunk as a diff"""
content = []
for hunk in patch.hunks:
content.append(
"@@ -%i,%i +%i,%i @@\n"
% (hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines)
)
for line in hunk.lines:
if hasattr(line, "content"):
origin = line.origin
if line.origin in ["<", ">", "="]:
origin = ""
content.append(origin + " " + line.content)
else:
# Avoid situation where at the end of a file we get:
# + foo<
# \ No newline at end of file
if line[0] in ["<", ">", "="]:
line = ("", line[1])
content.append(" ".join(line))
return "".join(content)
@UI_NS.app_template_filter("author2user")
def author_to_user(author, size=16, cssclass=None, with_name=True):
""" Template filter transforming a pygit2 Author object into a text
either with just the username or linking to the user in pagure.
"""
output = escape(author.name)
if not author.email:
return output
user = pagure.lib.query.search_user(flask.g.session, email=author.email)
if user:
output = (
"%(avatar)s <a title='%(name)s' href='%(url)s' "
"%(cssclass)s>%(username)s</a>"
)
if not with_name:
output = (
"<a title='%(name)s' href='%(url)s' "
"%(cssclass)s>%(avatar)s</a>"
)
output = output % (
{
"avatar": avatar(user.default_email, size),
"url": flask.url_for(
"ui_ns.view_user", username=user.username
),
"cssclass": ('class="%s"' % cssclass) if cssclass else "",
"username": user.username,
"name": escape(author.name),
}
)
return output
@UI_NS.app_template_filter("author2avatar")
def author_to_avatar(author, size=32):
""" Template filter transforming a pygit2 Author object into an avatar.
"""
if not author.email:
return ""
user = pagure.lib.query.search_user(flask.g.session, email=author.email)
output = user.default_email if user else author.email
return avatar(output.encode("utf-8"), size)
@UI_NS.app_template_filter("author2user_commits")
def author_to_user_commits(author, link, size=16, cssclass=None):
""" Template filter transforming a pygit2 Author object into a text
either with just the username or linking to the user in pagure.
"""
output = author.name
if not author.email:
return output
user = pagure.lib.query.search_user(flask.g.session, email=author.email)
if user:
output = "<a href='%s'>%s</a> <a href='%s' %s>%s</a>" % (
flask.url_for("ui_ns.view_user", username=user.username),
avatar(user.default_email, size),
link,
('class="%s"' % cssclass) if cssclass else "",
author.name,
)
return output
@UI_NS.app_template_filter("InsertDiv")
def insert_div(content):
""" Template filter inserting an opening <div> and closing </div>
after the first title and then at the end of the content.
"""
# This is quite a hack but simpler solution using .replace() didn't work
# for some reasons...
content = content.split("\n")
output = []
for row in content:
if row.startswith('<div class="document" id='):
continue
if '<h1 class="title">' in row:
row = ("%s" % row).replace(
'<h1 class="title">',
'<h1 class="title">'
'<span class="oi" data-glyph="collapse-down"></span> ',
)
output.append(row)
output = "\n".join(output)
output = output.replace("</h1>", "</h1>\n<div>", 1)
output = output.replace("h1", "h3")
return output
@UI_NS.app_template_filter("noJS")
def no_js(content, ignore=None):
""" Template filter replacing <script by <script and </script> by
</script>
"""
return pagure.lib.query.clean_input(content, ignore=ignore)
@UI_NS.app_template_filter("toRGB")
def int_to_rgb(percent):
""" Template filter converting a given percentage to a css RGB value.
"""
output = "rgb(255, 0, 0);"
try:
percent = int(percent)
if percent < 50:
red = 255
green = (255.0 / 50) * percent
else:
green = 255
red = (255.0 / 50) * (100 - percent)
output = "rgb(%s, %s, 0);" % (int(red), int(green))
except ValueError:
pass
return output
@UI_NS.app_template_filter("increment_largest_priority")
def largest_priority(dictionary):
""" Template filter to return the largest priority +1
"""
if dictionary:
keys = [int(k) for k in dictionary if k]
if keys:
return max(keys) + 1
return 1
@UI_NS.app_template_filter("unicode")
def convert_unicode(text):
""" If the provided string is a binary string, this filter converts it
to UTF-8 (unicode).
"""
if isinstance(text, str) and six.PY2:
return text.decode("utf8")
else:
return text
@UI_NS.app_template_filter("combine_url")
def combine_url(url, page, pagetitle, **kwargs):
""" Add the specified arguments in the provided kwargs dictionary to
the given URL.
"""
url_obj = urlparse(url)
url = url_obj.geturl().replace(url_obj.query, "").rstrip("?")
query = {}
for k, v in parse_qsl(url_obj.query):
if k in query:
if isinstance(query[k], list):
query[k].append(v)
else:
query[k] = [query[k], v]
else:
query[k] = v
query[pagetitle] = page
query.update(kwargs)
args = ""
for key in query:
if isinstance(query[key], list):
for val in query[key]:
args += "&%s=%s" % (key, val)
else:
args += "&%s=%s" % (key, query[key])
return url + "?" + args[1:]
@UI_NS.app_template_filter("add_or_remove")
def add_or_remove(item, items):
""" Adds the item to the list if it is not in there and remove it
otherwise.
"""
if item in items:
items.remove(item)
else:
items.append(item)
return items
@UI_NS.app_template_filter("table_sort_arrow")
def table_sort_arrow(column, order_key, order):
""" Outputs an arrow icon if the column is currently being sorted on
"""
arrow_html = '<span class="oi" data-glyph="arrow-thick-{0}"></span>'
if column == order_key:
if order == "desc":
return arrow_html.format("bottom")
else:
return arrow_html.format("top")
return ""
@UI_NS.app_template_filter("table_get_link_order")
def table_get_link_order(column, order_key, order):
""" Get the correct order parameter value for the table heading link
"""
if column == order_key:
# If the user is clicking on the column again, they want the
# oposite order
if order == "desc":
return "asc"
else:
return "desc"
else:
# Default to descending
return "desc"
@UI_NS.app_template_filter("flag2label")
def flag_to_label(flag):
""" For a given flag return the bootstrap label to use
"""
return pagure_config["FLAG_STATUSES_LABELS"][flag.status.lower()]
@UI_NS.app_template_filter("join_prefix")
def join_prefix(values, num):
"""Produce a string joining first `num` items in the list and indicate
total number total number of items.
"""
if len(values) <= 1:
return "".join(values)
if len(values) <= num:
return ", ".join(values[:-1]) + " and " + values[-1]
return "%s and %d others" % (", ".join(values[:num]), len(values) - num)
@UI_NS.app_template_filter("user_can_clone_ssh")
def user_can_clone_ssh(username):
has_ssh_keys = False
if flask.g.authenticated:
has_ssh_keys = (
len(
pagure.lib.query.search_user(
flask.g.session, username=flask.g.fas_user.username
).sshkeys
)
!= 0
)
always_render = pagure_config.get("ALWAYS_RENDER_SSH_CLONE_URL")
return always_render or has_ssh_keys
@UI_NS.app_template_filter("git_url_ssh")
def get_git_url_ssh(complement=""):
""" Return the GIT SSH URL to be displayed in the UI based on the
content of the configuration file.
"""
git_url_ssh = pagure_config.get("GIT_URL_SSH")
if flask.g.authenticated and git_url_ssh:
try:
git_url_ssh = git_url_ssh.format(
username=flask.g.fas_user.username
)
except (KeyError, IndexError):
pass
return git_url_ssh + complement
@UI_NS.app_template_filter("patch_stats")
def get_patch_stats(patch):
""" Return a dict of stats about the provided patch."""
try:
output = pagure.lib.git.get_stats_patch(patch)
except pagure.exceptions.PagureException:
_log.exception("Failed to get stats on a patch")
output = {}
return output