##// END OF EJS Templates
pull-requests: added version browsing for pull requests....
marcink -
r1192:5c951e10 default
parent child Browse files
Show More
@@ -45,16 +45,17 b' from rhodecode.lib.auth import ('
45 45 from rhodecode.lib.channelstream import channelstream_request
46 46 from rhodecode.lib.compat import OrderedDict
47 47 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode, UnsafeAttributeDict)
49 50 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
50 51 from rhodecode.lib.vcs.exceptions import (
51 52 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 53 NodeDoesNotExistError)
53 from rhodecode.lib.diffs import LimitedDiffContainer
54
54 55 from rhodecode.model.changeset_status import ChangesetStatusModel
55 56 from rhodecode.model.comment import ChangesetCommentsModel
56 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
57 Repository
57 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
58 Repository, PullRequestVersion)
58 59 from rhodecode.model.forms import PullRequestForm
59 60 from rhodecode.model.meta import Session
60 61 from rhodecode.model.pull_request import PullRequestModel
@@ -675,46 +676,133 b' class PullrequestsController(BaseRepoCon'
675 676 return redirect(url('my_account_pullrequests'))
676 677 raise HTTPForbidden()
677 678
679 def _get_pr_version(self, pull_request_id, version=None):
680 pull_request_id = safe_int(pull_request_id)
681 at_version = None
682 if version:
683 pull_request_ver = PullRequestVersion.get_or_404(version)
684 pull_request_obj = pull_request_ver
685 _org_pull_request_obj = pull_request_ver.pull_request
686 at_version = pull_request_ver.pull_request_version_id
687 else:
688 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
689
690 class PullRequestDisplay(object):
691 """
692 Special object wrapper for showing PullRequest data via Versions
693 It mimics PR object as close as possible. This is read only object
694 just for display
695 """
696 def __init__(self, attrs):
697 self.attrs = attrs
698 # internal have priority over the given ones via attrs
699 self.internal = ['versions']
700
701 def __getattr__(self, item):
702 if item in self.internal:
703 return getattr(self, item)
704 try:
705 return self.attrs[item]
706 except KeyError:
707 raise AttributeError(
708 '%s object has no attribute %s' % (self, item))
709
710 def versions(self):
711 return pull_request_obj.versions.order_by(
712 PullRequestVersion.pull_request_version_id).all()
713
714 def is_closed(self):
715 return pull_request_obj.is_closed()
716
717 attrs = UnsafeAttributeDict(pull_request_obj.get_api_data())
718
719 attrs.author = UnsafeAttributeDict(
720 pull_request_obj.author.get_api_data())
721 if pull_request_obj.target_repo:
722 attrs.target_repo = UnsafeAttributeDict(
723 pull_request_obj.target_repo.get_api_data())
724 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
725
726 if pull_request_obj.source_repo:
727 attrs.source_repo = UnsafeAttributeDict(
728 pull_request_obj.source_repo.get_api_data())
729 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
730
731 attrs.source_ref_parts = pull_request_obj.source_ref_parts
732 attrs.target_ref_parts = pull_request_obj.target_ref_parts
733
734 attrs.shadow_merge_ref = _org_pull_request_obj.shadow_merge_ref
735
736 pull_request_ver = PullRequestDisplay(attrs)
737
738 return _org_pull_request_obj, pull_request_obj, \
739 pull_request_ver, at_version
740
678 741 @LoginRequired()
679 742 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
680 743 'repository.admin')
681 744 def show(self, repo_name, pull_request_id):
682 745 pull_request_id = safe_int(pull_request_id)
683 c.pull_request = PullRequest.get_or_404(pull_request_id)
746
747 version = request.GET.get('version')
748 pull_request_latest, \
749 pull_request, \
750 pull_request_ver, \
751 at_version = self._get_pr_version(pull_request_id, version=version)
684 752
685 753 c.template_context['pull_request_data']['pull_request_id'] = \
686 754 pull_request_id
687 755
688 756 # pull_requests repo_name we opened it against
689 757 # ie. target_repo must match
690 if repo_name != c.pull_request.target_repo.repo_name:
758 if repo_name != pull_request.target_repo.repo_name:
691 759 raise HTTPNotFound
692 760
761 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
762 pull_request)
763
764 if at_version:
765 c.allowed_to_change_status = False
766 else:
693 767 c.allowed_to_change_status = PullRequestModel(). \
694 check_user_change_status(c.pull_request, c.rhodecode_user)
768 check_user_change_status(pull_request, c.rhodecode_user)
769
770 if at_version:
771 c.allowed_to_update = False
772 else:
695 773 c.allowed_to_update = PullRequestModel().check_user_update(
696 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
774 pull_request, c.rhodecode_user) and not pull_request.is_closed()
775
776 if at_version:
777 c.allowed_to_merge = False
778 else:
697 779 c.allowed_to_merge = PullRequestModel().check_user_merge(
698 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
699 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
700 c.pull_request)
780 pull_request, c.rhodecode_user) and not pull_request.is_closed()
781
782 if at_version:
783 c.allowed_to_delete = False
784 else:
701 785 c.allowed_to_delete = PullRequestModel().check_user_delete(
702 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
786 pull_request, c.rhodecode_user) and not pull_request.is_closed()
787
788 if at_version:
789 c.allowed_to_comment = False
790 else:
791 c.allowed_to_comment = not pull_request.is_closed()
703 792
704 793 cc_model = ChangesetCommentsModel()
705 794
706 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
795 c.pull_request_reviewers = pull_request.reviewers_statuses()
707 796
708 c.pull_request_review_status = c.pull_request.calculated_review_status()
797 c.pull_request_review_status = pull_request.calculated_review_status()
709 798 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
710 c.pull_request)
799 pull_request)
711 800 c.approval_msg = None
712 801 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
713 802 c.approval_msg = _('Reviewer approval is pending.')
714 803 c.pr_merge_status = False
715 804 # load compare data into template context
716 enable_comments = not c.pull_request.is_closed()
717
805 enable_comments = not pull_request.is_closed()
718 806
719 807 # inline comments
720 808 c.inline_comments = cc_model.get_inline_comments(
@@ -725,23 +813,26 b' class PullrequestsController(BaseRepoCon'
725 813 c.inline_comments, version=at_version)
726 814
727 815 self._load_compare_data(
728 c.pull_request, c.inline_comments, enable_comments=enable_comments)
816 pull_request, c.inline_comments, enable_comments=enable_comments)
729 817
730 818 # outdated comments
731 819 c.outdated_comments = {}
732 820 c.outdated_cnt = 0
733 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
821
822 if ChangesetCommentsModel.use_outdated_comments(pull_request):
734 823 c.outdated_comments = cc_model.get_outdated_comments(
735 824 c.rhodecode_db_repo.repo_id,
736 pull_request=c.pull_request)
825 pull_request=pull_request)
826
737 827 # Count outdated comments and check for deleted files
738 828 for file_name, lines in c.outdated_comments.iteritems():
739 829 for comments in lines.values():
830 comments = [comm for comm in comments
831 if comm.outdated_at_version(at_version)]
740 832 c.outdated_cnt += len(comments)
741 833 if file_name not in c.included_files:
742 834 c.deleted_files.append(file_name)
743 835
744
745 836 # this is a hack to properly display links, when creating PR, the
746 837 # compare view and others uses different notation, and
747 838 # compare_commits.html renders links based on the target_repo.
@@ -760,6 +851,9 b' class PullrequestsController(BaseRepoCon'
760 851 c.commit_statuses = statuses
761 852
762 853 c.ancestor = None # TODO: add ancestor here
854 c.pull_request = pull_request_ver
855 c.pull_request_latest = pull_request_latest
856 c.at_version = at_version
763 857
764 858 return render('/pullrequests/pullrequest_show.html')
765 859
@@ -813,8 +907,6 b' class PullrequestsController(BaseRepoCon'
813 907 closing_pr=close_pr
814 908 )
815 909
816
817
818 910 if allowed_to_change_status:
819 911 old_calculated_status = pull_request.calculated_review_status()
820 912 # get status if set !
@@ -656,6 +656,16 b' def extract_mentioned_users(s):'
656 656 return sorted(list(usrs), key=lambda k: k.lower())
657 657
658 658
659 class UnsafeAttributeDict(dict):
660 def __getattr__(self, attr):
661 try:
662 return self[attr]
663 except KeyError:
664 raise AttributeError('%s object has no attribute %s' % (self, attr))
665 __setattr__ = dict.__setitem__
666 __delattr__ = dict.__delitem__
667
668
659 669 class AttributeDict(dict):
660 670 def __getattr__(self, attr):
661 671 return self.get(attr, None)
@@ -90,7 +90,7 b' class BaseModel(object):'
90 90 """
91 91 Gets instance of given cls using some simple lookup mechanism.
92 92
93 :param cls: class to fetch
93 :param cls: classes to fetch
94 94 :param instance: int or Instance
95 95 :param callback: callback to call if all lookups failed
96 96 """
@@ -98,6 +98,9 b' class BaseModel(object):'
98 98 if isinstance(instance, cls):
99 99 return instance
100 100 elif isinstance(instance, (int, long)):
101 if isinstance(cls, tuple):
102 # if we pass multi instances we pick first to .get()
103 cls = cls[0]
101 104 return cls.get(instance)
102 105 else:
103 106 if instance:
@@ -2933,6 +2933,12 b' class ChangesetComment(Base, BaseModel):'
2933 2933 def outdated(self):
2934 2934 return self.display_state == self.COMMENT_OUTDATED
2935 2935
2936 def outdated_at_version(self, version):
2937 """
2938 Checks if comment is outdated for given pull request version
2939 """
2940 return self.outdated and self.pull_request_version_id != version
2941
2936 2942 def render(self, mentions=False):
2937 2943 from rhodecode.lib import helpers as h
2938 2944 return h.render(self.text, renderer=self.renderer, mentions=mentions)
@@ -3117,34 +3123,6 b' class _PullRequestBase(BaseModel):'
3117 3123 else:
3118 3124 return None
3119 3125
3120
3121 class PullRequest(Base, _PullRequestBase):
3122 __tablename__ = 'pull_requests'
3123 __table_args__ = (
3124 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3125 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3126 )
3127
3128 pull_request_id = Column(
3129 'pull_request_id', Integer(), nullable=False, primary_key=True)
3130
3131 def __repr__(self):
3132 if self.pull_request_id:
3133 return '<DB:PullRequest #%s>' % self.pull_request_id
3134 else:
3135 return '<DB:PullRequest at %#x>' % id(self)
3136
3137 reviewers = relationship('PullRequestReviewers',
3138 cascade="all, delete, delete-orphan")
3139 statuses = relationship('ChangesetStatus')
3140 comments = relationship('ChangesetComment',
3141 cascade="all, delete, delete-orphan")
3142 versions = relationship('PullRequestVersion',
3143 cascade="all, delete, delete-orphan")
3144
3145 def is_closed(self):
3146 return self.status == self.STATUS_CLOSED
3147
3148 3126 def get_api_data(self):
3149 3127 from rhodecode.model.pull_request import PullRequestModel
3150 3128 pull_request = self
@@ -3209,6 +3187,35 b' class PullRequest(Base, _PullRequestBase'
3209 3187
3210 3188 return data
3211 3189
3190
3191 class PullRequest(Base, _PullRequestBase):
3192 __tablename__ = 'pull_requests'
3193 __table_args__ = (
3194 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3195 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3196 )
3197
3198 pull_request_id = Column(
3199 'pull_request_id', Integer(), nullable=False, primary_key=True)
3200
3201 def __repr__(self):
3202 if self.pull_request_id:
3203 return '<DB:PullRequest #%s>' % self.pull_request_id
3204 else:
3205 return '<DB:PullRequest at %#x>' % id(self)
3206
3207 reviewers = relationship('PullRequestReviewers',
3208 cascade="all, delete, delete-orphan")
3209 statuses = relationship('ChangesetStatus')
3210 comments = relationship('ChangesetComment',
3211 cascade="all, delete, delete-orphan")
3212 versions = relationship('PullRequestVersion',
3213 cascade="all, delete, delete-orphan",
3214 lazy='dynamic')
3215
3216 def is_closed(self):
3217 return self.status == self.STATUS_CLOSED
3218
3212 3219 def __json__(self):
3213 3220 return {
3214 3221 'revisions': self.revisions,
@@ -3243,6 +3250,24 b' class PullRequestVersion(Base, _PullRequ'
3243 3250 else:
3244 3251 return '<DB:PullRequestVersion at %#x>' % id(self)
3245 3252
3253 @property
3254 def reviewers(self):
3255 return self.pull_request.reviewers
3256
3257 @property
3258 def versions(self):
3259 return self.pull_request.versions
3260
3261 def is_closed(self):
3262 # calculate from original
3263 return self.pull_request.status == self.STATUS_CLOSED
3264
3265 def calculated_review_status(self):
3266 return self.pull_request.calculated_review_status()
3267
3268 def reviewers_statuses(self):
3269 return self.pull_request.reviewers_statuses()
3270
3246 3271
3247 3272 class PullRequestReviewers(Base, BaseModel):
3248 3273 __tablename__ = 'pull_request_reviewers'
@@ -208,8 +208,14 b' input[type="button"] {'
208 208 color: @rcdarkblue;
209 209 }
210 210
211 //disabled buttons
212 //last; overrides any other styles
211 213 &:disabled {
214 opacity: .7;
215 cursor: auto;
216 background-color: white;
212 217 color: @grey4;
218 text-shadow: none;
213 219 }
214 220
215 221 // TODO: johbo: Check if we can avoid this, indicates that the structure
@@ -313,7 +313,7 b' table.code-difftable {'
313 313 // Comments
314 314
315 315 div.comment:target {
316 border-left: 6px solid @comment-highlight-color;
316 border-left: 6px solid @comment-highlight-color !important;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
@@ -737,6 +737,15 b' input.filediff-collapse-state {'
737 737 }
738 738 }
739 739
740 /* Main comments*/
741 #comments {
742 .comment-selected {
743 border-left: 6px solid @comment-highlight-color;
744 padding-left: 3px;
745 margin-left: -9px;
746 }
747 }
748
740 749 .filediff {
741 750 border: 1px solid @grey5;
742 751
@@ -894,6 +903,7 b' input.filediff-collapse-state {'
894 903 display: none;
895 904 }
896 905 }
906
897 907 .inline-comments {
898 908 border-radius: @border-radius;
899 909 background: @grey6;
@@ -904,6 +914,7 b' input.filediff-collapse-state {'
904 914 .comment-outdated {
905 915 opacity: 0.5;
906 916 }
917
907 918 .comment-inline {
908 919 background: white;
909 920 padding: (@comment-padding + 3px) @comment-padding;
@@ -170,22 +170,6 b' tr.inline-comments div {'
170 170 color: @rcblue;
171 171 }
172 172
173 .outdated {
174 display: none;
175 opacity: 0.6;
176
177 .comment {
178 margin: 0 0 @padding;
179
180 .date:after {
181 content: none;
182 }
183 }
184 .outdated_comment_block {
185 padding: 0 0 @space 0;
186 }
187 }
188
189 173 // Comment Form
190 174 div.comment-form {
191 175 margin-top: 20px;
@@ -1395,9 +1395,7 b' table.integrations {'
1395 1395 width: 92%;
1396 1396 margin-bottom: 1em;
1397 1397 }
1398 #update_commits {
1399 float: right;
1400 }
1398
1401 1399 .compare_view_commits tr{
1402 1400 height: 20px;
1403 1401 }
@@ -48,58 +48,6 b' var tableTr = function(cls, body){'
48 48 return _el.children[0].children[0].children[0];
49 49 };
50 50
51 var removeInlineForm = function(form) {
52 form.parentNode.removeChild(form);
53 };
54
55 var createInlineForm = function(parent_tr, f_path, line) {
56 var tmpl = $('#comment-inline-form-template').html();
57 tmpl = tmpl.format(f_path, line);
58 var form = tableTr('comment-form-inline', tmpl);
59 var form_hide_button = $(form).find('.hide-inline-form');
60
61 $(form_hide_button).click(function(e) {
62 $('.inline-comments').removeClass('hide-comment-button');
63 var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
64 if ($(newtr.nextElementSibling).hasClass('inline-comments-button')) {
65 $(newtr.nextElementSibling).show();
66 }
67 $(newtr).parents('.comment-form-inline').remove();
68 $(parent_tr).removeClass('form-open');
69 $(parent_tr).removeClass('hl-comment');
70 });
71
72 return form;
73 };
74
75 var getLineNo = function(tr) {
76 var line;
77 // Try to get the id and return "" (empty string) if it doesn't exist
78 var o = ($(tr).find('.lineno.old').attr('id')||"").split('_');
79 var n = ($(tr).find('.lineno.new').attr('id')||"").split('_');
80 if (n.length >= 2) {
81 line = n[n.length-1];
82 } else if (o.length >= 2) {
83 line = o[o.length-1];
84 }
85 return line;
86 };
87
88 /**
89 * make a single inline comment and place it inside
90 */
91 var renderInlineComment = function(json_data, show_add_button) {
92 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
93 try {
94 var html = json_data.rendered_text;
95 var lineno = json_data.line_no;
96 var target_id = json_data.target_id;
97 placeInline(target_id, lineno, html, show_add_button);
98 } catch (e) {
99 console.error(e);
100 }
101 };
102
103 51 function bindDeleteCommentButtons() {
104 52 $('.delete-comment').one('click', function() {
105 53 var comment_id = $(this).data("comment-id");
@@ -110,115 +58,6 b' function bindDeleteCommentButtons() {'
110 58 });
111 59 }
112 60
113 /**
114 * Inject inline comment for on given TR this tr should be always an .line
115 * tr containing the line. Code will detect comment, and always put the comment
116 * block at the very bottom
117 */
118 var injectInlineForm = function(tr){
119 if (!$(tr).hasClass('line')) {
120 return;
121 }
122
123 var _td = $(tr).find('.code').get(0);
124 if ($(tr).hasClass('form-open') ||
125 $(tr).hasClass('context') ||
126 $(_td).hasClass('no-comment')) {
127 return;
128 }
129 $(tr).addClass('form-open');
130 $(tr).addClass('hl-comment');
131 var node = $(tr.parentNode.parentNode.parentNode).find('.full_f_path').get(0);
132 var f_path = $(node).attr('path');
133 var lineno = getLineNo(tr);
134 var form = createInlineForm(tr, f_path, lineno);
135
136 var parent = tr;
137 while (1) {
138 var n = parent.nextElementSibling;
139 // next element are comments !
140 if ($(n).hasClass('inline-comments')) {
141 parent = n;
142 }
143 else {
144 break;
145 }
146 }
147 var _parent = $(parent).get(0);
148 $(_parent).after(form);
149 $('.comment-form-inline').prev('.inline-comments').addClass('hide-comment-button');
150 var f = $(form).get(0);
151
152 var _form = $(f).find('.inline-form').get(0);
153
154 var pullRequestId = templateContext.pull_request_data.pull_request_id;
155 var commitId = templateContext.commit_data.commit_id;
156
157 var commentForm = new CommentForm(_form, commitId, pullRequestId, lineno, false);
158 var cm = commentForm.getCmInstance();
159
160 // set a CUSTOM submit handler for inline comments.
161 commentForm.setHandleFormSubmit(function(o) {
162 var text = commentForm.cm.getValue();
163
164 if (text === "") {
165 return;
166 }
167
168 if (lineno === undefined) {
169 alert('missing line !');
170 return;
171 }
172 if (f_path === undefined) {
173 alert('missing file path !');
174 return;
175 }
176
177 var excludeCancelBtn = false;
178 var submitEvent = true;
179 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
180 commentForm.cm.setOption("readOnly", true);
181 var postData = {
182 'text': text,
183 'f_path': f_path,
184 'line': lineno,
185 'csrf_token': CSRF_TOKEN
186 };
187 var submitSuccessCallback = function(o) {
188 $(tr).removeClass('form-open');
189 removeInlineForm(f);
190 renderInlineComment(o);
191 $('.inline-comments').removeClass('hide-comment-button');
192
193 // re trigger the linkification of next/prev navigation
194 linkifyComments($('.inline-comment-injected'));
195 timeagoActivate();
196 bindDeleteCommentButtons();
197 commentForm.setActionButtonsDisabled(false);
198
199 };
200 var submitFailCallback = function(){
201 commentForm.resetCommentFormState(text)
202 };
203 commentForm.submitAjaxPOST(
204 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
205 });
206
207 setTimeout(function() {
208 // callbacks
209 if (cm !== undefined) {
210 cm.focus();
211 }
212 }, 10);
213
214 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
215 form:_form,
216 parent:_parent,
217 lineno: lineno,
218 f_path: f_path}
219 );
220 };
221
222 61 var deleteComment = function(comment_id) {
223 62 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
224 63 var postData = {
@@ -232,89 +71,6 b' var deleteComment = function(comment_id)'
232 71 ajaxPOST(url, postData, success);
233 72 };
234 73
235 var createInlineAddButton = function(tr){
236 var label = _gettext('Add another comment');
237 var html_el = document.createElement('div');
238 $(html_el).addClass('add-comment');
239 html_el.innerHTML = '<span class="btn btn-secondary">{0}</span>'.format(label);
240 var add = new $(html_el);
241 add.on('click', function(e) {
242 injectInlineForm(tr);
243 });
244 return add;
245 };
246
247 var placeAddButton = function(target_tr){
248 if(!target_tr){
249 return;
250 }
251 var last_node = target_tr;
252 // scan
253 while (1){
254 var n = last_node.nextElementSibling;
255 // next element are comments !
256 if($(n).hasClass('inline-comments')){
257 last_node = n;
258 // also remove the comment button from previous
259 var comment_add_buttons = $(last_node).find('.add-comment');
260 for(var i=0; i<comment_add_buttons.length; i++){
261 var b = comment_add_buttons[i];
262 b.parentNode.removeChild(b);
263 }
264 }
265 else{
266 break;
267 }
268 }
269 var add = createInlineAddButton(target_tr);
270 // get the comment div
271 var comment_block = $(last_node).find('.comment')[0];
272 // attach add button
273 $(add).insertAfter(comment_block);
274 };
275
276 /**
277 * Places the inline comment into the changeset block in proper line position
278 */
279 var placeInline = function(target_container, lineno, html, show_add_button) {
280 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
281
282 var lineid = "{0}_{1}".format(target_container, lineno);
283 var target_line = $('#' + lineid).get(0);
284 var comment = new $(tableTr('inline-comments', html));
285 // check if there are comments already !
286 if (target_line) {
287 var parent_node = target_line.parentNode;
288 var root_parent = parent_node;
289
290 while (1) {
291 var n = parent_node.nextElementSibling;
292 // next element are comments !
293 if ($(n).hasClass('inline-comments')) {
294 parent_node = n;
295 }
296 else {
297 break;
298 }
299 }
300 // put in the comment at the bottom
301 $(comment).insertAfter(parent_node);
302 $(comment).find('.comment-inline').addClass('inline-comment-injected');
303 // scan nodes, and attach add button to last one
304 if (show_add_button) {
305 placeAddButton(root_parent);
306 }
307 addCommentToggle(target_line);
308 }
309
310 return target_line;
311 };
312
313 var addCommentToggle = function(target_line) {
314 // exposes comment toggle button
315 $(target_line).siblings('.comment-toggle').addClass('active');
316 return;
317 };
318 74
319 75 var bindToggleButtons = function() {
320 76 $('.comment-toggle').on('click', function() {
@@ -348,37 +104,6 b' var linkifyComments = function(comments)'
348 104
349 105 };
350 106
351 /**
352 * Iterates over all the inlines, and places them inside proper blocks of data
353 */
354 var renderInlineComments = function(file_comments, show_add_button) {
355 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
356
357 for (var i = 0; i < file_comments.length; i++) {
358 var box = file_comments[i];
359
360 var target_id = $(box).attr('target_id');
361
362 // actually comments with line numbers
363 var comments = box.children;
364
365 for (var j = 0; j < comments.length; j++) {
366 var data = {
367 'rendered_text': comments[j].outerHTML,
368 'line_no': $(comments[j]).attr('line'),
369 'target_id': target_id
370 };
371 renderInlineComment(data, show_add_button);
372 }
373 }
374
375 // since order of injection is random, we're now re-iterating
376 // from correct order and filling in links
377 linkifyComments($('.inline-comment-injected'));
378 bindDeleteCommentButtons();
379 firefoxAnchorFix();
380 };
381
382 107
383 108 /* Comment form for main and inline comments */
384 109 var CommentForm = (function() {
@@ -679,11 +404,13 b' var CommentsController = function() { /*'
679 404 var $td = $node.closest('td');
680 405 $node.closest('.comment-inline-form').removeClass('comment-inline-form-open');
681 406 return false;
682 }
407 };
408
683 409 this.getLineNumber = function(node) {
684 410 var $node = $(node);
685 411 return $node.closest('td').attr('data-line-number');
686 }
412 };
413
687 414 this.scrollToComment = function(node, offset) {
688 415 if (!node) {
689 416 node = $('.comment-selected');
@@ -702,20 +429,23 b' var CommentsController = function() { /*'
702 429 }
703 430 var $next = $('.comment-current').eq(nextIdx);
704 431 var $cb = $next.closest('.cb');
705 $cb.removeClass('cb-collapsed')
432 $cb.removeClass('cb-collapsed');
706 433
707 434 var $filediffCollapseState = $cb.closest('.filediff').prev();
708 435 $filediffCollapseState.prop('checked', false);
709 436 $next.addClass('comment-selected');
710 437 scrollToElement($next);
711 438 return false;
712 }
439 };
440
713 441 this.nextComment = function(node) {
714 442 return self.scrollToComment(node, 1);
715 }
443 };
444
716 445 this.prevComment = function(node) {
717 446 return self.scrollToComment(node, -1);
718 }
447 };
448
719 449 this.deleteComment = function(node) {
720 450 if (!confirm(_gettext('Delete this comment?'))) {
721 451 return false;
@@ -744,7 +474,8 b' var CommentsController = function() { /*'
744 474 return false;
745 475 };
746 476 ajaxPOST(url, postData, success, failure);
747 }
477 };
478
748 479 this.toggleComments = function(node, show) {
749 480 var $filediff = $(node).closest('.filediff');
750 481 if (show === true) {
@@ -757,12 +488,14 b' var CommentsController = function() { /*'
757 488 $filediff.toggleClass('hide-comments');
758 489 }
759 490 return false;
760 }
491 };
492
761 493 this.toggleLineComments = function(node) {
762 494 self.toggleComments(node, true);
763 495 var $node = $(node);
764 496 $node.closest('tr').toggleClass('hide-line-comments');
765 }
497 };
498
766 499 this.createComment = function(node) {
767 500 var $node = $(node);
768 501 var $td = $node.closest('td');
@@ -832,7 +565,6 b' var CommentsController = function() { /*'
832 565 console.error(e);
833 566 }
834 567
835
836 568 // re trigger the linkification of next/prev navigation
837 569 linkifyComments($('.inline-comment-injected'));
838 570 timeagoActivate();
@@ -863,7 +595,7 b' var CommentsController = function() { /*'
863 595 }
864 596
865 597 $form.addClass('comment-inline-form-open');
866 }
598 };
867 599
868 600 this.renderInlineComments = function(file_comments) {
869 601 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
@@ -892,4 +624,4 b' var CommentsController = function() { /*'
892 624 firefoxAnchorFix();
893 625 };
894 626
895 } No newline at end of file
627 }; No newline at end of file
@@ -178,9 +178,8 b''
178 178
179 179 ## template for inline comment form
180 180 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
181 ${comment.comment_inline_form()}
182 181
183 ## ## render comments and inlines
182 ## render comments
184 183 ${comment.generate_comments()}
185 184
186 185 ## main comment form and it status
@@ -210,12 +209,12 b''
210 209 if(button.hasClass("comments-visible")) {
211 210 $('#{0} .inline-comments'.format(boxid)).each(function(index){
212 211 $(this).hide();
213 })
212 });
214 213 button.removeClass("comments-visible");
215 214 } else {
216 215 $('#{0} .inline-comments'.format(boxid)).each(function(index){
217 216 $(this).show();
218 })
217 });
219 218 button.addClass("comments-visible");
220 219 }
221 220 });
@@ -230,7 +229,7 b''
230 229 url: '${h.url('changeset_children',repo_name=c.repo_name, revision=c.commit.raw_id)}',
231 230 success: function(data) {
232 231 if(data.results.length === 0){
233 $('#child_link').html('${_('No Child Commits')}').addClass('disabled');
232 $('#child_link').html("${_('No Child Commits')}").addClass('disabled');
234 233 }
235 234 if(data.results.length === 1){
236 235 var commit = data.results[0];
@@ -263,7 +262,7 b''
263 262 // >1 links show them to user to choose
264 263 if(!$('#parent_link').hasClass('disabled')){
265 264 $.ajax({
266 url: '${h.url('changeset_parents',repo_name=c.repo_name, revision=c.commit.raw_id)}',
265 url: '${h.url("changeset_parents",repo_name=c.repo_name, revision=c.commit.raw_id)}',
267 266 success: function(data) {
268 267 if(data.results.length === 0){
269 268 $('#parent_link').html('${_('No Parent Commits')}').addClass('disabled');
@@ -6,14 +6,14 b''
6 6 <%namespace name="base" file="/base/base.html"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 <div
10 class="comment
9 <div class="comment
11 10 ${'comment-inline' if inline else ''}
12 ${'comment-outdated' if comment.outdated else 'comment-current'}"
13 "
11 ${'comment-outdated' if comment.outdated_at_version(getattr(c, 'at_version', None)) else 'comment-current'}"
14 12 id="comment-${comment.comment_id}"
15 13 line="${comment.line_no}"
16 data-comment-id="${comment.comment_id}">
14 data-comment-id="${comment.comment_id}"
15 style="${'display: none;' if comment.outdated_at_version(getattr(c, 'at_version', None)) else ''}">
16
17 17 <div class="meta">
18 18 <div class="author">
19 19 ${base.gravatar_with_user(comment.author.email, 16)}
@@ -23,6 +23,11 b''
23 23 </div>
24 24 <div class="status-change">
25 25 %if comment.pull_request:
26 % if comment.outdated:
27 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
28 ${_('Outdated comment from pull request version {}').format(comment.pull_request_version_id)}
29 </a>
30 % else:
26 31 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
27 32 %if comment.status_change:
28 33 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
@@ -30,6 +35,7 b''
30 35 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
31 36 %endif
32 37 </a>
38 % endif
33 39 %else:
34 40 %if comment.status_change:
35 41 ${_('Status change on commit')}:
@@ -52,14 +58,19 b''
52 58 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
53 59 ## only super-admin, repo admin OR comment owner can delete
54 60 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
61 ## permissions to delete
55 62 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
56 63 ## TODO: dan: add edit comment here
57 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a> |
58 %if not comment.outdated:
59 <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a> |
60 <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
64 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
65 %else:
66 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
61 67 %endif
68 %else:
69 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
62 70 %endif
71 %if not comment.outdated_at_version(getattr(c, 'at_version', None)):
72 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
73 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
63 74 %endif
64 75
65 76 </div>
@@ -67,102 +78,9 b''
67 78 <div class="text">
68 79 ${comment.render(mentions=True)|n}
69 80 </div>
70 </div>
71 </%def>
72 81
73 <%def name="comment_block_outdated(comment)">
74 <div class="comments" id="comment-${comment.comment_id}">
75 <div class="comment comment-wrapp">
76 <div class="meta">
77 <div class="author">
78 ${base.gravatar_with_user(comment.author.email, 16)}
79 </div>
80 <div class="date">
81 ${h.age_component(comment.modified_at, time_is_local=True)}
82 </div>
83 %if comment.status_change:
84 <span class="changeset-status-container">
85 <span class="changeset-status-ico">
86 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
87 </span>
88 <span title="${_('Commit status')}" class="changeset-status-lbl"> ${comment.status_change[0].status_lbl}</span>
89 </span>
90 %endif
91 <a class="permalink" href="#comment-${comment.comment_id}">&para;</a>
92 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
93 ## only super-admin, repo admin OR comment owner can delete
94 %if not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed()):
95 <div class="comment-links-block">
96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
97 <div data-comment-id=${comment.comment_id} class="delete-comment">${_('Delete')}</div>
98 %endif
99 </div>
100 %endif
101 </div>
102 <div class="text">
103 ${comment.render(mentions=True)|n}
104 </div>
105 </div>
106 82 </div>
107 83 </%def>
108
109 <%def name="comment_inline_form()">
110 <div id="comment-inline-form-template" style="display: none;">
111 <div class="comment-inline-form ac">
112 %if c.rhodecode_user.username != h.DEFAULT_USER:
113 ${h.form('#', class_='inline-form', method='get')}
114 <div id="edit-container_{1}" class="clearfix">
115 <div class="comment-title pull-left">
116 ${_('Create a comment on line {1}.')}
117 </div>
118 <div class="comment-help pull-right">
119 ${(_('Comments parsed using %s syntax with %s support.') % (
120 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
121 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
122 )
123 )|n
124 }
125 </div>
126 <div style="clear: both"></div>
127 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
128 </div>
129 <div id="preview-container_{1}" class="clearfix" style="display: none;">
130 <div class="comment-help">
131 ${_('Comment preview')}
132 </div>
133 <div id="preview-box_{1}" class="preview-box"></div>
134 </div>
135 <div class="comment-footer">
136 <div class="comment-button hide-inline-form-button cancel-button">
137 ${h.reset('hide-inline-form', _('Cancel'), class_='btn hide-inline-form', id_="cancel-btn_{1}")}
138 </div>
139 <div class="action-buttons">
140 <input type="hidden" name="f_path" value="{0}">
141 <input type="hidden" name="line" value="{1}">
142 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
143 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
144 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
145 </div>
146 ${h.end_form()}
147 </div>
148 %else:
149 ${h.form('', class_='inline-form comment-form-login', method='get')}
150 <div class="pull-left">
151 <div class="comment-help pull-right">
152 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
153 </div>
154 </div>
155 <div class="comment-button pull-right">
156 ${h.reset('hide-inline-form', _('Hide'), class_='btn hide-inline-form')}
157 </div>
158 <div class="clearfix"></div>
159 ${h.end_form()}
160 %endif
161 </div>
162 </div>
163 </%def>
164
165
166 84 ## generate main comments
167 85 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
168 86 <div id="comments">
@@ -182,6 +100,7 b''
182 100
183 101 ## MAIN COMMENT FORM
184 102 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
103
185 104 %if is_compare:
186 105 <% form_id = "comments_form_compare" %>
187 106 %else:
@@ -394,10 +394,13 b' from rhodecode.lib.diffs import NEW_FILE'
394 394 %for comment in comments:
395 395 ${commentblock.comment_block(comment, inline=True)}
396 396 %endfor
397
397 398 <span onclick="return Rhodecode.comments.createComment(this)"
398 class="btn btn-secondary cb-comment-add-button">
399 class="btn btn-secondary cb-comment-add-button ${'comment-outdated' if comments and comments[-1].outdated else ''}"
400 style="${'display: none;' if comments and comments[-1].outdated else ''}">
399 401 ${_('Add another comment')}
400 402 </span>
403
401 404 </div>
402 405 </%def>
403 406
@@ -918,9 +918,6 b''
918 918 $(btns).each(fn_display);
919 919 });
920 920
921 // inject comments into they proper positions
922 var file_comments = $('.inline-comment-placeholder');
923 renderInlineComments(file_comments);
924 921 var commentTotals = {};
925 922 $.each(file_comments, function(i, comment) {
926 923 var path = $(comment).attr('path');
@@ -165,24 +165,62 b''
165 165 <div>
166 166 <div class="comments-number">
167 167 %if c.comments:
168 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
168 <a href="#comments">${ungettext("%d General Comment", "%d General Comments", len(c.comments)) % len(c.comments)}</a>,
169 169 %else:
170 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
170 ${ungettext("%d General Comment", "%d General Comments", len(c.comments)) % len(c.comments)}
171 171 %endif
172
172 173 %if c.inline_cnt:
173 174 <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
174 175 %else:
175 176 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
176 177 %endif
177 178
178
179 179 % if c.outdated_cnt:
180 180 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
181 181 % endif
182 182 </div>
183 183 </div>
184 184 </div>
185
185 186 </div>
187
188 <div class="field">
189 <div class="label-summary">
190 <label>${_('Versions')}:</label>
191 </div>
192 <div>
193 <table>
194 <tr>
195 <td>
196 % if c.at_version == None:
197 <i class="icon-ok link"></i>
198 % endif
199 </td>
200 <td><code><a href="${h.url.current()}">latest</a></code></td>
201 <td>
202 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
203 </td>
204 <td>${_('created')} ${h.age_component(c.pull_request.created_on)}</td>
205 </tr>
206 % for ver in reversed(c.pull_request.versions()):
207 <tr>
208 <td>
209 % if c.at_version == ver.pull_request_version_id:
210 <i class="icon-ok link"></i>
211 % endif
212 </td>
213 <td><code><a href="${h.url.current(version=ver.pull_request_version_id)}">version ${ver.pull_request_version_id}</a></code></td>
214 <td>
215 <code>${ver.source_ref_parts.commit_id[:6]}</code>
216 </td>
217 <td>${_('created')} ${h.age_component(ver.created_on)}</td>
218 </tr>
219 % endfor
220 </table>
221 </div>
222 </div>
223
186 224 <div id="pr-save" class="field" style="display: none;">
187 225 <div class="label-summary"></div>
188 226 <div class="input">
@@ -291,7 +329,9 b''
291 329 % endif
292 330 <div class="compare_view_commits_title">
293 331 % if c.allowed_to_update and not c.pull_request.is_closed():
294 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
332 <button id="update_commits" class="btn pull-right">${_('Update commits')}</button>
333 % else:
334 <button class="btn disabled pull-right" disabled="disabled">${_('Update commits')}</button>
295 335 % endif
296 336 % if len(c.commit_ranges):
297 337 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
@@ -305,7 +345,7 b''
305 345 ${cbdiffs.render_diffset(
306 346 c.diffset, use_comments=True,
307 347 collapse_when_files_over=30,
308 disable_new_comments=c.pull_request.is_closed())}
348 disable_new_comments=not c.allowed_to_comment)}
309 349
310 350 </div>
311 351 % endif
@@ -313,9 +353,8 b''
313 353
314 354 ## template for inline comment form
315 355 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
316 ${comment.comment_inline_form()}
317 356
318 ## render comments and inlines
357 ## render general comments
319 358 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
320 359
321 360 % if not c.pull_request.is_closed():
@@ -394,7 +433,7 b''
394 433 this.closeButton.hide();
395 434 this.addButton.hide();
396 435 this.removeButtons.css('visibility', 'hidden');
397 },
436 }
398 437 };
399 438
400 439 PRDetails.init();
@@ -402,7 +441,7 b''
402 441
403 442 $('#show-outdated-comments').on('click', function(e){
404 443 var button = $(this);
405 var outdated = $('.outdated');
444 var outdated = $('.comment-outdated');
406 445 if (button.html() === "(Show)") {
407 446 button.html("(Hide)");
408 447 outdated.show();
@@ -428,29 +467,6 b''
428 467 $(btns).each(fn_display);
429 468 });
430 469
431 // inject comments into their proper positions
432 var file_comments = $('.inline-comment-placeholder');
433 %if c.pull_request.is_closed():
434 renderInlineComments(file_comments, false);
435 %else:
436 renderInlineComments(file_comments, true);
437 %endif
438 var commentTotals = {};
439 $.each(file_comments, function(i, comment) {
440 var path = $(comment).attr('path');
441 var comms = $(comment).children().length;
442 if (path in commentTotals) {
443 commentTotals[path] += comms;
444 } else {
445 commentTotals[path] = comms;
446 }
447 });
448 $.each(commentTotals, function(path, total) {
449 var elem = $('.comment-bubble[data-path="'+ path +'"]');
450 elem.css('visibility', 'visible');
451 elem.html(elem.html() + ' ' + total );
452 });
453
454 470 $('#merge_pull_request_form').submit(function() {
455 471 if (!$('#merge_pull_request').attr('disabled')) {
456 472 $('#merge_pull_request').attr('disabled', 'disabled');
General Comments 0
You need to be logged in to leave comments. Login now