'.format(self.id)
+
+ id = Column('id', Integer())
+ namespace = Column('namespace', String(255), primary_key=True)
+ accessed = Column('accessed', DateTime, nullable=False)
+ created = Column('created', DateTime, nullable=False)
+ data = Column('data', PickleType, nullable=False)
diff --git a/rhodecode/lib/dbmigrate/versions/108_version_4_19_1.py b/rhodecode/lib/dbmigrate/versions/108_version_4_19_1.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/dbmigrate/versions/108_version_4_19_1.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+import logging
+from sqlalchemy import *
+
+from alembic.migration import MigrationContext
+from alembic.operations import Operations
+from sqlalchemy import BigInteger
+
+from rhodecode.lib.dbmigrate.versions import _reset_base
+from rhodecode.model import init_model_encryption
+
+
+log = logging.getLogger(__name__)
+
+
+def upgrade(migrate_engine):
+ """
+ Upgrade operations go here.
+ Don't create your own engine; bind migrate_engine to your metadata
+ """
+ _reset_base(migrate_engine)
+ from rhodecode.lib.dbmigrate.schema import db_4_19_0_2 as db
+
+ init_model_encryption(db)
+ db.ChangesetCommentHistory().__table__.create()
+
+
+def downgrade(migrate_engine):
+ meta = MetaData()
+ meta.bind = migrate_engine
+
+
+def fixups(models, _SESSION):
+ pass
diff --git a/rhodecode/lib/html_filters.py b/rhodecode/lib/html_filters.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/lib/html_filters.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2020-2020 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+## base64 filter e.g ${ example | base64,n }
+def base64(text):
+ import base64
+ from rhodecode.lib.helpers import safe_str
+ return base64.encodestring(safe_str(text))
diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py
--- a/rhodecode/model/comment.py
+++ b/rhodecode/model/comment.py
@@ -35,7 +35,13 @@ from rhodecode.lib import audit_logger
from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
from rhodecode.model import BaseModel
from rhodecode.model.db import (
- ChangesetComment, User, Notification, PullRequest, AttributeDict)
+ ChangesetComment,
+ User,
+ Notification,
+ PullRequest,
+ AttributeDict,
+ ChangesetCommentHistory,
+)
from rhodecode.model.notification import NotificationModel
from rhodecode.model.meta import Session
from rhodecode.model.settings import VcsSettingsModel
@@ -479,6 +485,54 @@ class CommentsModel(BaseModel):
return comment
+ def edit(self, comment_id, text, auth_user, version):
+ """
+ Change existing comment for commit or pull request.
+
+ :param comment_id:
+ :param text:
+ :param auth_user: current authenticated user calling this method
+ :param version: last comment version
+ """
+ if not text:
+ log.warning('Missing text for comment, skipping...')
+ return
+
+ comment = ChangesetComment.get(comment_id)
+ old_comment_text = comment.text
+ comment.text = text
+ comment_version = ChangesetCommentHistory.get_version(comment_id)
+ if (comment_version - version) != 1:
+ log.warning(
+ 'Version mismatch, skipping... '
+ 'version {} but should be {}'.format(
+ (version - 1),
+ comment_version,
+ )
+ )
+ return
+ comment_history = ChangesetCommentHistory()
+ comment_history.comment_id = comment_id
+ comment_history.version = comment_version
+ comment_history.created_by_user_id = auth_user.user_id
+ comment_history.text = old_comment_text
+ # TODO add email notification
+ Session().add(comment_history)
+ Session().add(comment)
+ Session().flush()
+
+ if comment.pull_request:
+ action = 'repo.pull_request.comment.edit'
+ else:
+ action = 'repo.commit.comment.edit'
+
+ comment_data = comment.get_api_data()
+ comment_data['old_comment_text'] = old_comment_text
+ self._log_audit_action(
+ action, {'data': comment_data}, auth_user, comment)
+
+ return comment_history
+
def delete(self, comment, auth_user):
"""
Deletes given comment
@@ -712,6 +766,7 @@ class CommentsModel(BaseModel):
.filter(ChangesetComment.line_no == None)\
.filter(ChangesetComment.f_path == None)\
.filter(ChangesetComment.pull_request == pull_request)
+
return comments
@staticmethod
diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py
--- a/rhodecode/model/db.py
+++ b/rhodecode/model/db.py
@@ -3755,6 +3755,7 @@ class ChangesetComment(Base, BaseModel):
status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
pull_request = relationship('PullRequest', lazy='joined')
pull_request_version = relationship('PullRequestVersion')
+ history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
@classmethod
def get_users(cls, revision=None, pull_request_id=None):
@@ -3849,6 +3850,36 @@ class ChangesetComment(Base, BaseModel):
return data
+class ChangesetCommentHistory(Base, BaseModel):
+ __tablename__ = 'changeset_comments_history'
+ __table_args__ = (
+ Index('cch_comment_id_idx', 'comment_id'),
+ base_table_args,
+ )
+
+ comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
+ comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
+ version = Column("version", Integer(), nullable=False, default=0)
+ created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
+ text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
+ created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
+ deleted = Column('deleted', Boolean(), default=False)
+
+ author = relationship('User', lazy='joined')
+ comment = relationship('ChangesetComment', cascade="all, delete")
+
+ @classmethod
+ def get_version(cls, comment_id):
+ q = Session().query(ChangesetCommentHistory).filter(
+ ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
+ if q.count() == 0:
+ return 1
+ elif q.count() >= q[0].version:
+ return q.count() + 1
+ else:
+ return q[0].version + 1
+
+
class ChangesetStatus(Base, BaseModel):
__tablename__ = 'changeset_statuses'
__table_args__ = (
diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js
--- a/rhodecode/public/js/rhodecode/routes.js
+++ b/rhodecode/public/js/rhodecode/routes.js
@@ -185,8 +185,10 @@ function registerRCRoutes() {
pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
+ pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_history_id)s/history_view', ['repo_name', 'commit_id', 'comment_history_id']);
pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
+ pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
@@ -242,6 +244,7 @@ function registerRCRoutes() {
pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
+ pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js
--- a/rhodecode/public/js/src/rhodecode/comments.js
+++ b/rhodecode/public/js/src/rhodecode/comments.js
@@ -80,9 +80,10 @@ var _submitAjaxPOST = function(url, post
})(function() {
"use strict";
- function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
+ function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
+
if (!(this instanceof CommentForm)) {
- return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
+ return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
}
// bind the element instance to our Form
@@ -126,10 +127,22 @@ var _submitAjaxPOST = function(url, post
this.submitButton = $(this.submitForm).find('input[type="submit"]');
this.submitButtonText = this.submitButton.val();
+
this.previewUrl = pyroutes.url('repo_commit_comment_preview',
{'repo_name': templateContext.repo_name,
'commit_id': templateContext.commit_data.commit_id});
+ if (edit){
+ this.submitButtonText = _gettext('Updated Comment');
+ $(this.commentType).prop('disabled', true);
+ $(this.commentType).addClass('disabled');
+ var editInfo =
+ '';
+ $(editInfo).insertBefore($(this.editButton).parent());
+ }
+
if (resolvesCommentId){
this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
@@ -153,17 +166,27 @@ var _submitAjaxPOST = function(url, post
// based on commitId, or pullRequestId decide where do we submit
// out data
if (this.commitId){
- this.submitUrl = pyroutes.url('repo_commit_comment_create',
+ var pyurl = 'repo_commit_comment_create';
+ if(edit){
+ pyurl = 'repo_commit_comment_edit';
+ }
+ this.submitUrl = pyroutes.url(pyurl,
{'repo_name': templateContext.repo_name,
- 'commit_id': this.commitId});
+ 'commit_id': this.commitId,
+ 'comment_id': comment_id});
this.selfUrl = pyroutes.url('repo_commit',
{'repo_name': templateContext.repo_name,
'commit_id': this.commitId});
} else if (this.pullRequestId) {
- this.submitUrl = pyroutes.url('pullrequest_comment_create',
+ var pyurl = 'pullrequest_comment_create';
+ if(edit){
+ pyurl = 'pullrequest_comment_edit';
+ }
+ this.submitUrl = pyroutes.url(pyurl,
{'repo_name': templateContext.repo_name,
- 'pull_request_id': this.pullRequestId});
+ 'pull_request_id': this.pullRequestId,
+ 'comment_id': comment_id});
this.selfUrl = pyroutes.url('pullrequest_show',
{'repo_name': templateContext.repo_name,
'pull_request_id': this.pullRequestId});
@@ -277,7 +300,7 @@ var _submitAjaxPOST = function(url, post
this.globalSubmitSuccessCallback = function(){
// default behaviour is to call GLOBAL hook, if it's registered.
if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
- commentFormGlobalSubmitSuccessCallback()
+ commentFormGlobalSubmitSuccessCallback();
}
};
@@ -480,13 +503,75 @@ var CommentsController = function() {
var mainComment = '#text';
var self = this;
- this.cancelComment = function(node) {
+ this.cancelComment = function (node) {
var $node = $(node);
- var $td = $node.closest('td');
+ var edit = $(this).attr('edit');
+ if (edit) {
+ var $general_comments = null;
+ var $inline_comments = $node.closest('div.inline-comments');
+ if (!$inline_comments.length) {
+ $general_comments = $('#comments');
+ var $comment = $general_comments.parent().find('div.comment:hidden');
+ // show hidden general comment form
+ $('#cb-comment-general-form-placeholder').show();
+ } else {
+ var $comment = $inline_comments.find('div.comment:hidden');
+ }
+ $comment.show();
+ }
$node.closest('.comment-inline-form').remove();
return false;
};
+ this.showVersion = function (node) {
+ var $node = $(node);
+ var selectedIndex = $node.context.selectedIndex;
+ var option = $node.find('option[value="'+ selectedIndex +'"]');
+ var zero_option = $node.find('option[value="0"]');
+ if (!option){
+ return;
+ }
+
+ // little trick to cheat onchange and allow to display the same version again
+ $node.context.selectedIndex = 0;
+ zero_option.text(selectedIndex);
+
+ var comment_history_id = option.attr('data-comment-history-id');
+ var comment_id = option.attr('data-comment-id');
+ var historyViewUrl = pyroutes.url(
+ 'repo_commit_comment_history_view',
+ {
+ 'repo_name': templateContext.repo_name,
+ 'commit_id': comment_id,
+ 'comment_history_id': comment_history_id,
+ }
+ );
+ successRenderCommit = function (data) {
+ Swal.fire({
+ html: data,
+ title: '',
+ showClass: {
+ popup: 'swal2-noanimation',
+ backdrop: 'swal2-noanimation'
+ },
+ });
+ };
+ failRenderCommit = function () {
+ Swal.fire({
+ html: 'Error while loading comment',
+ title: '',
+ showClass: {
+ popup: 'swal2-noanimation',
+ backdrop: 'swal2-noanimation'
+ },
+ });
+ };
+ _submitAjaxPOST(
+ historyViewUrl, {'csrf_token': CSRF_TOKEN}, successRenderCommit,
+ failRenderCommit
+ );
+ };
+
this.getLineNumber = function(node) {
var $node = $(node);
var lineNo = $node.closest('td').attr('data-line-no');
@@ -638,12 +723,12 @@ var CommentsController = function() {
$node.closest('tr').toggleClass('hide-line-comments');
};
- this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
+ this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
var pullRequestId = templateContext.pull_request_data.pull_request_id;
var commitId = templateContext.commit_data.commit_id;
var commentForm = new CommentForm(
- formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
+ formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
var cm = commentForm.getCmInstance();
if (resolvesCommentId){
@@ -780,18 +865,200 @@ var CommentsController = function() {
var _form = $($form[0]);
var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
+ var edit = false;
+ var comment_id = null;
var commentForm = this.createCommentForm(
- _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
+ _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
commentForm.initStatusChangeSelector();
return commentForm;
};
+ this.editComment = function(node) {
+ var $node = $(node);
+ var $comment = $(node).closest('.comment');
+ var comment_id = $comment.attr('data-comment-id');
+ var $form = null
+ var $comments = $node.closest('div.inline-comments');
+ var $general_comments = null;
+ var lineno = null;
+
+ if($comments.length){
+ // inline comments setup
+ $form = $comments.find('.comment-inline-form');
+ lineno = self.getLineNumber(node)
+ }
+ else{
+ // general comments setup
+ $comments = $('#comments');
+ $form = $comments.find('.comment-inline-form');
+ lineno = $comment[0].id
+ $('#cb-comment-general-form-placeholder').hide();
+ }
+
+ this.edit = true;
+
+ if (!$form.length) {
+
+ var $filediff = $node.closest('.filediff');
+ $filediff.removeClass('hide-comments');
+ var f_path = $filediff.attr('data-f-path');
+
+ // create a new HTML from template
+
+ var tmpl = $('#cb-comment-inline-form-template').html();
+ tmpl = tmpl.format(escapeHtml(f_path), lineno);
+ $form = $(tmpl);
+ $comment.after($form)
+
+ var _form = $($form[0]).find('form');
+ var autocompleteActions = ['as_note',];
+ var commentForm = this.createCommentForm(
+ _form, lineno, '', autocompleteActions, resolvesCommentId,
+ this.edit, comment_id);
+ var old_comment_text_binary = $comment.attr('data-comment-text');
+ var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
+ commentForm.cm.setValue(old_comment_text);
+ $comment.hide();
+
+ $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
+ form: _form,
+ parent: $comments,
+ lineno: lineno,
+ f_path: f_path}
+ );
+ // set a CUSTOM submit handler for inline comments.
+ commentForm.setHandleFormSubmit(function(o) {
+ var text = commentForm.cm.getValue();
+ var commentType = commentForm.getCommentType();
+ var resolvesCommentId = commentForm.getResolvesId();
+
+ if (text === "") {
+ return;
+ }
+ if (old_comment_text == text) {
+ Swal.fire({
+ title: 'Error',
+ html: _gettext('Comment body should be changed'),
+ showClass: {
+ popup: 'swal2-noanimation',
+ backdrop: 'swal2-noanimation'
+ },
+ });
+ return;
+ }
+ var excludeCancelBtn = false;
+ var submitEvent = true;
+ commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
+ commentForm.cm.setOption("readOnly", true);
+ var dropDown = $('#comment_history_for_comment_'+comment_id);
+
+ var version = dropDown.children().last().val()
+ if(!version){
+ version = 0;
+ }
+ var postData = {
+ 'text': text,
+ 'f_path': f_path,
+ 'line': lineno,
+ 'comment_type': commentType,
+ 'csrf_token': CSRF_TOKEN,
+ 'version': version,
+ };
+
+ var submitSuccessCallback = function(json_data) {
+ $form.remove();
+ $comment.show();
+ var postData = {
+ 'text': text,
+ 'renderer': $comment.attr('data-comment-renderer'),
+ 'csrf_token': CSRF_TOKEN
+ };
+
+ var updateCommentVersionDropDown = function () {
+ var dropDown = $('#comment_history_for_comment_'+comment_id);
+ $comment.attr('data-comment-text', btoa(text));
+ var version = json_data['comment_version']
+ var option = new Option(version, version);
+ var $option = $(option);
+ $option.attr('data-comment-history-id', json_data['comment_history_id']);
+ $option.attr('data-comment-id', json_data['comment_id']);
+ dropDown.append(option);
+ dropDown.parent().show();
+ }
+ updateCommentVersionDropDown();
+ // by default we reset state of comment preserving the text
+ var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
+ var prefix = "Error while editing of comment.\n"
+ var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
+ ajaxErrorSwal(message);
+
+ };
+ var successRenderCommit = function(o){
+ $comment.show();
+ $comment[0].lastElementChild.innerHTML = o;
+ }
+ var previewUrl = pyroutes.url('repo_commit_comment_preview',
+ {'repo_name': templateContext.repo_name,
+ 'commit_id': templateContext.commit_data.commit_id});
+
+ _submitAjaxPOST(
+ previewUrl, postData, successRenderCommit,
+ failRenderCommit
+ );
+
+ try {
+ var html = json_data.rendered_text;
+ var lineno = json_data.line_no;
+ var target_id = json_data.target_id;
+
+ $comments.find('.cb-comment-add-button').before(html);
+
+ //mark visually which comment was resolved
+ if (resolvesCommentId) {
+ commentForm.markCommentResolved(resolvesCommentId);
+ }
+
+ // run global callback on submit
+ commentForm.globalSubmitSuccessCallback();
+
+ } catch (e) {
+ console.error(e);
+ }
+
+ // re trigger the linkification of next/prev navigation
+ linkifyComments($('.inline-comment-injected'));
+ timeagoActivate();
+ tooltipActivate();
+
+ if (window.updateSticky !== undefined) {
+ // potentially our comments change the active window size, so we
+ // notify sticky elements
+ updateSticky()
+ }
+
+ commentForm.setActionButtonsDisabled(false);
+
+ };
+ var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
+ var prefix = "Error while editing comment.\n"
+ var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
+ ajaxErrorSwal(message);
+ commentForm.resetCommentFormState(text)
+ };
+ commentForm.submitAjaxPOST(
+ commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
+ });
+ }
+
+ $form.addClass('comment-inline-form-open');
+ };
this.createComment = function(node, resolutionComment) {
var resolvesCommentId = resolutionComment || null;
var $node = $(node);
var $td = $node.closest('td');
var $form = $td.find('.comment-inline-form');
+ this.edit = false;
if (!$form.length) {
@@ -816,8 +1083,9 @@ var CommentsController = function() {
var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
var _form = $($form[0]).find('form');
var autocompleteActions = ['as_note', 'as_todo'];
+ var comment_id=null;
var commentForm = this.createCommentForm(
- _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
+ _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
$.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
form: _form,
diff --git a/rhodecode/public/js/src/rhodecode/utils/string.js b/rhodecode/public/js/src/rhodecode/utils/string.js
--- a/rhodecode/public/js/src/rhodecode/utils/string.js
+++ b/rhodecode/public/js/src/rhodecode/utils/string.js
@@ -182,3 +182,9 @@ var htmlEnDeCode = (function() {
htmlDecode: htmlDecode
};
})();
+
+function b64DecodeUnicode(str) {
+ return decodeURIComponent(atob(str).split('').map(function (c) {
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+ }).join(''));
+}
diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako
--- a/rhodecode/templates/base/base.mako
+++ b/rhodecode/templates/base/base.mako
@@ -1,11 +1,7 @@
## -*- coding: utf-8 -*-
<%!
- ## base64 filter e.g ${ example | base64 }
- def base64(text):
- import base64
- from rhodecode.lib.helpers import safe_str
- return base64.encodestring(safe_str(text))
+ from rhodecode.lib import html_filters
%>
<%inherit file="root.mako"/>
diff --git a/rhodecode/templates/changeset/changeset_file_comment.mako b/rhodecode/templates/changeset/changeset_file_comment.mako
--- a/rhodecode/templates/changeset/changeset_file_comment.mako
+++ b/rhodecode/templates/changeset/changeset_file_comment.mako
@@ -3,8 +3,12 @@
## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
## ${comment.comment_block(comment)}
##
+
+<%!
+ from rhodecode.lib import html_filters
+%>
+
<%namespace name="base" file="/base/base.mako"/>
-
<%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
<% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
<% latest_ver = len(getattr(c, 'versions', [])) %>
@@ -21,6 +25,8 @@
line="${comment.line_no}"
data-comment-id="${comment.comment_id}"
data-comment-type="${comment.comment_type}"
+ data-comment-renderer="${comment.renderer}"
+ data-comment-text="${comment.text | html_filters.base64,n}"
data-comment-line-no="${comment.line_no}"
data-comment-inline=${h.json.dumps(inline)}
style="${'display: none;' if outdated_at_ver else ''}">
@@ -60,6 +66,31 @@
${h.age_component(comment.modified_at, time_is_local=True)}
+ % if comment.history:
+
+
+
+
+ % else:
+
+
+
+
+ %endif
% if inline:
% else:
@@ -136,21 +167,29 @@
%if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
## permissions to delete
%if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
- ## TODO: dan: add edit comment here
-
+ %if comment.comment_type == 'note':
+
+ %else:
+
+ %endif
+ |
%else:
-
+
+ |
%endif
%else:
-
+
+ |
%endif
% if outdated_at_ver:
- |
- |
+ |
+ |
% else:
- |
- |
+ |
+ |
% endif
diff --git a/rhodecode/templates/changeset/comment_history.mako b/rhodecode/templates/changeset/comment_history.mako
new file mode 100644
--- /dev/null
+++ b/rhodecode/templates/changeset/comment_history.mako
@@ -0,0 +1,7 @@
+<%namespace name="base" file="/base/base.mako"/>
+
+${c.comment_history.author.email}
+${base.gravatar_with_user(c.comment_history.author.email, 16, tooltip=True)}
+${h.age_component(c.comment_history.created_on)}
+${c.comment_history.text}
+${c.comment_history.version}
\ No newline at end of file