##// END OF EJS Templates
sidebar: fixes to comment links, and new hovercard info about a comment.
marcink -
r4488:e79c19d1 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,397 +1,397 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import itertools
23 23 import logging
24 24 import collections
25 25
26 26 from rhodecode.model import BaseModel
27 27 from rhodecode.model.db import (
28 28 ChangesetStatus, ChangesetComment, PullRequest, Session)
29 29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 30 from rhodecode.lib.markup_renderer import (
31 31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class ChangesetStatusModel(BaseModel):
37 37
38 38 cls = ChangesetStatus
39 39
40 40 def __get_changeset_status(self, changeset_status):
41 41 return self._get_instance(ChangesetStatus, changeset_status)
42 42
43 43 def __get_pull_request(self, pull_request):
44 44 return self._get_instance(PullRequest, pull_request)
45 45
46 46 def _get_status_query(self, repo, revision, pull_request,
47 47 with_revisions=False):
48 48 repo = self._get_repo(repo)
49 49
50 50 q = ChangesetStatus.query()\
51 51 .filter(ChangesetStatus.repo == repo)
52 52 if not with_revisions:
53 53 q = q.filter(ChangesetStatus.version == 0)
54 54
55 55 if revision:
56 56 q = q.filter(ChangesetStatus.revision == revision)
57 57 elif pull_request:
58 58 pull_request = self.__get_pull_request(pull_request)
59 59 # TODO: johbo: Think about the impact of this join, there must
60 60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 61 # to the pull request. Might be that we want to do the same for
62 62 # the pull_request_version_id.
63 63 q = q.join(ChangesetComment).filter(
64 64 ChangesetStatus.pull_request == pull_request,
65 65 ChangesetComment.pull_request_version_id == None)
66 66 else:
67 67 raise Exception('Please specify revision or pull_request')
68 68 q = q.order_by(ChangesetStatus.version.asc())
69 69 return q
70 70
71 71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 72 trim_votes=True):
73 73 """
74 74 Calculate status based on given group members, and voting rule
75 75
76 76
77 77 group1 - 4 members, 3 required for approval
78 78 user1 - approved
79 79 user2 - reject
80 80 user3 - approved
81 81 user4 - rejected
82 82
83 83 final_state: rejected, reasons not at least 3 votes
84 84
85 85
86 86 group1 - 4 members, 2 required for approval
87 87 user1 - approved
88 88 user2 - reject
89 89 user3 - approved
90 90 user4 - rejected
91 91
92 92 final_state: approved, reasons got at least 2 approvals
93 93
94 94 group1 - 4 members, ALL required for approval
95 95 user1 - approved
96 96 user2 - reject
97 97 user3 - approved
98 98 user4 - rejected
99 99
100 100 final_state: rejected, reasons not all approvals
101 101
102 102
103 103 group1 - 4 members, ALL required for approval
104 104 user1 - approved
105 105 user2 - approved
106 106 user3 - approved
107 107 user4 - approved
108 108
109 109 final_state: approved, reason all approvals received
110 110
111 111 group1 - 4 members, 5 required for approval
112 112 (approval should be shorted to number of actual members)
113 113
114 114 user1 - approved
115 115 user2 - approved
116 116 user3 - approved
117 117 user4 - approved
118 118
119 119 final_state: approved, reason all approvals received
120 120
121 121 """
122 122 group_vote_data = {}
123 123 got_rule = False
124 124 members = collections.OrderedDict()
125 125 for review_obj, user, reasons, mandatory, statuses \
126 126 in group_statuses_by_reviewers:
127 127
128 128 if not got_rule:
129 129 group_vote_data = review_obj.rule_user_group_data()
130 130 got_rule = bool(group_vote_data)
131 131
132 132 members[user.user_id] = statuses
133 133
134 134 if not group_vote_data:
135 135 return []
136 136
137 137 required_votes = group_vote_data['vote_rule']
138 138 if required_votes == -1:
139 139 # -1 means all required, so we replace it with how many people
140 140 # are in the members
141 141 required_votes = len(members)
142 142
143 143 if trim_votes and required_votes > len(members):
144 144 # we require more votes than we have members in the group
145 145 # in this case we trim the required votes to the number of members
146 146 required_votes = len(members)
147 147
148 148 approvals = sum([
149 149 1 for statuses in members.values()
150 150 if statuses and
151 151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152 152
153 153 calculated_votes = []
154 154 # we have all votes from users, now check if we have enough votes
155 155 # to fill other
156 156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 157 if approvals >= required_votes:
158 158 fill_in = ChangesetStatus.STATUS_APPROVED
159 159
160 160 for member, statuses in members.items():
161 161 if statuses:
162 162 ver, latest = statuses[0]
163 163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 164 calculated_votes.append(fill_in)
165 165 else:
166 166 calculated_votes.append(latest.status)
167 167 else:
168 168 calculated_votes.append(fill_in)
169 169
170 170 return calculated_votes
171 171
172 172 def calculate_status(self, statuses_by_reviewers):
173 173 """
174 174 Given the approval statuses from reviewers, calculates final approval
175 175 status. There can only be 3 results, all approved, all rejected. If
176 176 there is no consensus the PR is under review.
177 177
178 178 :param statuses_by_reviewers:
179 179 """
180 180
181 181 def group_rule(element):
182 182 review_obj = element[0]
183 183 rule_data = review_obj.rule_user_group_data()
184 184 if rule_data and rule_data['id']:
185 185 return rule_data['id']
186 186
187 187 voting_groups = itertools.groupby(
188 188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189 189
190 190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191 191
192 192 reviewers_number = len(statuses_by_reviewers)
193 193 votes = collections.defaultdict(int)
194 194 for group, group_statuses_by_reviewers in voting_by_groups:
195 195 if group:
196 196 # calculate how the "group" voted
197 197 for vote_status in self.calculate_group_vote(
198 198 group, group_statuses_by_reviewers):
199 199 votes[vote_status] += 1
200 200 else:
201 201
202 202 for review_obj, user, reasons, mandatory, statuses \
203 203 in group_statuses_by_reviewers:
204 204 # individual vote
205 205 if statuses:
206 206 ver, latest = statuses[0]
207 207 votes[latest.status] += 1
208 208
209 209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211 211
212 212 # TODO(marcink): with group voting, how does rejected work,
213 213 # do we ever get rejected state ?
214 214
215 if approved_votes_count == reviewers_number:
215 if approved_votes_count and (approved_votes_count == reviewers_number):
216 216 return ChangesetStatus.STATUS_APPROVED
217 217
218 if rejected_votes_count == reviewers_number:
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 219 return ChangesetStatus.STATUS_REJECTED
220 220
221 221 return ChangesetStatus.STATUS_UNDER_REVIEW
222 222
223 223 def get_statuses(self, repo, revision=None, pull_request=None,
224 224 with_revisions=False):
225 225 q = self._get_status_query(repo, revision, pull_request,
226 226 with_revisions)
227 227 return q.all()
228 228
229 229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 230 """
231 231 Returns latest status of changeset for given revision or for given
232 232 pull request. Statuses are versioned inside a table itself and
233 233 version == 0 is always the current one
234 234
235 235 :param repo:
236 236 :param revision: 40char hash or None
237 237 :param pull_request: pull_request reference
238 238 :param as_str: return status as string not object
239 239 """
240 240 q = self._get_status_query(repo, revision, pull_request)
241 241
242 242 # need to use first here since there can be multiple statuses
243 243 # returned from pull_request
244 244 status = q.first()
245 245 if as_str:
246 246 status = status.status if status else status
247 247 st = status or ChangesetStatus.DEFAULT
248 248 return str(st)
249 249 return status
250 250
251 251 def _render_auto_status_message(
252 252 self, status, commit_id=None, pull_request=None):
253 253 """
254 254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 255 so it's always looking the same disregarding on which default
256 256 renderer system is using.
257 257
258 258 :param status: status text to change into
259 259 :param commit_id: the commit_id we change the status for
260 260 :param pull_request: the pull request we change the status for
261 261 """
262 262
263 263 new_status = ChangesetStatus.get_status_lbl(status)
264 264
265 265 params = {
266 266 'new_status_label': new_status,
267 267 'pull_request': pull_request,
268 268 'commit_id': commit_id,
269 269 }
270 270 renderer = RstTemplateRenderer()
271 271 return renderer.render('auto_status_change.mako', **params)
272 272
273 273 def set_status(self, repo, status, user, comment=None, revision=None,
274 274 pull_request=None, dont_allow_on_closed_pull_request=False):
275 275 """
276 276 Creates new status for changeset or updates the old ones bumping their
277 277 version, leaving the current status at
278 278
279 279 :param repo:
280 280 :param revision:
281 281 :param status:
282 282 :param user:
283 283 :param comment:
284 284 :param dont_allow_on_closed_pull_request: don't allow a status change
285 285 if last status was for pull request and it's closed. We shouldn't
286 286 mess around this manually
287 287 """
288 288 repo = self._get_repo(repo)
289 289
290 290 q = ChangesetStatus.query()
291 291
292 292 if revision:
293 293 q = q.filter(ChangesetStatus.repo == repo)
294 294 q = q.filter(ChangesetStatus.revision == revision)
295 295 elif pull_request:
296 296 pull_request = self.__get_pull_request(pull_request)
297 297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 299 cur_statuses = q.all()
300 300
301 301 # if statuses exists and last is associated with a closed pull request
302 302 # we need to check if we can allow this status change
303 303 if (dont_allow_on_closed_pull_request and cur_statuses
304 304 and getattr(cur_statuses[0].pull_request, 'status', '')
305 305 == PullRequest.STATUS_CLOSED):
306 306 raise StatusChangeOnClosedPullRequestError(
307 307 'Changing status on closed pull request is not allowed'
308 308 )
309 309
310 310 # update all current statuses with older version
311 311 if cur_statuses:
312 312 for st in cur_statuses:
313 313 st.version += 1
314 314 Session().add(st)
315 315 Session().flush()
316 316
317 317 def _create_status(user, repo, status, comment, revision, pull_request):
318 318 new_status = ChangesetStatus()
319 319 new_status.author = self._get_user(user)
320 320 new_status.repo = self._get_repo(repo)
321 321 new_status.status = status
322 322 new_status.comment = comment
323 323 new_status.revision = revision
324 324 new_status.pull_request = pull_request
325 325 return new_status
326 326
327 327 if not comment:
328 328 from rhodecode.model.comment import CommentsModel
329 329 comment = CommentsModel().create(
330 330 text=self._render_auto_status_message(
331 331 status, commit_id=revision, pull_request=pull_request),
332 332 repo=repo,
333 333 user=user,
334 334 pull_request=pull_request,
335 335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 336 )
337 337
338 338 if revision:
339 339 new_status = _create_status(
340 340 user=user, repo=repo, status=status, comment=comment,
341 341 revision=revision, pull_request=pull_request)
342 342 Session().add(new_status)
343 343 return new_status
344 344 elif pull_request:
345 345 # pull request can have more than one revision associated to it
346 346 # we need to create new version for each one
347 347 new_statuses = []
348 348 repo = pull_request.source_repo
349 349 for rev in pull_request.revisions:
350 350 new_status = _create_status(
351 351 user=user, repo=repo, status=status, comment=comment,
352 352 revision=rev, pull_request=pull_request)
353 353 new_statuses.append(new_status)
354 354 Session().add(new_status)
355 355 return new_statuses
356 356
357 357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
358 358
359 359 commit_statuses_map = collections.defaultdict(list)
360 360 for st in commit_statuses:
361 361 commit_statuses_map[st.author.username] += [st]
362 362
363 363 reviewers = []
364 364
365 365 def version(commit_status):
366 366 return commit_status.version
367 367
368 368 for obj in reviewers_data:
369 369 if not obj.user:
370 370 continue
371 371 statuses = commit_statuses_map.get(obj.user.username, None)
372 372 if statuses:
373 373 status_groups = itertools.groupby(
374 374 sorted(statuses, key=version), version)
375 375 statuses = [(x, list(y)[0]) for x, y in status_groups]
376 376
377 377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378 378
379 379 return reviewers
380 380
381 381 def reviewers_statuses(self, pull_request):
382 382 _commit_statuses = self.get_statuses(
383 383 pull_request.source_repo,
384 384 pull_request=pull_request,
385 385 with_revisions=True)
386 386
387 387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
388 388
389 389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
390 390 """
391 391 calculate pull request status based on reviewers, it should be a list
392 392 of two element lists.
393 393
394 394 :param reviewers_statuses:
395 395 """
396 396 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
397 397 return self.calculate_status(reviewers)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,697 +1,706 b''
1 1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 RhodeCode JS Files
21 21 **/
22 22
23 23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 24 console = { log: function() {} }
25 25 }
26 26
27 27 // TODO: move the following function to submodules
28 28
29 29 /**
30 30 * show more
31 31 */
32 32 var show_more_event = function(){
33 33 $('table .show_more').click(function(e) {
34 34 var cid = e.target.id.substring(1);
35 35 var button = $(this);
36 36 if (button.hasClass('open')) {
37 37 $('#'+cid).hide();
38 38 button.removeClass('open');
39 39 } else {
40 40 $('#'+cid).show();
41 41 button.addClass('open one');
42 42 }
43 43 });
44 44 };
45 45
46 46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 47 $('#compare_action').on('click', function(e){
48 48 e.preventDefault();
49 49
50 50 var source = $('input[name=compare_source]:checked').val();
51 51 var target = $('input[name=compare_target]:checked').val();
52 52 if(source && target){
53 53 var url_data = {
54 54 repo_name: repo_name,
55 55 source_ref: source,
56 56 source_ref_type: compare_ref_type,
57 57 target_ref: target,
58 58 target_ref_type: compare_ref_type,
59 59 merge: 1
60 60 };
61 61 window.location = pyroutes.url('repo_compare', url_data);
62 62 }
63 63 });
64 64 $('.compare-radio-button').on('click', function(e){
65 65 var source = $('input[name=compare_source]:checked').val();
66 66 var target = $('input[name=compare_target]:checked').val();
67 67 if(source && target){
68 68 $('#compare_action').removeAttr("disabled");
69 69 $('#compare_action').removeClass("disabled");
70 70 }
71 71 })
72 72 };
73 73
74 74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 75 var container = $('#' + target);
76 76 var url = pyroutes.url('repo_stats',
77 77 {"repo_name": repo_name, "commit_id": commit_id});
78 78
79 79 container.show();
80 80 if (!container.hasClass('loaded')) {
81 81 $.ajax({url: url})
82 82 .complete(function (data) {
83 83 var responseJSON = data.responseJSON;
84 84 container.addClass('loaded');
85 85 container.html(responseJSON.size);
86 86 callback(responseJSON.code_stats)
87 87 })
88 88 .fail(function (data) {
89 89 console.log('failed to load repo stats');
90 90 });
91 91 }
92 92
93 93 };
94 94
95 95 var showRepoStats = function(target, data){
96 96 var container = $('#' + target);
97 97
98 98 if (container.hasClass('loaded')) {
99 99 return
100 100 }
101 101
102 102 var total = 0;
103 103 var no_data = true;
104 104 var tbl = document.createElement('table');
105 105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106 106
107 107 $.each(data, function(key, val){
108 108 total += val.count;
109 109 });
110 110
111 111 var sortedStats = [];
112 112 for (var obj in data){
113 113 sortedStats.push([obj, data[obj]])
114 114 }
115 115 var sortedData = sortedStats.sort(function (a, b) {
116 116 return b[1].count - a[1].count
117 117 });
118 118 var cnt = 0;
119 119 $.each(sortedData, function(idx, val){
120 120 cnt += 1;
121 121 no_data = false;
122 122
123 123 var tr = document.createElement('tr');
124 124
125 125 var key = val[0];
126 126 var obj = {"desc": val[1].desc, "count": val[1].count};
127 127
128 128 // meta language names
129 129 var td1 = document.createElement('td');
130 130 var trending_language_label = document.createElement('div');
131 131 trending_language_label.innerHTML = obj.desc;
132 132 td1.appendChild(trending_language_label);
133 133
134 134 // extensions
135 135 var td2 = document.createElement('td');
136 136 var extension = document.createElement('div');
137 137 extension.innerHTML = ".{0}".format(key)
138 138 td2.appendChild(extension);
139 139
140 140 // number of files
141 141 var td3 = document.createElement('td');
142 142 var file_count = document.createElement('div');
143 143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 144 var label = _ngettext('file', 'files', obj.count);
145 145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 146 td3.appendChild(file_count);
147 147
148 148 // percentage
149 149 var td4 = document.createElement('td');
150 150 td4.setAttribute("class", 'trending_language');
151 151
152 152 var percentage = document.createElement('div');
153 153 percentage.setAttribute('class', 'lang-bar');
154 154 percentage.innerHTML = "&nbsp;";
155 155 percentage.style.width = percentage_num + '%';
156 156 td4.appendChild(percentage);
157 157
158 158 tr.appendChild(td1);
159 159 tr.appendChild(td2);
160 160 tr.appendChild(td3);
161 161 tr.appendChild(td4);
162 162 tbl.appendChild(tr);
163 163
164 164 });
165 165
166 166 $(container).html(tbl);
167 167 $(container).addClass('loaded');
168 168
169 169 $('#code_stats_show_more').on('click', function (e) {
170 170 e.preventDefault();
171 171 $('.stats_hidden').each(function (idx) {
172 172 $(this).css("display", "");
173 173 });
174 174 $('#code_stats_show_more').hide();
175 175 });
176 176
177 177 };
178 178
179 179 // returns a node from given html;
180 180 var fromHTML = function(html){
181 181 var _html = document.createElement('element');
182 182 _html.innerHTML = html;
183 183 return _html;
184 184 };
185 185
186 186 // Toggle Collapsable Content
187 187 function collapsableContent() {
188 188
189 189 $('.collapsable-content').not('.no-hide').hide();
190 190
191 191 $('.btn-collapse').unbind(); //in case we've been here before
192 192 $('.btn-collapse').click(function() {
193 193 var button = $(this);
194 194 var togglename = $(this).data("toggle");
195 195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 196 if ($(this).html()=="Show Less")
197 197 $(this).html("Show More");
198 198 else
199 199 $(this).html("Show Less");
200 200 });
201 201 };
202 202
203 203 var timeagoActivate = function() {
204 204 $("time.timeago").timeago();
205 205 };
206 206
207 207
208 208 var clipboardActivate = function() {
209 209 /*
210 210 *
211 211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 212 * */
213 213 var clipboard = new ClipboardJS('.clipboard-action');
214 214
215 215 clipboard.on('success', function(e) {
216 216 var callback = function () {
217 217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 218 };
219 219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 220 e.clearSelection();
221 221 });
222 222 };
223 223
224 224 var tooltipActivate = function () {
225 225 var delay = 50;
226 226 var animation = 'fade';
227 227 var theme = 'tooltipster-shadow';
228 228 var debug = false;
229 229
230 230 $('.tooltip').tooltipster({
231 231 debug: debug,
232 232 theme: theme,
233 233 animation: animation,
234 234 delay: delay,
235 235 contentCloning: true,
236 236 contentAsHTML: true,
237 237
238 238 functionBefore: function (instance, helper) {
239 239 var $origin = $(helper.origin);
240 240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 241 instance.content(data);
242 242 }
243 243 });
244 244 var hovercardCache = {};
245 245
246 246 var loadHoverCard = function (url, altHovercard, callback) {
247 247 var id = url;
248 248
249 249 if (hovercardCache[id] !== undefined) {
250 250 callback(hovercardCache[id]);
251 251 return true;
252 252 }
253 253
254 254 hovercardCache[id] = undefined;
255 255 $.get(url, function (data) {
256 256 hovercardCache[id] = data;
257 257 callback(hovercardCache[id]);
258 258 return true;
259 259 }).fail(function (data, textStatus, errorThrown) {
260 260
261 261 if (parseInt(data.status) === 404) {
262 262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
263 263 } else {
264 264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
265 265 }
266 266 callback(msg);
267 267 return false
268 268 });
269 269 };
270 270
271 271 $('.tooltip-hovercard').tooltipster({
272 272 debug: debug,
273 273 theme: theme,
274 274 animation: animation,
275 275 delay: delay,
276 276 interactive: true,
277 277 contentCloning: true,
278 278
279 279 trigger: 'custom',
280 280 triggerOpen: {
281 281 mouseenter: true,
282 282 },
283 283 triggerClose: {
284 284 mouseleave: true,
285 285 originClick: true,
286 286 touchleave: true
287 287 },
288 288 content: _gettext('Loading...'),
289 289 contentAsHTML: true,
290 290 updateAnimation: null,
291 291
292 292 functionBefore: function (instance, helper) {
293 293
294 294 var $origin = $(helper.origin);
295 295
296 296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
297 297 if ($origin.data('loaded') !== true) {
298 298 var hovercardUrl = $origin.data('hovercardUrl');
299 299 var altHovercard =$origin.data('hovercardAlt');
300 300
301 301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 var urlLoad = true;
302 303 if (hovercardUrl.substr(0,12) === 'pyroutes.url'){
303 304 hovercardUrl = eval(hovercardUrl)
305 } else if (hovercardUrl.substr(0, 11) === 'javascript:') {
306 var jsFunc = hovercardUrl.substr(11);
307 urlLoad = false;
308 loaded = true;
309 instance.content(eval(jsFunc))
304 310 }
305 311
312 if (urlLoad) {
306 313 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
307 314 instance.content(data);
308 315 })
316 }
317
309 318 } else {
310 319 if ($origin.data('hovercardAltHtml')) {
311 320 var data = atob($origin.data('hovercardAltHtml'));
312 321 } else {
313 322 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
314 323 }
315 324 var loaded = true;
316 325 instance.content(data);
317 326 }
318 327
319 328 // to remember that the data has been loaded
320 329 $origin.data('loaded', loaded);
321 330 }
322 331 }
323 332 })
324 333 };
325 334
326 335 // Formatting values in a Select2 dropdown of commit references
327 336 var formatSelect2SelectionRefs = function(commit_ref){
328 337 var tmpl = '';
329 338 if (!commit_ref.text || commit_ref.type === 'sha'){
330 339 return commit_ref.text;
331 340 }
332 341 if (commit_ref.type === 'branch'){
333 342 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
334 343 } else if (commit_ref.type === 'tag'){
335 344 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
336 345 } else if (commit_ref.type === 'book'){
337 346 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
338 347 }
339 348 return tmpl.concat(escapeHtml(commit_ref.text));
340 349 };
341 350
342 351 // takes a given html element and scrolls it down offset pixels
343 352 function offsetScroll(element, offset) {
344 353 setTimeout(function() {
345 354 var location = element.offset().top;
346 355 // some browsers use body, some use html
347 356 $('html, body').animate({ scrollTop: (location - offset) });
348 357 }, 100);
349 358 }
350 359
351 360 // scroll an element `percent`% from the top of page in `time` ms
352 361 function scrollToElement(element, percent, time) {
353 362 percent = (percent === undefined ? 25 : percent);
354 363 time = (time === undefined ? 100 : time);
355 364
356 365 var $element = $(element);
357 366 if ($element.length == 0) {
358 367 throw('Cannot scroll to {0}'.format(element))
359 368 }
360 369 var elOffset = $element.offset().top;
361 370 var elHeight = $element.height();
362 371 var windowHeight = $(window).height();
363 372 var offset = elOffset;
364 373 if (elHeight < windowHeight) {
365 374 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
366 375 }
367 376 setTimeout(function() {
368 377 $('html, body').animate({ scrollTop: offset});
369 378 }, time);
370 379 }
371 380
372 381 /**
373 382 * global hooks after DOM is loaded
374 383 */
375 384 $(document).ready(function() {
376 385 firefoxAnchorFix();
377 386
378 387 $('.navigation a.menulink').on('click', function(e){
379 388 var menuitem = $(this).parent('li');
380 389 if (menuitem.hasClass('open')) {
381 390 menuitem.removeClass('open');
382 391 } else {
383 392 menuitem.addClass('open');
384 393 $(document).on('click', function(event) {
385 394 if (!$(event.target).closest(menuitem).length) {
386 395 menuitem.removeClass('open');
387 396 }
388 397 });
389 398 }
390 399 });
391 400
392 401 $('body').on('click', '.cb-lineno a', function(event) {
393 402 function sortNumber(a,b) {
394 403 return a - b;
395 404 }
396 405
397 406 var lineNo = $(this).data('lineNo');
398 407 var lineName = $(this).attr('name');
399 408
400 409 if (lineNo) {
401 410 var prevLine = $('.cb-line-selected a').data('lineNo');
402 411
403 412 // on shift, we do a range selection, if we got previous line
404 413 if (event.shiftKey && prevLine !== undefined) {
405 414 var prevLine = parseInt(prevLine);
406 415 var nextLine = parseInt(lineNo);
407 416 var pos = [prevLine, nextLine].sort(sortNumber);
408 417 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
409 418
410 419 // single click
411 420 } else {
412 421 var nextLine = parseInt(lineNo);
413 422 var pos = [nextLine, nextLine];
414 423 var anchor = '#L{0}'.format(pos[0]);
415 424
416 425 }
417 426 // highlight
418 427 var range = [];
419 428 for (var i = pos[0]; i <= pos[1]; i++) {
420 429 range.push(i);
421 430 }
422 431 // clear old selected lines
423 432 $('.cb-line-selected').removeClass('cb-line-selected');
424 433
425 434 $.each(range, function (i, lineNo) {
426 435 var line_td = $('td.cb-lineno#L' + lineNo);
427 436
428 437 if (line_td.length) {
429 438 line_td.addClass('cb-line-selected'); // line number td
430 439 line_td.prev().addClass('cb-line-selected'); // line data
431 440 line_td.next().addClass('cb-line-selected'); // line content
432 441 }
433 442 });
434 443
435 444 } else if (lineName !== undefined) { // lineName only occurs in diffs
436 445 // clear old selected lines
437 446 $('td.cb-line-selected').removeClass('cb-line-selected');
438 447 var anchor = '#{0}'.format(lineName);
439 448 var diffmode = templateContext.session_attrs.diffmode || "sideside";
440 449
441 450 if (diffmode === "unified") {
442 451 $(this).closest('tr').find('td').addClass('cb-line-selected');
443 452 } else {
444 453 var activeTd = $(this).closest('td');
445 454 activeTd.addClass('cb-line-selected');
446 455 activeTd.next('td').addClass('cb-line-selected');
447 456 }
448 457
449 458 }
450 459
451 460 // Replace URL without jumping to it if browser supports.
452 461 // Default otherwise
453 462 if (history.pushState && anchor !== undefined) {
454 463 var new_location = location.href.rstrip('#');
455 464 if (location.hash) {
456 465 // location without hash
457 466 new_location = new_location.replace(location.hash, "");
458 467 }
459 468
460 469 // Make new anchor url
461 470 new_location = new_location + anchor;
462 471 history.pushState(true, document.title, new_location);
463 472
464 473 return false;
465 474 }
466 475
467 476 });
468 477
469 478 $('.collapse_file').on('click', function(e) {
470 479 e.stopPropagation();
471 480 if ($(e.target).is('a')) { return; }
472 481 var node = $(e.delegateTarget).first();
473 482 var icon = $($(node.children().first()).children().first());
474 483 var id = node.attr('fid');
475 484 var target = $('#'+id);
476 485 var tr = $('#tr_'+id);
477 486 var diff = $('#diff_'+id);
478 487 if(node.hasClass('expand_file')){
479 488 node.removeClass('expand_file');
480 489 icon.removeClass('expand_file_icon');
481 490 node.addClass('collapse_file');
482 491 icon.addClass('collapse_file_icon');
483 492 diff.show();
484 493 tr.show();
485 494 target.show();
486 495 } else {
487 496 node.removeClass('collapse_file');
488 497 icon.removeClass('collapse_file_icon');
489 498 node.addClass('expand_file');
490 499 icon.addClass('expand_file_icon');
491 500 diff.hide();
492 501 tr.hide();
493 502 target.hide();
494 503 }
495 504 });
496 505
497 506 $('#expand_all_files').click(function() {
498 507 $('.expand_file').each(function() {
499 508 var node = $(this);
500 509 var icon = $($(node.children().first()).children().first());
501 510 var id = $(this).attr('fid');
502 511 var target = $('#'+id);
503 512 var tr = $('#tr_'+id);
504 513 var diff = $('#diff_'+id);
505 514 node.removeClass('expand_file');
506 515 icon.removeClass('expand_file_icon');
507 516 node.addClass('collapse_file');
508 517 icon.addClass('collapse_file_icon');
509 518 diff.show();
510 519 tr.show();
511 520 target.show();
512 521 });
513 522 });
514 523
515 524 $('#collapse_all_files').click(function() {
516 525 $('.collapse_file').each(function() {
517 526 var node = $(this);
518 527 var icon = $($(node.children().first()).children().first());
519 528 var id = $(this).attr('fid');
520 529 var target = $('#'+id);
521 530 var tr = $('#tr_'+id);
522 531 var diff = $('#diff_'+id);
523 532 node.removeClass('collapse_file');
524 533 icon.removeClass('collapse_file_icon');
525 534 node.addClass('expand_file');
526 535 icon.addClass('expand_file_icon');
527 536 diff.hide();
528 537 tr.hide();
529 538 target.hide();
530 539 });
531 540 });
532 541
533 542 // Mouse over behavior for comments and line selection
534 543
535 544 // Select the line that comes from the url anchor
536 545 // At the time of development, Chrome didn't seem to support jquery's :target
537 546 // element, so I had to scroll manually
538 547
539 548 if (location.hash) {
540 549 var result = splitDelimitedHash(location.hash);
541 550
542 551 var loc = result.loc;
543 552
544 553 if (loc.length > 1) {
545 554
546 555 var highlightable_line_tds = [];
547 556
548 557 // source code line format
549 558 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
550 559
551 560 // multi-line HL, for files
552 561 if (page_highlights.length > 1) {
553 562 var highlight_ranges = page_highlights[1].split(",");
554 563 var h_lines = [];
555 564 for (var pos in highlight_ranges) {
556 565 var _range = highlight_ranges[pos].split('-');
557 566 if (_range.length === 2) {
558 567 var start = parseInt(_range[0]);
559 568 var end = parseInt(_range[1]);
560 569 if (start < end) {
561 570 for (var i = start; i <= end; i++) {
562 571 h_lines.push(i);
563 572 }
564 573 }
565 574 } else {
566 575 h_lines.push(parseInt(highlight_ranges[pos]));
567 576 }
568 577 }
569 578 for (pos in h_lines) {
570 579 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
571 580 if (line_td.length) {
572 581 highlightable_line_tds.push(line_td);
573 582 }
574 583 }
575 584 }
576 585
577 586 // now check a direct id reference of line in diff / pull-request page)
578 587 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
579 588 highlightable_line_tds.push($(loc));
580 589 }
581 590
582 591 // mark diff lines as selected
583 592 $.each(highlightable_line_tds, function (i, $td) {
584 593 $td.addClass('cb-line-selected'); // line number td
585 594 $td.prev().addClass('cb-line-selected'); // line data
586 595 $td.next().addClass('cb-line-selected'); // line content
587 596 });
588 597
589 598 if (highlightable_line_tds.length > 0) {
590 599 var $first_line_td = highlightable_line_tds[0];
591 600 scrollToElement($first_line_td);
592 601 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
593 602 td: $first_line_td,
594 603 remainder: result.remainder
595 604 });
596 605 } else {
597 606 // case for direct anchor to comments
598 607 var $line = $(loc);
599 608
600 609 if ($line.hasClass('comment-general')) {
601 610 $line.show();
602 611 } else if ($line.hasClass('comment-inline')) {
603 612 $line.show();
604 613 var $cb = $line.closest('.cb');
605 614 $cb.removeClass('cb-collapsed')
606 615 }
607 616 if ($line.length > 0) {
608 617 $line.addClass('comment-selected-hl');
609 618 offsetScroll($line, 70);
610 619 }
611 620 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
612 621 $line.nextAll('.cb-comment-add-button').trigger('click');
613 622 }
614 623 }
615 624
616 625 }
617 626 }
618 627 collapsableContent();
619 628 });
620 629
621 630 var feedLifetimeOptions = function(query, initialData){
622 631 var data = {results: []};
623 632 var isQuery = typeof query.term !== 'undefined';
624 633
625 634 var section = _gettext('Lifetime');
626 635 var children = [];
627 636
628 637 //filter results
629 638 $.each(initialData.results, function(idx, value) {
630 639
631 640 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
632 641 children.push({
633 642 'id': this.id,
634 643 'text': this.text
635 644 })
636 645 }
637 646
638 647 });
639 648 data.results.push({
640 649 'text': section,
641 650 'children': children
642 651 });
643 652
644 653 if (isQuery) {
645 654
646 655 var now = moment.utc();
647 656
648 657 var parseQuery = function(entry, now){
649 658 var fmt = 'DD/MM/YYYY H:mm';
650 659 var parsed = moment.utc(entry, fmt);
651 660 var diffInMin = parsed.diff(now, 'minutes');
652 661
653 662 if (diffInMin > 0){
654 663 return {
655 664 id: diffInMin,
656 665 text: parsed.format(fmt)
657 666 }
658 667 } else {
659 668 return {
660 669 id: undefined,
661 670 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
662 671 }
663 672 }
664 673
665 674
666 675 };
667 676
668 677 data.results.push({
669 678 'text': _gettext('Specified expiration date'),
670 679 'children': [{
671 680 'id': parseQuery(query.term, now).id,
672 681 'text': parseQuery(query.term, now).text
673 682 }]
674 683 });
675 684 }
676 685
677 686 query.callback(data);
678 687 };
679 688
680 689 /*
681 690 * Retrievew via templateContext.session_attrs.key
682 691 * */
683 692 var storeUserSessionAttr = function (key, val) {
684 693
685 694 var postData = {
686 695 'key': key,
687 696 'val': val,
688 697 'csrf_token': CSRF_TOKEN
689 698 };
690 699
691 700 var success = function(o) {
692 701 return true
693 702 };
694 703
695 704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
696 705 return false;
697 706 };
@@ -1,134 +1,140 b''
1 1 ## snippet for sidebar elements
2 2 ## usage:
3 3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 4 ## ${sidebar.comments_table()}
5 5 <%namespace name="base" file="/base/base.mako"/>
6 6
7 7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
8 8 <%
9 9 if todo_comments:
10 10 cls_ = 'todos-content-table'
11 11 def sorter(entry):
12 12 user_id = entry.author.user_id
13 13 resolved = '1' if entry.resolved else '0'
14 14 if user_id == c.rhodecode_user.user_id:
15 15 # own comments first
16 16 user_id = 0
17 17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 18 else:
19 19 cls_ = 'comments-content-table'
20 20 def sorter(entry):
21 21 user_id = entry.author.user_id
22 22 return '{}'.format(str(entry.comment_id).zfill(10000))
23 23
24 24 existing_ids = existing_ids or []
25 25
26 26 %>
27 27
28 28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29 29
30 30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 31 <%
32 32 display = ''
33 33 _cls = ''
34 34 %>
35 35
36 36 <%
37 37 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
38 38 prev_comment_ver_index = 0
39 39 if loop_obj.previous:
40 40 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
41 41
42 42 ver_info = None
43 43 if getattr(c, 'versions', []):
44 44 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
45 45 %>
46 46 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
47 47 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
48 48 <%
49 49 if (prev_comment_ver_index > comment_ver_index):
50 50 comments_ver_divider = comment_ver_index
51 51 else:
52 52 comments_ver_divider = None
53 53 %>
54 54
55 55 % if todo_comments:
56 56 % if comment_obj.resolved:
57 57 <% _cls = 'resolved-todo' %>
58 58 <% display = 'none' %>
59 59 % endif
60 60 % else:
61 61 ## SKIP TODOs we display them in other area
62 62 % if comment_obj.is_todo:
63 63 <% display = 'none' %>
64 64 % endif
65 65 ## Skip outdated comments
66 66 % if comment_obj.outdated:
67 67 <% display = 'none' %>
68 68 <% _cls = 'hidden-comment' %>
69 69 % endif
70 70 % endif
71 71
72 72 % if not todo_comments and comments_ver_divider:
73 73 <tr class="old-comments-marker">
74 74 <td colspan="3">
75 75 % if ver_info:
76 76 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
77 77 % else:
78 78 <code>v${comments_ver_divider}</code>
79 79 % endif
80 80 </td>
81 81 </tr>
82 82
83 83 % endif
84 84
85 85 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
86 86 <td class="td-todo-number">
87
88 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
89 href="#comment-${comment_obj.comment_id}"
90 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
91
92 87 <%
93 88 version_info = ''
94 89 if is_pr:
95 90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
96 91 %>
92 <script type="text/javascript">
93 // closure function helper
94 var sidebarComment${comment_obj.comment_id} = function() {
95 return renderTemplate('sideBarCommentHovercard', {
96 version_info: "${version_info}",
97 file_name: "${comment_obj.f_path}",
98 line_no: "${comment_obj.line_no}",
99 outdated: ${h.json.dumps(comment_obj.outdated)},
100 inline: ${h.json.dumps(comment_obj.is_inline)},
101 is_todo: ${h.json.dumps(comment_obj.is_todo)},
102 created_on: "${h.format_date(comment_obj.created_on)}",
103 datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
104 })
105 }
106 </script>
97 107
98 % if todo_comments:
99 % if comment_obj.is_inline:
100 <i class="tooltip icon-code" title="Inline TODO comment${version_info}."></i>
101 % else:
102 <i class="tooltip icon-comment" title="General TODO comment${version_info}."></i>
103 % endif
108 % if comment_obj.outdated:
109 <i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
110 % elif comment_obj.is_inline:
111 <i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
104 112 % else:
105 % if comment_obj.outdated:
106 <i class="tooltip icon-comment-toggle" title="Inline Outdated made in v${comment_ver_index}."></i>
107 % elif comment_obj.is_inline:
108 <i class="tooltip icon-code" title="Inline comment${version_info}."></i>
109 % else:
110 <i class="tooltip icon-comment" title="General comment${version_info}."></i>
111 % endif
113 <i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
112 114 % endif
113 115
114 </a>
115 116 ## NEW, since refresh
116 117 % if existing_ids and comment_obj.comment_id not in existing_ids:
117 118 <span class="tag">NEW</span>
118 119 % endif
119 120 </td>
120 121
121 122 <td class="td-todo-gravatar">
122 123 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
123 124 </td>
124 125 <td class="todo-comment-text-wrapper">
125 <div class="tooltip todo-comment-text timeago ${('todo-resolved' if comment_obj.resolved else '')} " title="${h.format_date(comment_obj.created_on)}" datetime="${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}">
126 <code>${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}</code>
126 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
127 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
128 href="#comment-${comment_obj.comment_id}"
129 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
130
131 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
132 </a>
127 133 </div>
128 134 </td>
129 135 </tr>
130 136 % endfor
131 137
132 138 </table>
133 139
134 140 </%def> No newline at end of file
@@ -1,193 +1,236 b''
1 1 <%text>
2 2 <div style="display: none">
3 3
4 4 <script>
5 5 var CG = new ColorGenerator();
6 6 </script>
7 7
8 8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
9 9
10 10 <%
11 11 if (size > 16) {
12 12 var gravatar_class = 'gravatar gravatar-large';
13 13 } else {
14 14 var gravatar_class = 'gravatar';
15 15 }
16 16
17 17 if (tooltip) {
18 18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
19 19 }
20 20
21 21 var data_hovercard_alt = username;
22 22
23 23 %>
24 24
25 25 <%
26 26 if (show_disabled) {
27 27 var user_cls = 'user user-disabled';
28 28 } else {
29 29 var user_cls = 'user';
30 30 }
31 31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
32 32 %>
33 33
34 34 <div class="rc-user">
35 35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
36 36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
37 37 </div>
38 38
39 39 </script>
40 40
41 41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 42 <%
43 43 if (create) {
44 44 var edit_visibility = 'visible';
45 45 } else {
46 46 var edit_visibility = 'hidden';
47 47 }
48 48
49 49 if (member.user_group && member.user_group.vote_rule) {
50 50 var reviewGroup = '<i class="icon-user-group"></i>';
51 51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 52 } else {
53 53 var reviewGroup = null;
54 54 var reviewGroupColor = 'transparent';
55 55 }
56 56 var rule_show = rule_show || false;
57 57
58 58 if (rule_show) {
59 59 var rule_visibility = 'table-cell';
60 60 } else {
61 61 var rule_visibility = 'none';
62 62 }
63 63
64 64 %>
65 65
66 66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67 67
68 68 <td style="width: 20px">
69 69 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
70 70 <i class="icon-circle review-status-<%= review_status %>"></i>
71 71 </div>
72 72 </td>
73 73
74 74 <td>
75 75 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
76 76 <%-
77 77 renderTemplate('gravatarWithUser', {
78 78 'size': 16,
79 79 'show_disabled': false,
80 80 'tooltip': true,
81 81 'username': member.username,
82 82 'user_id': member.user_id,
83 83 'user_link': member.user_link,
84 84 'gravatar_url': member.gravatar_link
85 85 })
86 86 %>
87 87 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
88 88 <i class="icon-eye" style="color: #0ac878"></i>
89 89 </span>
90 90 </div>
91 91 </td>
92 92
93 93 <td style="width: 10px">
94 94 <% if (reviewGroup !== null) { %>
95 95 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
96 96 <%- reviewGroup %>
97 97 </span>
98 98 <% } %>
99 99 </td>
100 100
101 101 <% if (mandatory) { %>
102 102 <td style="text-align: right;width: 10px;">
103 103 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
104 104 <i class="icon-lock"></i>
105 105 </div>
106 106 </td>
107 107
108 108 <% } else { %>
109 109 <td style="text-align: right;width: 10px;">
110 110 <% if (allowed_to_update) { %>
111 111 <div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
112 112 <i class="icon-remove"></i>
113 113 </div>
114 114 <% } %>
115 115 </td>
116 116 <% } %>
117 117
118 118 </tr>
119 119
120 120 <tr>
121 121 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
122 122 <input type="hidden" name="__start__" value="reviewer:mapping">
123 123
124 124 <%if (member.user_group && member.user_group.vote_rule) { %>
125 125 <div class="reviewer_reason">
126 126
127 127 <%if (member.user_group.vote_rule == -1) {%>
128 128 - group votes required: ALL
129 129 <%} else {%>
130 130 - group votes required: <%= member.user_group.vote_rule %>
131 131 <%}%>
132 132 </div>
133 133 <%} %>
134 134
135 135 <input type="hidden" name="__start__" value="reasons:sequence">
136 136 <% for (var i = 0; i < reasons.length; i++) { %>
137 137 <% var reason = reasons[i] %>
138 138 <div class="reviewer_reason">- <%= reason %></div>
139 139 <input type="hidden" name="reason" value="<%= reason %>">
140 140 <% } %>
141 141 <input type="hidden" name="__end__" value="reasons:sequence">
142 142
143 143 <input type="hidden" name="__start__" value="rules:sequence">
144 144 <% for (var i = 0; i < member.rules.length; i++) { %>
145 145 <% var rule = member.rules[i] %>
146 146 <input type="hidden" name="rule_id" value="<%= rule %>">
147 147 <% } %>
148 148 <input type="hidden" name="__end__" value="rules:sequence">
149 149
150 150 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
151 151 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
152 152
153 153 <input type="hidden" name="__end__" value="reviewer:mapping">
154 154 </td>
155 155 </tr>
156 156
157 157 </script>
158 158
159 159 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
160 160
161 161 <%
162 162 if (size > 16) {
163 163 var gravatar_class = 'gravatar gravatar-large';
164 164 } else {
165 165 var gravatar_class = 'gravatar';
166 166 }
167 167
168 168 %>
169 169
170 170 <%
171 171 if (show_disabled) {
172 172 var user_cls = 'user user-disabled';
173 173 } else {
174 174 var user_cls = 'user';
175 175 }
176 176
177 177 %>
178 178
179 179 <div style='line-height: 20px'>
180 180 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
181 181 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
182 182 </div>
183 183
184 184 </script>
185 185
186
187 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
188
189 <div>
190 <% if (is_todo) { %>
191 <% if (inline) { %>
192 <strong>Inline</strong> TODO on line: <%= line_no %>
193 <% if (version_info) { %>
194 <%= version_info %>
195 <% } %>
196 <br/>
197 File: <code><%- file_name -%></code>
198 <% } else { %>
199 <strong>General</strong> TODO
200 <% if (version_info) { %>
201 <%= version_info %>
202 <% } %>
203 <% } %>
204 <% } else { %>
205 <% if (inline) { %>
206 <strong>Inline</strong> comment on line: <%= line_no %>
207 <% if (version_info) { %>
208 <%= version_info %>
209 <% } %>
210 <br/>
211 File: <code><%- file_name -%></code>
212 <% } else { %>
213 <strong>General</strong> comment
214 <% if (version_info) { %>
215 <%= version_info %>
216 <% } %>
217 <% } %>
218 <% } %>
219 <br/>
220 Created:
221 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
222
186 223 </div>
187 224
225 </script>
226
227 ##// END OF EJS Templates
228 </div>
229
230
188 231 <script>
189 232 // registers the templates into global cache
190 233 registerTemplates();
191 234 </script>
192 235
193 236 </%text>
General Comments 0
You need to be logged in to leave comments. Login now