454 lines
14 KiB
Python
454 lines
14 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
"""
|
||
|
(c) 2014-2016 - 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
|
||
|
|
||
|
|
||
|
import datetime
|
||
|
import textwrap
|
||
|
import urlparse
|
||
|
|
||
|
import arrow
|
||
|
import flask
|
||
|
import md5
|
||
|
|
||
|
from pygments import highlight
|
||
|
from pygments.lexers.text import DiffLexer
|
||
|
from pygments.formatters import HtmlFormatter
|
||
|
|
||
|
import pagure.exceptions
|
||
|
import pagure.lib
|
||
|
import pagure.forms
|
||
|
from pagure import (APP, SESSION, authenticated, is_repo_admin)
|
||
|
|
||
|
|
||
|
# Jinja filters
|
||
|
|
||
|
|
||
|
@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)
|
||
|
|
||
|
|
||
|
@APP.template_filter('render')
|
||
|
def jinja_render(tmpl, **kwargs):
|
||
|
""" Render the given template with the provided arguments
|
||
|
"""
|
||
|
return flask.render_template_string(tmpl, **kwargs)
|
||
|
|
||
|
|
||
|
@APP.template_filter('humanize')
|
||
|
def humanize_date(date):
|
||
|
""" Template filter returning the last commit date of the provided repo.
|
||
|
"""
|
||
|
return arrow.get(date).humanize()
|
||
|
|
||
|
|
||
|
@APP.template_filter('format_ts')
|
||
|
def format_ts(string):
|
||
|
""" Template filter transforming a timestamp to a date
|
||
|
"""
|
||
|
dattime = datetime.datetime.fromtimestamp(int(string))
|
||
|
return dattime.strftime('%b %d %Y %H:%M:%S')
|
||
|
|
||
|
|
||
|
@APP.template_filter('format_loc')
|
||
|
def format_loc(loc, commit=None, filename=None, tree_id=None, prequest=None,
|
||
|
index=None):
|
||
|
""" Template filter putting the provided lines of code into a table
|
||
|
"""
|
||
|
if loc is None:
|
||
|
return
|
||
|
|
||
|
output = [
|
||
|
'<div class="highlight">',
|
||
|
'<table class="code_table">'
|
||
|
]
|
||
|
|
||
|
comments = {}
|
||
|
if prequest and not isinstance(prequest, flask.wrappers.Request):
|
||
|
for com in prequest.comments:
|
||
|
if commit and unicode(com.commit_id) == unicode(commit) \
|
||
|
and unicode(com.filename) == unicode(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 line == '</pre></div>':
|
||
|
break
|
||
|
if filename and commit:
|
||
|
output.append(
|
||
|
'<tr id="c-%(commit)s-%(cnt_lbl)s"><td class="cell1">'
|
||
|
'<a id="%(cnt)s" href="#%(cnt)s" data-line-number='
|
||
|
'"%(cnt_lbl)s"></a></td>'
|
||
|
'<td class="prc" data-row="%(cnt_lbl)s"'
|
||
|
' data-filename="%(filename)s" data-commit="%(commit)s"'
|
||
|
' data-tree="%(tree_id)s">'
|
||
|
'<p>'
|
||
|
'<span class="oi prc_img" data-glyph="comment-square" '
|
||
|
'alt="Add comment" title="Add comment"></span>'
|
||
|
'</p>'
|
||
|
'</td>' % (
|
||
|
{
|
||
|
'cnt': '%s_%s' % (index, cnt),
|
||
|
'cnt_lbl': cnt,
|
||
|
'filename': filename.decode('UTF-8'),
|
||
|
'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('<div'):
|
||
|
line = line.split('<pre style="line-height: 125%">')[1]
|
||
|
output.append('<td class="cell2"><pre>%s</pre></td>' % line)
|
||
|
output.append('</tr>')
|
||
|
|
||
|
tpl_edit = '<a href="%(edit_url)s" ' \
|
||
|
'class="btn btn-secondary btn-sm" data-comment="%(commentid)s" ' \
|
||
|
'data-objid="%(requestid)s">' \
|
||
|
'<span class="oi" data-glyph="pencil"></span>' \
|
||
|
'</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-secondary btn-sm" '\
|
||
|
'title="Remove comment" '\
|
||
|
'name="drop_comment" value="%(commentid)s" type="submit" ' \
|
||
|
'onclick="return confirm(\'Do you really want to remove this comment?\');" '\
|
||
|
'><span class="oi" data-glyph="trash"></span>' \
|
||
|
'</button>'
|
||
|
|
||
|
if cnt - 1 in comments:
|
||
|
for comment in comments[cnt - 1]:
|
||
|
|
||
|
templ_delete = ''
|
||
|
templ_edit = ''
|
||
|
templ_edited = ''
|
||
|
status = str(comment.parent.status).lower()
|
||
|
if authenticated() and (
|
||
|
(
|
||
|
status in ['true', 'open']
|
||
|
and comment.user.user == flask.g.fas_user.username
|
||
|
)
|
||
|
or is_repo_admin(comment.parent.project)):
|
||
|
templ_delete = tpl_delete % ({'commentid': comment.id})
|
||
|
templ_edit = tpl_edit % ({
|
||
|
'edit_url': flask.url_for(
|
||
|
'pull_request_edit_comment',
|
||
|
repo=comment.parent.project.name,
|
||
|
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': comment.edited_on.strftime(
|
||
|
'%b %d %Y %H:%M:%S'),
|
||
|
'human_edit_date': humanize_date(comment.edited_on),
|
||
|
'user': comment.editor.user,
|
||
|
})
|
||
|
|
||
|
output.append(
|
||
|
'<tr class="inline-pr-comment"><td></td>'
|
||
|
'<td colspan="2">'
|
||
|
'<div class="card clearfix m-x-1 ">'
|
||
|
'<div class="card-block">'
|
||
|
'<small><div id="comment-%(commentid)s">'
|
||
|
'<img class="avatar circle" src="%(avatar_url)s"/>'
|
||
|
'<a href="%(url)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></small>'
|
||
|
'<section class="issue_comment">'
|
||
|
'<div class="comment_body">'
|
||
|
'%(comment)s'
|
||
|
'</div>'
|
||
|
'</section>'
|
||
|
'<div class="issue_actions m-t-2">'
|
||
|
'%(templ_edited)s'
|
||
|
'<aside class="btn-group issue_action icon '
|
||
|
'pull-xs-right p-b-1">'
|
||
|
'%(templ_edit)s'
|
||
|
'%(templ_delete)s'
|
||
|
'</aside>'
|
||
|
'</div></div></div>'
|
||
|
'</td></tr>' % (
|
||
|
{
|
||
|
'url': flask.url_for(
|
||
|
'view_user', username=comment.user.user),
|
||
|
'templ_delete': templ_delete,
|
||
|
'templ_edit': templ_edit,
|
||
|
'templ_edited': templ_edited,
|
||
|
'user': comment.user.user,
|
||
|
'avatar_url': avatar_url(
|
||
|
comment.user.default_email, 16),
|
||
|
'date': comment.date_created.strftime(
|
||
|
'%b %d %Y %H:%M:%S'),
|
||
|
'human_date': humanize_date(comment.date_created),
|
||
|
'comment': markdown_filter(comment.comment),
|
||
|
'commentid': comment.id,
|
||
|
}
|
||
|
)
|
||
|
)
|
||
|
|
||
|
output.append('</table></div>')
|
||
|
|
||
|
return '\n'.join(output)
|
||
|
|
||
|
|
||
|
@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
|
||
|
|
||
|
|
||
|
@APP.template_filter('avatar')
|
||
|
def avatar(packager, size=64):
|
||
|
""" Template filter sorting the given branches, Fedora first then EPEL,
|
||
|
then whatever is left.
|
||
|
"""
|
||
|
if '@' not in packager:
|
||
|
user = pagure.lib.search_user(SESSION, username=packager)
|
||
|
if user:
|
||
|
packager = user.default_email
|
||
|
|
||
|
output = '<img class="avatar circle" src="%s"/>' % (
|
||
|
avatar_url(packager, size)
|
||
|
)
|
||
|
|
||
|
return output
|
||
|
|
||
|
|
||
|
@APP.template_filter('avatar_url')
|
||
|
def avatar_url(email, size=64):
|
||
|
""" Template filter sorting the given branches, Fedora first then EPEL,
|
||
|
then whatever is left.
|
||
|
"""
|
||
|
return pagure.lib.avatar_url_from_openid(email, size)
|
||
|
|
||
|
|
||
|
@APP.template_filter('short')
|
||
|
def shorted_commit(cid):
|
||
|
"""Gets short version of the commit id"""
|
||
|
return str(cid)[:APP.config['SHORT_LENGTH']]
|
||
|
|
||
|
|
||
|
@APP.template_filter('markdown')
|
||
|
def markdown_filter(text):
|
||
|
""" Template filter converting a string into html content using the
|
||
|
markdown library.
|
||
|
"""
|
||
|
return pagure.lib.text2markdown(text)
|
||
|
|
||
|
|
||
|
@APP.template_filter('html_diff')
|
||
|
def html_diff(diff):
|
||
|
"""Display diff as HTML"""
|
||
|
if diff is None:
|
||
|
return
|
||
|
return highlight(
|
||
|
diff,
|
||
|
DiffLexer(),
|
||
|
HtmlFormatter(
|
||
|
noclasses=True,
|
||
|
style="tango",)
|
||
|
)
|
||
|
|
||
|
|
||
|
@APP.template_filter('patch_to_diff')
|
||
|
def patch_to_diff(patch):
|
||
|
"""Render a hunk as a diff"""
|
||
|
content = ""
|
||
|
for hunk in patch.hunks:
|
||
|
content = content + "@@ -%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 = content + 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 = content + ' '.join(line)
|
||
|
|
||
|
return content
|
||
|
|
||
|
|
||
|
@APP.template_filter('author2user')
|
||
|
def author_to_user(author, size=16):
|
||
|
""" 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.search_user(SESSION, email=author.email)
|
||
|
if user:
|
||
|
output = "%s <a href='%s'>%s</a>" % (
|
||
|
avatar(user.default_email, size),
|
||
|
flask.url_for('view_user', username=user.username),
|
||
|
author.name,
|
||
|
)
|
||
|
return output
|
||
|
|
||
|
|
||
|
@APP.template_filter('author2avatar')
|
||
|
def author_to_avatar(author, size=32):
|
||
|
""" Template filter transforming a pygit2 Author object into an avatar.
|
||
|
"""
|
||
|
user = pagure.lib.search_user(SESSION, email=author.email)
|
||
|
output = user.default_email if user else author.email
|
||
|
return avatar(output.encode('utf-8'), size)
|
||
|
|
||
|
|
||
|
@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 = str(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
|
||
|
|
||
|
|
||
|
@APP.template_filter('noJS')
|
||
|
def no_js(content, ignore=None):
|
||
|
""" Template filter replacing <script by <script and </script> by
|
||
|
</script>
|
||
|
"""
|
||
|
return pagure.lib.clean_input(content, ignore=ignore)
|
||
|
|
||
|
|
||
|
@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
|
||
|
|
||
|
|
||
|
@APP.template_filter('return_md5')
|
||
|
def return_md5(text):
|
||
|
""" Template filter to return an MD5 for a string
|
||
|
"""
|
||
|
hashedtext = md5.new()
|
||
|
hashedtext.update(text)
|
||
|
return pagure.lib.clean_input(hashedtext.hexdigest())
|
||
|
|
||
|
|
||
|
@APP.template_filter('increment_largest_priority')
|
||
|
def largest_priority(dictionary):
|
||
|
""" Template filter to return the largest priority +1
|
||
|
"""
|
||
|
if dictionary:
|
||
|
return max([int(k) for k in dictionary if k]) + 1
|
||
|
else:
|
||
|
return 1
|
||
|
|
||
|
|
||
|
@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):
|
||
|
return text.decode("utf8")
|
||
|
else:
|
||
|
return text
|
||
|
|
||
|
|
||
|
@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.urlparse(url)
|
||
|
url = url_obj.geturl().replace(url_obj.query, '').rstrip('?')
|
||
|
query = dict(urlparse.parse_qsl(url_obj.query))
|
||
|
query[pagetitle] = page
|
||
|
query.update(kwargs)
|
||
|
return url + '?' + '&'.join(['%s=%s' % (k, query[k]) for k in query])
|