##// END OF EJS Templates
comment-history: fixes/ui changes...
marcink -
r4408:1349565d default
parent child Browse files
Show More
@@ -378,9 +378,9 b' class TestRepoCommitCommentsView(TestCon'
378 378 'text': test_text_v2,
379 379 'version': '0',
380 380 },
381 status=404,
381 status=409,
382 382 )
383 assert response.status_int == 404
383 assert response.status_int == 409
384 384
385 385 text_form_db = ChangesetComment.query().filter(
386 386 ChangesetComment.comment_id == comment_id).first().text
@@ -480,9 +480,7 b' class TestPullrequestsView(object):'
480 480 )
481 481 assert response.status_int == 404
482 482
483 def test_comment_and_try_edit_already_edited(
484 self, pr_util, csrf_token, xhr_header
485 ):
483 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
486 484 pull_request = pr_util.create_pull_request()
487 485 response = self.app.post(
488 486 route_path(
@@ -498,8 +496,9 b' class TestPullrequestsView(object):'
498 496 assert response.json
499 497 comment_id = response.json.get('comment_id', None)
500 498 assert comment_id
499
501 500 test_text = 'test'
502 response = self.app.post(
501 self.app.post(
503 502 route_path(
504 503 'pullrequest_comment_edit',
505 504 repo_name=pull_request.target_repo.scm_instance().name,
@@ -528,9 +527,9 b' class TestPullrequestsView(object):'
528 527 'text': test_text_v2,
529 528 'version': '0',
530 529 },
531 status=404,
530 status=409,
532 531 )
533 assert response.status_int == 404
532 assert response.status_int == 409
534 533
535 534 text_form_db = ChangesetComment.query().filter(
536 535 ChangesetComment.comment_id == comment_id).first().text
@@ -20,9 +20,9 b''
20 20
21 21
22 22 import logging
23 import collections
24 23
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
@@ -39,7 +39,7 b' from rhodecode.lib.compat import Ordered'
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
@@ -595,6 +595,8 b' class RepoCommitsView(RepoAppView):'
595 595 route_name='repo_commit_comment_edit', request_method='POST',
596 596 renderer='json_ext')
597 597 def repo_commit_comment_edit(self):
598 self.load_default_context()
599
598 600 comment_id = self.request.matchdict['comment_id']
599 601 comment = ChangesetComment.get_or_404(comment_id)
600 602
@@ -615,11 +617,12 b' class RepoCommitsView(RepoAppView):'
615 617 log.warning(
616 618 'Comment(repo): '
617 619 'Trying to create new version '
618 'of existing comment {}'.format(
620 'with the same comment body {}'.format(
619 621 comment_id,
620 622 )
621 623 )
622 624 raise HTTPNotFound()
625
623 626 if version.isdigit():
624 627 version = int(version)
625 628 else:
@@ -633,19 +636,28 b' class RepoCommitsView(RepoAppView):'
633 636 )
634 637 raise HTTPNotFound()
635 638
639 try:
636 640 comment_history = CommentsModel().edit(
637 641 comment_id=comment_id,
638 642 text=text,
639 643 auth_user=self._rhodecode_user,
640 644 version=version,
641 645 )
646 except CommentVersionMismatch:
647 raise HTTPConflict()
648
642 649 if not comment_history:
643 650 raise HTTPNotFound()
651
644 652 Session().commit()
645 653 return {
646 654 'comment_history_id': comment_history.comment_history_id,
647 655 'comment_id': comment.comment_id,
648 656 'comment_version': comment_history.version,
657 'comment_author_username': comment_history.author.username,
658 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
659 'comment_created_on': h.age_component(comment_history.created_on,
660 time_is_local=True),
649 661 }
650 662 else:
651 663 log.warning('No permissions for user %s to edit comment_id: %s',
@@ -25,7 +25,7 b' import formencode'
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
@@ -34,6 +34,7 b' from rhodecode.apps._base import RepoApp'
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 38 from rhodecode.lib.ext_json import json
38 39 from rhodecode.lib.auth import (
39 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
@@ -1528,6 +1529,8 b' class RepoPullRequestsView(RepoAppView, '
1528 1529 route_name='pullrequest_comment_edit', request_method='POST',
1529 1530 renderer='json_ext')
1530 1531 def pull_request_comment_edit(self):
1532 self.load_default_context()
1533
1531 1534 pull_request = PullRequest.get_or_404(
1532 1535 self.request.matchdict['pull_request_id']
1533 1536 )
@@ -1566,11 +1569,12 b' class RepoPullRequestsView(RepoAppView, '
1566 1569 log.warning(
1567 1570 'Comment(PR): '
1568 1571 'Trying to create new version '
1569 'of existing comment {}'.format(
1572 'with the same comment body {}'.format(
1570 1573 comment_id,
1571 1574 )
1572 1575 )
1573 1576 raise HTTPNotFound()
1577
1574 1578 if version.isdigit():
1575 1579 version = int(version)
1576 1580 else:
@@ -1584,24 +1588,30 b' class RepoPullRequestsView(RepoAppView, '
1584 1588 )
1585 1589 raise HTTPNotFound()
1586 1590
1591 try:
1587 1592 comment_history = CommentsModel().edit(
1588 1593 comment_id=comment_id,
1589 1594 text=text,
1590 1595 auth_user=self._rhodecode_user,
1591 1596 version=version,
1592 1597 )
1598 except CommentVersionMismatch:
1599 raise HTTPConflict()
1600
1593 1601 if not comment_history:
1594 1602 raise HTTPNotFound()
1603
1595 1604 Session().commit()
1596 1605 return {
1597 1606 'comment_history_id': comment_history.comment_history_id,
1598 1607 'comment_id': comment.comment_id,
1599 1608 'comment_version': comment_history.version,
1609 'comment_author_username': comment_history.author.username,
1610 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1611 'comment_created_on': h.age_component(comment_history.created_on,
1612 time_is_local=True),
1600 1613 }
1601 1614 else:
1602 log.warning(
1603 'No permissions for user {} to edit comment_id: {}'.format(
1604 self._rhodecode_db_user, comment_id
1605 )
1606 )
1615 log.warning('No permissions for user %s to edit comment_id: %s',
1616 self._rhodecode_db_user, comment_id)
1607 1617 raise HTTPNotFound()
@@ -177,3 +177,7 b' class ArtifactMetadataDuplicate(ValueErr'
177 177
178 178 class ArtifactMetadataBadValueType(ValueError):
179 179 pass
180
181
182 class CommentVersionMismatch(ValueError):
183 pass
@@ -24,6 +24,7 b' Helper functions'
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 import base64
27 28
28 29 import os
29 30 import random
@@ -1352,7 +1353,7 b' class InitialsGravatar(object):'
1352 1353
1353 1354 def generate_svg(self, svg_type=None):
1354 1355 img_data = self.get_img_data(svg_type)
1355 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1356 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1356 1357
1357 1358
1358 1359 def initials_gravatar(email_address, first_name, last_name, size=30):
@@ -33,6 +33,7 b' from sqlalchemy.sql.functions import coa'
33 33
34 34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
37 38 from rhodecode.model import BaseModel
38 39 from rhodecode.model.db import (
@@ -507,13 +508,13 b' class CommentsModel(BaseModel):'
507 508 comment_version = ChangesetCommentHistory.get_version(comment_id)
508 509 if (comment_version - version) != 1:
509 510 log.warning(
510 'Version mismatch, skipping... '
511 'version {} but should be {}'.format(
512 (version - 1),
511 'Version mismatch comment_version {} submitted {}, skipping'.format(
513 512 comment_version,
513 version
514 514 )
515 515 )
516 return
516 raise CommentVersionMismatch()
517
517 518 comment_history = ChangesetCommentHistory()
518 519 comment_history.comment_id = comment_id
519 520 comment_history.version = comment_version
@@ -148,6 +148,38 b' select.select2{height:28px;visibility:hi'
148 148 margin: 0;
149 149 }
150 150
151
152 .drop-menu-comment-history {
153 .drop-menu-core;
154 border: none;
155 padding: 0 6px 0 0;
156 width: auto;
157 min-width: 0;
158 margin: 0;
159 position: relative;
160 display: inline-block;
161 line-height: 1em;
162 z-index: 2;
163 cursor: pointer;
164
165 a {
166 display:block;
167 padding: 0;
168 position: relative;
169
170 &:after {
171 position: absolute;
172 content: "\00A0\25BE";
173 right: -0.80em;
174 line-height: 1em;
175 top: -0.20em;
176 width: 1em;
177 font-size: 16px;
178 }
179 }
180
181 }
182
151 183 .field-sm .drop-menu {
152 184 padding: 1px 0 0 0;
153 185 a {
@@ -496,6 +496,43 b' var _submitAjaxPOST = function(url, post'
496 496 return CommentForm;
497 497 });
498 498
499 /* selector for comment versions */
500 var initVersionSelector = function(selector, initialData) {
501
502 var formatResult = function(result, container, query, escapeMarkup) {
503
504 return renderTemplate('commentVersion', {
505 show_disabled: true,
506 version: result.comment_version,
507 user_name: result.comment_author_username,
508 gravatar_url: result.comment_author_gravatar,
509 size: 16,
510 timeago_component: result.comment_created_on,
511 })
512 };
513
514 $(selector).select2({
515 placeholder: "Edited",
516 containerCssClass: "drop-menu-comment-history",
517 dropdownCssClass: "drop-menu-dropdown",
518 dropdownAutoWidth: true,
519 minimumResultsForSearch: -1,
520 data: initialData,
521 formatResult: formatResult,
522 });
523
524 $(selector).on('select2-selecting', function (e) {
525 // hide the mast as we later do preventDefault()
526 $("#select2-drop-mask").click();
527 e.preventDefault();
528 e.choice.action();
529 });
530
531 $(selector).on("select2-open", function() {
532 timeagoActivate();
533 });
534 };
535
499 536 /* comments controller */
500 537 var CommentsController = function() {
501 538 var mainComment = '#text';
@@ -521,21 +558,8 b' var CommentsController = function() {'
521 558 return false;
522 559 };
523 560
524 this.showVersion = function (node) {
525 var $node = $(node);
526 var selectedIndex = $node.context.selectedIndex;
527 var option = $node.find('option[value="'+ selectedIndex +'"]');
528 var zero_option = $node.find('option[value="0"]');
529 if (!option){
530 return;
531 }
561 this.showVersion = function (comment_id, comment_history_id) {
532 562
533 // little trick to cheat onchange and allow to display the same version again
534 $node.context.selectedIndex = 0;
535 zero_option.text(selectedIndex);
536
537 var comment_history_id = option.attr('data-comment-history-id');
538 var comment_id = option.attr('data-comment-id');
539 563 var historyViewUrl = pyroutes.url(
540 564 'repo_commit_comment_history_view',
541 565 {
@@ -557,7 +581,8 b' var CommentsController = function() {'
557 581 });
558 582 };
559 583 _submitAjaxPOST(
560 historyViewUrl, {'csrf_token': CSRF_TOKEN}, successRenderCommit,
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
585 successRenderCommit,
561 586 failRenderCommit
562 587 );
563 588 };
@@ -863,6 +888,7 b' var CommentsController = function() {'
863 888
864 889 return commentForm;
865 890 };
891
866 892 this.editComment = function(node) {
867 893 var $node = $(node);
868 894 var $comment = $(node).closest('.comment');
@@ -917,15 +943,16 b' var CommentsController = function() {'
917 943 lineno: lineno,
918 944 f_path: f_path}
919 945 );
946
920 947 // set a CUSTOM submit handler for inline comments.
921 948 commentForm.setHandleFormSubmit(function(o) {
922 949 var text = commentForm.cm.getValue();
923 950 var commentType = commentForm.getCommentType();
924 var resolvesCommentId = commentForm.getResolvesId();
925 951
926 952 if (text === "") {
927 953 return;
928 954 }
955
929 956 if (old_comment_text == text) {
930 957 SwalNoAnimation.fire({
931 958 title: 'Unable to edit comment',
@@ -937,19 +964,22 b' var CommentsController = function() {'
937 964 var submitEvent = true;
938 965 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
939 966 commentForm.cm.setOption("readOnly", true);
940 var dropDown = $('#comment_history_for_comment_'+comment_id);
941 967
942 var version = dropDown.children().last().val()
968 // Read last version known
969 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
970 var version = versionSelector.data('lastVersion');
971
943 972 if(!version){
944 973 version = 0;
945 974 }
975
946 976 var postData = {
947 977 'text': text,
948 978 'f_path': f_path,
949 979 'line': lineno,
950 980 'comment_type': commentType,
951 'csrf_token': CSRF_TOKEN,
952 981 'version': version,
982 'csrf_token': CSRF_TOKEN
953 983 };
954 984
955 985 var submitSuccessCallback = function(json_data) {
@@ -961,30 +991,59 b' var CommentsController = function() {'
961 991 'csrf_token': CSRF_TOKEN
962 992 };
963 993
994 /* Inject new edited version selector */
964 995 var updateCommentVersionDropDown = function () {
965 var dropDown = $('#comment_history_for_comment_'+comment_id);
996 var versionSelectId = '#comment_versions_'+comment_id;
997 var preLoadVersionData = [
998 {
999 id: json_data['comment_version'],
1000 text: "v{0}".format(json_data['comment_version']),
1001 action: function () {
1002 Rhodecode.comments.showVersion(
1003 json_data['comment_id'],
1004 json_data['comment_history_id']
1005 )
1006 },
1007 comment_version: json_data['comment_version'],
1008 comment_author_username: json_data['comment_author_username'],
1009 comment_author_gravatar: json_data['comment_author_gravatar'],
1010 comment_created_on: json_data['comment_created_on'],
1011 },
1012 ]
1013
1014
1015 if ($(versionSelectId).data('select2')) {
1016 var oldData = $(versionSelectId).data('select2').opts.data.results;
1017 $(versionSelectId).select2("destroy");
1018 preLoadVersionData = oldData.concat(preLoadVersionData)
1019 }
1020
1021 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1022
966 1023 $comment.attr('data-comment-text', btoa(text));
967 var version = json_data['comment_version']
968 var option = new Option(version, version);
969 var $option = $(option);
970 $option.attr('data-comment-history-id', json_data['comment_history_id']);
971 $option.attr('data-comment-id', json_data['comment_id']);
972 dropDown.append(option);
973 dropDown.parent().show();
1024
1025 var versionSelector = $('#comment_versions_'+comment_id);
1026
1027 // set lastVersion so we know our last edit version
1028 versionSelector.data('lastVersion', json_data['comment_version'])
1029 versionSelector.parent().show();
974 1030 }
975 1031 updateCommentVersionDropDown();
1032
976 1033 // by default we reset state of comment preserving the text
977 1034 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
978 var prefix = "Error while editing of comment.\n"
1035 var prefix = "Error while editing this comment.\n"
979 1036 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
980 1037 ajaxErrorSwal(message);
1038 };
981 1039
982 };
983 1040 var successRenderCommit = function(o){
984 1041 $comment.show();
985 1042 $comment[0].lastElementChild.innerHTML = o;
986 }
987 var previewUrl = pyroutes.url('repo_commit_comment_preview',
1043 };
1044
1045 var previewUrl = pyroutes.url(
1046 'repo_commit_comment_preview',
988 1047 {'repo_name': templateContext.repo_name,
989 1048 'commit_id': templateContext.commit_data.commit_id});
990 1049
@@ -1000,11 +1059,6 b' var CommentsController = function() {'
1000 1059
1001 1060 $comments.find('.cb-comment-add-button').before(html);
1002 1061
1003 //mark visually which comment was resolved
1004 if (resolvesCommentId) {
1005 commentForm.markCommentResolved(resolvesCommentId);
1006 }
1007
1008 1062 // run global callback on submit
1009 1063 commentForm.globalSubmitSuccessCallback();
1010 1064
@@ -1029,16 +1083,25 b' var CommentsController = function() {'
1029 1083 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1030 1084 var prefix = "Error while editing comment.\n"
1031 1085 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1086 if (jqXHR.status == 409){
1087 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1088 ajaxErrorSwal(message, 'Comment version mismatch.');
1089 } else {
1032 1090 ajaxErrorSwal(message);
1091 }
1092
1033 1093 commentForm.resetCommentFormState(text)
1034 1094 };
1035 1095 commentForm.submitAjaxPOST(
1036 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1096 commentForm.submitUrl, postData,
1097 submitSuccessCallback,
1098 submitFailCallback);
1037 1099 });
1038 1100 }
1039 1101
1040 1102 $form.addClass('comment-inline-form-open');
1041 1103 };
1104
1042 1105 this.createComment = function(node, resolutionComment) {
1043 1106 var resolvesCommentId = resolutionComment || null;
1044 1107 var $node = $(node);
@@ -130,10 +130,13 b' function formatErrorMessage(jqXHR, textS'
130 130 }
131 131 }
132 132
133 function ajaxErrorSwal(message) {
133 function ajaxErrorSwal(message, title) {
134
135 var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
136
134 137 SwalNoAnimation.fire({
135 138 icon: 'error',
136 title: _gettext('Ajax Request Error'),
139 title: title,
137 140 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
138 141 showClass: {
139 142 popup: 'swal2-noanimation',
@@ -66,31 +66,47 b''
66 66 <div class="date">
67 67 ${h.age_component(comment.modified_at, time_is_local=True)}
68 68 </div>
69 <%
70 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
71 %>
72
69 73 % if comment.history:
70 74 <div class="date">
71 <span class="comment-area-text">${_('Edited')}:</span>
72 <select class="comment-version-select" id="comment_history_for_comment_${comment.comment_id}"
73 onchange="return Rhodecode.comments.showVersion(this)"
74 name="comment_type">
75 75
76 <option style="display: none" value="0">---</option>
76 <input id="${comment_version_selector}" name="${comment_version_selector}"
77 type="hidden"
78 data-last-version="${comment.history[-1].version}">
79
80 <script type="text/javascript">
81
82 var preLoadVersionData = [
77 83 % for comment_history in comment.history:
78 <option data-comment-history-id="${comment_history.comment_history_id}"
79 data-comment-id="${comment.comment_id}"
80 value="${comment_history.version}">
81 ${comment_history.version}
82 </option>
84 {
85 id: ${comment_history.comment_history_id},
86 text: 'v${comment_history.version}',
87 action: function () {
88 Rhodecode.comments.showVersion(
89 "${comment.comment_id}",
90 "${comment_history.comment_history_id}"
91 )
92 },
93 comment_version: "${comment_history.version}",
94 comment_author_username: "${comment_history.author.username}",
95 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
96 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
97 },
83 98 % endfor
84 </select>
99 ]
100 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
101
102 </script>
103
85 104 </div>
86 105 % else:
87 106 <div class="date" style="display: none">
88 <span class="comment-area-text">${_('Edited')}</span>
89 <select class="comment-version-select" id="comment_history_for_comment_${comment.comment_id}"
90 onchange="return Rhodecode.comments.showVersion(this)"
91 name="comment_type">
92 <option style="display: none" value="0">---</option>
93 </select>
107 <input id="${comment_version_selector}" name="${comment_version_selector}"
108 type="hidden"
109 data-last-version="0">
94 110 </div>
95 111 %endif
96 112 % if inline:
@@ -169,12 +185,8 b''
169 185 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
170 186 ## permissions to delete
171 187 %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):
172 %if comment.comment_type == 'note':
173 188 <a onclick="return Rhodecode.comments.editComment(this);"
174 189 class="edit-comment">${_('Edit')}</a>
175 %else:
176 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
177 %endif
178 190 | <a onclick="return Rhodecode.comments.deleteComment(this);"
179 191 class="delete-comment">${_('Delete')}</a>
180 192 %else:
@@ -130,6 +130,34 b' var CG = new ColorGenerator();'
130 130 </script>
131 131
132 132
133 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
134
135 <%
136 if (size > 16) {
137 var gravatar_class = 'gravatar gravatar-large';
138 } else {
139 var gravatar_class = 'gravatar';
140 }
141
142 %>
143
144 <%
145 if (show_disabled) {
146 var user_cls = 'user user-disabled';
147 } else {
148 var user_cls = 'user';
149 }
150
151 %>
152
153 <div style='line-height: 20px'>
154 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
155 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
156 </div>
157
158 </script>
159
160
133 161 </div>
134 162
135 163 <script>
General Comments 0
You need to be logged in to leave comments. Login now