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