##// END OF EJS Templates
diffs: store wide-diff mode in user sessions for saved state.
marcink -
r3642:ffe3283f new-ui
parent child Browse files
Show More
@@ -1,746 +1,746 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2019 RhodeCode GmbH
3 # Copyright (C) 2016-2019 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 import re
21 import re
22 import logging
22 import logging
23 import collections
23 import collections
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator,
31 CSRFRequired)
31 CSRFRequired)
32 from rhodecode.lib.index import searcher_from_config
32 from rhodecode.lib.index import searcher_from_config
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
33 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.model.db import (
35 from rhodecode.model.db import (
36 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
36 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup)
37 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.repo_group import RepoGroupModel
38 from rhodecode.model.repo_group import RepoGroupModel
39 from rhodecode.model.scm import RepoGroupList, RepoList
39 from rhodecode.model.scm import RepoGroupList, RepoList
40 from rhodecode.model.user import UserModel
40 from rhodecode.model.user import UserModel
41 from rhodecode.model.user_group import UserGroupModel
41 from rhodecode.model.user_group import UserGroupModel
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class HomeView(BaseAppView):
46 class HomeView(BaseAppView):
47
47
48 def load_default_context(self):
48 def load_default_context(self):
49 c = self._get_local_tmpl_context()
49 c = self._get_local_tmpl_context()
50 c.user = c.auth_user.get_instance()
50 c.user = c.auth_user.get_instance()
51
51
52 return c
52 return c
53
53
54 @LoginRequired()
54 @LoginRequired()
55 @view_config(
55 @view_config(
56 route_name='user_autocomplete_data', request_method='GET',
56 route_name='user_autocomplete_data', request_method='GET',
57 renderer='json_ext', xhr=True)
57 renderer='json_ext', xhr=True)
58 def user_autocomplete_data(self):
58 def user_autocomplete_data(self):
59 self.load_default_context()
59 self.load_default_context()
60 query = self.request.GET.get('query')
60 query = self.request.GET.get('query')
61 active = str2bool(self.request.GET.get('active') or True)
61 active = str2bool(self.request.GET.get('active') or True)
62 include_groups = str2bool(self.request.GET.get('user_groups'))
62 include_groups = str2bool(self.request.GET.get('user_groups'))
63 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
63 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
64 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
64 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
65
65
66 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
66 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
67 query, active, include_groups)
67 query, active, include_groups)
68
68
69 _users = UserModel().get_users(
69 _users = UserModel().get_users(
70 name_contains=query, only_active=active)
70 name_contains=query, only_active=active)
71
71
72 def maybe_skip_default_user(usr):
72 def maybe_skip_default_user(usr):
73 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
73 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
74 return False
74 return False
75 return True
75 return True
76 _users = filter(maybe_skip_default_user, _users)
76 _users = filter(maybe_skip_default_user, _users)
77
77
78 if include_groups:
78 if include_groups:
79 # extend with user groups
79 # extend with user groups
80 _user_groups = UserGroupModel().get_user_groups(
80 _user_groups = UserGroupModel().get_user_groups(
81 name_contains=query, only_active=active,
81 name_contains=query, only_active=active,
82 expand_groups=expand_groups)
82 expand_groups=expand_groups)
83 _users = _users + _user_groups
83 _users = _users + _user_groups
84
84
85 return {'suggestions': _users}
85 return {'suggestions': _users}
86
86
87 @LoginRequired()
87 @LoginRequired()
88 @NotAnonymous()
88 @NotAnonymous()
89 @view_config(
89 @view_config(
90 route_name='user_group_autocomplete_data', request_method='GET',
90 route_name='user_group_autocomplete_data', request_method='GET',
91 renderer='json_ext', xhr=True)
91 renderer='json_ext', xhr=True)
92 def user_group_autocomplete_data(self):
92 def user_group_autocomplete_data(self):
93 self.load_default_context()
93 self.load_default_context()
94 query = self.request.GET.get('query')
94 query = self.request.GET.get('query')
95 active = str2bool(self.request.GET.get('active') or True)
95 active = str2bool(self.request.GET.get('active') or True)
96 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
96 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
97
97
98 log.debug('generating user group list, query:%s, active:%s',
98 log.debug('generating user group list, query:%s, active:%s',
99 query, active)
99 query, active)
100
100
101 _user_groups = UserGroupModel().get_user_groups(
101 _user_groups = UserGroupModel().get_user_groups(
102 name_contains=query, only_active=active,
102 name_contains=query, only_active=active,
103 expand_groups=expand_groups)
103 expand_groups=expand_groups)
104 _user_groups = _user_groups
104 _user_groups = _user_groups
105
105
106 return {'suggestions': _user_groups}
106 return {'suggestions': _user_groups}
107
107
108 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
108 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
109 org_query = name_contains
109 org_query = name_contains
110 allowed_ids = self._rhodecode_user.repo_acl_ids(
110 allowed_ids = self._rhodecode_user.repo_acl_ids(
111 ['repository.read', 'repository.write', 'repository.admin'],
111 ['repository.read', 'repository.write', 'repository.admin'],
112 cache=False, name_filter=name_contains) or [-1]
112 cache=False, name_filter=name_contains) or [-1]
113
113
114 query = Repository.query()\
114 query = Repository.query()\
115 .filter(Repository.archived.isnot(true()))\
115 .filter(Repository.archived.isnot(true()))\
116 .filter(or_(
116 .filter(or_(
117 # generate multiple IN to fix limitation problems
117 # generate multiple IN to fix limitation problems
118 *in_filter_generator(Repository.repo_id, allowed_ids)
118 *in_filter_generator(Repository.repo_id, allowed_ids)
119 ))
119 ))
120
120
121 query = query.order_by(case(
121 query = query.order_by(case(
122 [
122 [
123 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
123 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
124 ],
124 ],
125 ))
125 ))
126 query = query.order_by(func.length(Repository.repo_name))
126 query = query.order_by(func.length(Repository.repo_name))
127 query = query.order_by(Repository.repo_name)
127 query = query.order_by(Repository.repo_name)
128
128
129 if repo_type:
129 if repo_type:
130 query = query.filter(Repository.repo_type == repo_type)
130 query = query.filter(Repository.repo_type == repo_type)
131
131
132 if name_contains:
132 if name_contains:
133 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
133 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
134 query = query.filter(
134 query = query.filter(
135 Repository.repo_name.ilike(ilike_expression))
135 Repository.repo_name.ilike(ilike_expression))
136 query = query.limit(limit)
136 query = query.limit(limit)
137
137
138 acl_iter = query
138 acl_iter = query
139
139
140 return [
140 return [
141 {
141 {
142 'id': obj.repo_name,
142 'id': obj.repo_name,
143 'value': org_query,
143 'value': org_query,
144 'value_display': obj.repo_name,
144 'value_display': obj.repo_name,
145 'text': obj.repo_name,
145 'text': obj.repo_name,
146 'type': 'repo',
146 'type': 'repo',
147 'repo_id': obj.repo_id,
147 'repo_id': obj.repo_id,
148 'repo_type': obj.repo_type,
148 'repo_type': obj.repo_type,
149 'private': obj.private,
149 'private': obj.private,
150 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
150 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
151 }
151 }
152 for obj in acl_iter]
152 for obj in acl_iter]
153
153
154 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
154 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
155 org_query = name_contains
155 org_query = name_contains
156 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
156 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
157 ['group.read', 'group.write', 'group.admin'],
157 ['group.read', 'group.write', 'group.admin'],
158 cache=False, name_filter=name_contains) or [-1]
158 cache=False, name_filter=name_contains) or [-1]
159
159
160 query = RepoGroup.query()\
160 query = RepoGroup.query()\
161 .filter(or_(
161 .filter(or_(
162 # generate multiple IN to fix limitation problems
162 # generate multiple IN to fix limitation problems
163 *in_filter_generator(RepoGroup.group_id, allowed_ids)
163 *in_filter_generator(RepoGroup.group_id, allowed_ids)
164 ))
164 ))
165
165
166 query = query.order_by(case(
166 query = query.order_by(case(
167 [
167 [
168 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
168 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
169 ],
169 ],
170 ))
170 ))
171 query = query.order_by(func.length(RepoGroup.group_name))
171 query = query.order_by(func.length(RepoGroup.group_name))
172 query = query.order_by(RepoGroup.group_name)
172 query = query.order_by(RepoGroup.group_name)
173
173
174 if name_contains:
174 if name_contains:
175 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
175 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
176 query = query.filter(
176 query = query.filter(
177 RepoGroup.group_name.ilike(ilike_expression))
177 RepoGroup.group_name.ilike(ilike_expression))
178 query = query.limit(limit)
178 query = query.limit(limit)
179
179
180 acl_iter = query
180 acl_iter = query
181
181
182 return [
182 return [
183 {
183 {
184 'id': obj.group_name,
184 'id': obj.group_name,
185 'value': org_query,
185 'value': org_query,
186 'value_display': obj.group_name,
186 'value_display': obj.group_name,
187 'text': obj.group_name,
187 'text': obj.group_name,
188 'type': 'repo_group',
188 'type': 'repo_group',
189 'repo_group_id': obj.group_id,
189 'repo_group_id': obj.group_id,
190 'url': h.route_path(
190 'url': h.route_path(
191 'repo_group_home', repo_group_name=obj.group_name)
191 'repo_group_home', repo_group_name=obj.group_name)
192 }
192 }
193 for obj in acl_iter]
193 for obj in acl_iter]
194
194
195 def _get_user_list(self, name_contains=None, limit=20):
195 def _get_user_list(self, name_contains=None, limit=20):
196 org_query = name_contains
196 org_query = name_contains
197 if not name_contains:
197 if not name_contains:
198 return [], False
198 return [], False
199
199
200 # TODO(marcink): should all logged in users be allowed to search others?
200 # TODO(marcink): should all logged in users be allowed to search others?
201 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
201 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
202 if not allowed_user_search:
202 if not allowed_user_search:
203 return [], False
203 return [], False
204
204
205 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
205 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
206 if len(name_contains) != 1:
206 if len(name_contains) != 1:
207 return [], False
207 return [], False
208
208
209 name_contains = name_contains[0]
209 name_contains = name_contains[0]
210
210
211 query = User.query()\
211 query = User.query()\
212 .order_by(func.length(User.username))\
212 .order_by(func.length(User.username))\
213 .order_by(User.username) \
213 .order_by(User.username) \
214 .filter(User.username != User.DEFAULT_USER)
214 .filter(User.username != User.DEFAULT_USER)
215
215
216 if name_contains:
216 if name_contains:
217 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
217 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
218 query = query.filter(
218 query = query.filter(
219 User.username.ilike(ilike_expression))
219 User.username.ilike(ilike_expression))
220 query = query.limit(limit)
220 query = query.limit(limit)
221
221
222 acl_iter = query
222 acl_iter = query
223
223
224 return [
224 return [
225 {
225 {
226 'id': obj.user_id,
226 'id': obj.user_id,
227 'value': org_query,
227 'value': org_query,
228 'value_display': 'user: `{}`'.format(obj.username),
228 'value_display': 'user: `{}`'.format(obj.username),
229 'type': 'user',
229 'type': 'user',
230 'icon_link': h.gravatar_url(obj.email, 30),
230 'icon_link': h.gravatar_url(obj.email, 30),
231 'url': h.route_path(
231 'url': h.route_path(
232 'user_profile', username=obj.username)
232 'user_profile', username=obj.username)
233 }
233 }
234 for obj in acl_iter], True
234 for obj in acl_iter], True
235
235
236 def _get_user_groups_list(self, name_contains=None, limit=20):
236 def _get_user_groups_list(self, name_contains=None, limit=20):
237 org_query = name_contains
237 org_query = name_contains
238 if not name_contains:
238 if not name_contains:
239 return [], False
239 return [], False
240
240
241 # TODO(marcink): should all logged in users be allowed to search others?
241 # TODO(marcink): should all logged in users be allowed to search others?
242 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
242 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
243 if not allowed_user_search:
243 if not allowed_user_search:
244 return [], False
244 return [], False
245
245
246 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
246 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
247 if len(name_contains) != 1:
247 if len(name_contains) != 1:
248 return [], False
248 return [], False
249
249
250 name_contains = name_contains[0]
250 name_contains = name_contains[0]
251
251
252 query = UserGroup.query()\
252 query = UserGroup.query()\
253 .order_by(func.length(UserGroup.users_group_name))\
253 .order_by(func.length(UserGroup.users_group_name))\
254 .order_by(UserGroup.users_group_name)
254 .order_by(UserGroup.users_group_name)
255
255
256 if name_contains:
256 if name_contains:
257 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
257 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
258 query = query.filter(
258 query = query.filter(
259 UserGroup.users_group_name.ilike(ilike_expression))
259 UserGroup.users_group_name.ilike(ilike_expression))
260 query = query.limit(limit)
260 query = query.limit(limit)
261
261
262 acl_iter = query
262 acl_iter = query
263
263
264 return [
264 return [
265 {
265 {
266 'id': obj.users_group_id,
266 'id': obj.users_group_id,
267 'value': org_query,
267 'value': org_query,
268 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
268 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
269 'type': 'user_group',
269 'type': 'user_group',
270 'url': h.route_path(
270 'url': h.route_path(
271 'user_group_profile', user_group_name=obj.users_group_name)
271 'user_group_profile', user_group_name=obj.users_group_name)
272 }
272 }
273 for obj in acl_iter], True
273 for obj in acl_iter], True
274
274
275 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
275 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
276 repo_name = repo_group_name = None
276 repo_name = repo_group_name = None
277 if repo:
277 if repo:
278 repo_name = repo.repo_name
278 repo_name = repo.repo_name
279 if repo_group:
279 if repo_group:
280 repo_group_name = repo_group.group_name
280 repo_group_name = repo_group.group_name
281
281
282 org_query = query
282 org_query = query
283 if not query or len(query) < 3 or not searcher:
283 if not query or len(query) < 3 or not searcher:
284 return [], False
284 return [], False
285
285
286 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
286 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
287
287
288 if len(commit_hashes) != 1:
288 if len(commit_hashes) != 1:
289 return [], False
289 return [], False
290
290
291 commit_hash = commit_hashes[0]
291 commit_hash = commit_hashes[0]
292
292
293 result = searcher.search(
293 result = searcher.search(
294 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
294 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
295 repo_name, repo_group_name, raise_on_exc=False)
295 repo_name, repo_group_name, raise_on_exc=False)
296
296
297 commits = []
297 commits = []
298 for entry in result['results']:
298 for entry in result['results']:
299 repo_data = {
299 repo_data = {
300 'repository_id': entry.get('repository_id'),
300 'repository_id': entry.get('repository_id'),
301 'repository_type': entry.get('repo_type'),
301 'repository_type': entry.get('repo_type'),
302 'repository_name': entry.get('repository'),
302 'repository_name': entry.get('repository'),
303 }
303 }
304
304
305 commit_entry = {
305 commit_entry = {
306 'id': entry['commit_id'],
306 'id': entry['commit_id'],
307 'value': org_query,
307 'value': org_query,
308 'value_display': '`{}` commit: {}'.format(
308 'value_display': '`{}` commit: {}'.format(
309 entry['repository'], entry['commit_id']),
309 entry['repository'], entry['commit_id']),
310 'type': 'commit',
310 'type': 'commit',
311 'repo': entry['repository'],
311 'repo': entry['repository'],
312 'repo_data': repo_data,
312 'repo_data': repo_data,
313
313
314 'url': h.route_path(
314 'url': h.route_path(
315 'repo_commit',
315 'repo_commit',
316 repo_name=entry['repository'], commit_id=entry['commit_id'])
316 repo_name=entry['repository'], commit_id=entry['commit_id'])
317 }
317 }
318
318
319 commits.append(commit_entry)
319 commits.append(commit_entry)
320 return commits, True
320 return commits, True
321
321
322 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
322 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
323 repo_name = repo_group_name = None
323 repo_name = repo_group_name = None
324 if repo:
324 if repo:
325 repo_name = repo.repo_name
325 repo_name = repo.repo_name
326 if repo_group:
326 if repo_group:
327 repo_group_name = repo_group.group_name
327 repo_group_name = repo_group.group_name
328
328
329 org_query = query
329 org_query = query
330 if not query or len(query) < 3 or not searcher:
330 if not query or len(query) < 3 or not searcher:
331 return [], False
331 return [], False
332
332
333 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
333 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
334 if len(paths_re) != 1:
334 if len(paths_re) != 1:
335 return [], False
335 return [], False
336
336
337 file_path = paths_re[0]
337 file_path = paths_re[0]
338
338
339 search_path = searcher.escape_specials(file_path)
339 search_path = searcher.escape_specials(file_path)
340 result = searcher.search(
340 result = searcher.search(
341 'file.raw:*{}*'.format(search_path), 'path', auth_user,
341 'file.raw:*{}*'.format(search_path), 'path', auth_user,
342 repo_name, repo_group_name, raise_on_exc=False)
342 repo_name, repo_group_name, raise_on_exc=False)
343
343
344 files = []
344 files = []
345 for entry in result['results']:
345 for entry in result['results']:
346 repo_data = {
346 repo_data = {
347 'repository_id': entry.get('repository_id'),
347 'repository_id': entry.get('repository_id'),
348 'repository_type': entry.get('repo_type'),
348 'repository_type': entry.get('repo_type'),
349 'repository_name': entry.get('repository'),
349 'repository_name': entry.get('repository'),
350 }
350 }
351
351
352 file_entry = {
352 file_entry = {
353 'id': entry['commit_id'],
353 'id': entry['commit_id'],
354 'value': org_query,
354 'value': org_query,
355 'value_display': '`{}` file: {}'.format(
355 'value_display': '`{}` file: {}'.format(
356 entry['repository'], entry['file']),
356 entry['repository'], entry['file']),
357 'type': 'file',
357 'type': 'file',
358 'repo': entry['repository'],
358 'repo': entry['repository'],
359 'repo_data': repo_data,
359 'repo_data': repo_data,
360
360
361 'url': h.route_path(
361 'url': h.route_path(
362 'repo_files',
362 'repo_files',
363 repo_name=entry['repository'], commit_id=entry['commit_id'],
363 repo_name=entry['repository'], commit_id=entry['commit_id'],
364 f_path=entry['file'])
364 f_path=entry['file'])
365 }
365 }
366
366
367 files.append(file_entry)
367 files.append(file_entry)
368 return files, True
368 return files, True
369
369
370 @LoginRequired()
370 @LoginRequired()
371 @view_config(
371 @view_config(
372 route_name='repo_list_data', request_method='GET',
372 route_name='repo_list_data', request_method='GET',
373 renderer='json_ext', xhr=True)
373 renderer='json_ext', xhr=True)
374 def repo_list_data(self):
374 def repo_list_data(self):
375 _ = self.request.translate
375 _ = self.request.translate
376 self.load_default_context()
376 self.load_default_context()
377
377
378 query = self.request.GET.get('query')
378 query = self.request.GET.get('query')
379 repo_type = self.request.GET.get('repo_type')
379 repo_type = self.request.GET.get('repo_type')
380 log.debug('generating repo list, query:%s, repo_type:%s',
380 log.debug('generating repo list, query:%s, repo_type:%s',
381 query, repo_type)
381 query, repo_type)
382
382
383 res = []
383 res = []
384 repos = self._get_repo_list(query, repo_type=repo_type)
384 repos = self._get_repo_list(query, repo_type=repo_type)
385 if repos:
385 if repos:
386 res.append({
386 res.append({
387 'text': _('Repositories'),
387 'text': _('Repositories'),
388 'children': repos
388 'children': repos
389 })
389 })
390
390
391 data = {
391 data = {
392 'more': False,
392 'more': False,
393 'results': res
393 'results': res
394 }
394 }
395 return data
395 return data
396
396
397 @LoginRequired()
397 @LoginRequired()
398 @view_config(
398 @view_config(
399 route_name='repo_group_list_data', request_method='GET',
399 route_name='repo_group_list_data', request_method='GET',
400 renderer='json_ext', xhr=True)
400 renderer='json_ext', xhr=True)
401 def repo_group_list_data(self):
401 def repo_group_list_data(self):
402 _ = self.request.translate
402 _ = self.request.translate
403 self.load_default_context()
403 self.load_default_context()
404
404
405 query = self.request.GET.get('query')
405 query = self.request.GET.get('query')
406
406
407 log.debug('generating repo group list, query:%s',
407 log.debug('generating repo group list, query:%s',
408 query)
408 query)
409
409
410 res = []
410 res = []
411 repo_groups = self._get_repo_group_list(query)
411 repo_groups = self._get_repo_group_list(query)
412 if repo_groups:
412 if repo_groups:
413 res.append({
413 res.append({
414 'text': _('Repository Groups'),
414 'text': _('Repository Groups'),
415 'children': repo_groups
415 'children': repo_groups
416 })
416 })
417
417
418 data = {
418 data = {
419 'more': False,
419 'more': False,
420 'results': res
420 'results': res
421 }
421 }
422 return data
422 return data
423
423
424 def _get_default_search_queries(self, search_context, searcher, query):
424 def _get_default_search_queries(self, search_context, searcher, query):
425 if not searcher:
425 if not searcher:
426 return []
426 return []
427
427
428 is_es_6 = searcher.is_es_6
428 is_es_6 = searcher.is_es_6
429
429
430 queries = []
430 queries = []
431 repo_group_name, repo_name, repo_context = None, None, None
431 repo_group_name, repo_name, repo_context = None, None, None
432
432
433 # repo group context
433 # repo group context
434 if search_context.get('search_context[repo_group_name]'):
434 if search_context.get('search_context[repo_group_name]'):
435 repo_group_name = search_context.get('search_context[repo_group_name]')
435 repo_group_name = search_context.get('search_context[repo_group_name]')
436 if search_context.get('search_context[repo_name]'):
436 if search_context.get('search_context[repo_name]'):
437 repo_name = search_context.get('search_context[repo_name]')
437 repo_name = search_context.get('search_context[repo_name]')
438 repo_context = search_context.get('search_context[repo_view_type]')
438 repo_context = search_context.get('search_context[repo_view_type]')
439
439
440 if is_es_6 and repo_name:
440 if is_es_6 and repo_name:
441 # files
441 # files
442 def query_modifier():
442 def query_modifier():
443 qry = query
443 qry = query
444 return {'q': qry, 'type': 'content'}
444 return {'q': qry, 'type': 'content'}
445 label = u'File search for `{}` in this repository.'.format(query)
445 label = u'File search for `{}` in this repository.'.format(query)
446 file_qry = {
446 file_qry = {
447 'id': -10,
447 'id': -10,
448 'value': query,
448 'value': query,
449 'value_display': label,
449 'value_display': label,
450 'type': 'search',
450 'type': 'search',
451 'url': h.route_path('search_repo',
451 'url': h.route_path('search_repo',
452 repo_name=repo_name,
452 repo_name=repo_name,
453 _query=query_modifier())
453 _query=query_modifier())
454 }
454 }
455
455
456 # commits
456 # commits
457 def query_modifier():
457 def query_modifier():
458 qry = query
458 qry = query
459 return {'q': qry, 'type': 'commit'}
459 return {'q': qry, 'type': 'commit'}
460
460
461 label = u'Commit search for `{}` in this repository.'.format(query)
461 label = u'Commit search for `{}` in this repository.'.format(query)
462 commit_qry = {
462 commit_qry = {
463 'id': -20,
463 'id': -20,
464 'value': query,
464 'value': query,
465 'value_display': label,
465 'value_display': label,
466 'type': 'search',
466 'type': 'search',
467 'url': h.route_path('search_repo',
467 'url': h.route_path('search_repo',
468 repo_name=repo_name,
468 repo_name=repo_name,
469 _query=query_modifier())
469 _query=query_modifier())
470 }
470 }
471
471
472 if repo_context in ['commit', 'changelog']:
472 if repo_context in ['commit', 'changelog']:
473 queries.extend([commit_qry, file_qry])
473 queries.extend([commit_qry, file_qry])
474 elif repo_context in ['files', 'summary']:
474 elif repo_context in ['files', 'summary']:
475 queries.extend([file_qry, commit_qry])
475 queries.extend([file_qry, commit_qry])
476 else:
476 else:
477 queries.extend([commit_qry, file_qry])
477 queries.extend([commit_qry, file_qry])
478
478
479 elif is_es_6 and repo_group_name:
479 elif is_es_6 and repo_group_name:
480 # files
480 # files
481 def query_modifier():
481 def query_modifier():
482 qry = query
482 qry = query
483 return {'q': qry, 'type': 'content'}
483 return {'q': qry, 'type': 'content'}
484
484
485 label = u'File search for `{}` in this repository group'.format(query)
485 label = u'File search for `{}` in this repository group'.format(query)
486 file_qry = {
486 file_qry = {
487 'id': -30,
487 'id': -30,
488 'value': query,
488 'value': query,
489 'value_display': label,
489 'value_display': label,
490 'type': 'search',
490 'type': 'search',
491 'url': h.route_path('search_repo_group',
491 'url': h.route_path('search_repo_group',
492 repo_group_name=repo_group_name,
492 repo_group_name=repo_group_name,
493 _query=query_modifier())
493 _query=query_modifier())
494 }
494 }
495
495
496 # commits
496 # commits
497 def query_modifier():
497 def query_modifier():
498 qry = query
498 qry = query
499 return {'q': qry, 'type': 'commit'}
499 return {'q': qry, 'type': 'commit'}
500
500
501 label = u'Commit search for `{}` in this repository group'.format(query)
501 label = u'Commit search for `{}` in this repository group'.format(query)
502 commit_qry = {
502 commit_qry = {
503 'id': -40,
503 'id': -40,
504 'value': query,
504 'value': query,
505 'value_display': label,
505 'value_display': label,
506 'type': 'search',
506 'type': 'search',
507 'url': h.route_path('search_repo_group',
507 'url': h.route_path('search_repo_group',
508 repo_group_name=repo_group_name,
508 repo_group_name=repo_group_name,
509 _query=query_modifier())
509 _query=query_modifier())
510 }
510 }
511
511
512 if repo_context in ['commit', 'changelog']:
512 if repo_context in ['commit', 'changelog']:
513 queries.extend([commit_qry, file_qry])
513 queries.extend([commit_qry, file_qry])
514 elif repo_context in ['files', 'summary']:
514 elif repo_context in ['files', 'summary']:
515 queries.extend([file_qry, commit_qry])
515 queries.extend([file_qry, commit_qry])
516 else:
516 else:
517 queries.extend([commit_qry, file_qry])
517 queries.extend([commit_qry, file_qry])
518
518
519 # Global, not scoped
519 # Global, not scoped
520 if not queries:
520 if not queries:
521 queries.append(
521 queries.append(
522 {
522 {
523 'id': -1,
523 'id': -1,
524 'value': query,
524 'value': query,
525 'value_display': u'File search for: `{}`'.format(query),
525 'value_display': u'File search for: `{}`'.format(query),
526 'type': 'search',
526 'type': 'search',
527 'url': h.route_path('search',
527 'url': h.route_path('search',
528 _query={'q': query, 'type': 'content'})
528 _query={'q': query, 'type': 'content'})
529 })
529 })
530 queries.append(
530 queries.append(
531 {
531 {
532 'id': -2,
532 'id': -2,
533 'value': query,
533 'value': query,
534 'value_display': u'Commit search for: `{}`'.format(query),
534 'value_display': u'Commit search for: `{}`'.format(query),
535 'type': 'search',
535 'type': 'search',
536 'url': h.route_path('search',
536 'url': h.route_path('search',
537 _query={'q': query, 'type': 'commit'})
537 _query={'q': query, 'type': 'commit'})
538 })
538 })
539
539
540 return queries
540 return queries
541
541
542 @LoginRequired()
542 @LoginRequired()
543 @view_config(
543 @view_config(
544 route_name='goto_switcher_data', request_method='GET',
544 route_name='goto_switcher_data', request_method='GET',
545 renderer='json_ext', xhr=True)
545 renderer='json_ext', xhr=True)
546 def goto_switcher_data(self):
546 def goto_switcher_data(self):
547 c = self.load_default_context()
547 c = self.load_default_context()
548
548
549 _ = self.request.translate
549 _ = self.request.translate
550
550
551 query = self.request.GET.get('query')
551 query = self.request.GET.get('query')
552 log.debug('generating main filter data, query %s', query)
552 log.debug('generating main filter data, query %s', query)
553
553
554 res = []
554 res = []
555 if not query:
555 if not query:
556 return {'suggestions': res}
556 return {'suggestions': res}
557
557
558 def no_match(name):
558 def no_match(name):
559 return {
559 return {
560 'id': -1,
560 'id': -1,
561 'value': "",
561 'value': "",
562 'value_display': name,
562 'value_display': name,
563 'type': 'text',
563 'type': 'text',
564 'url': ""
564 'url': ""
565 }
565 }
566 searcher = searcher_from_config(self.request.registry.settings)
566 searcher = searcher_from_config(self.request.registry.settings)
567 has_specialized_search = False
567 has_specialized_search = False
568
568
569 # set repo context
569 # set repo context
570 repo = None
570 repo = None
571 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
571 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
572 if repo_id:
572 if repo_id:
573 repo = Repository.get(repo_id)
573 repo = Repository.get(repo_id)
574
574
575 # set group context
575 # set group context
576 repo_group = None
576 repo_group = None
577 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
577 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
578 if repo_group_id:
578 if repo_group_id:
579 repo_group = RepoGroup.get(repo_group_id)
579 repo_group = RepoGroup.get(repo_group_id)
580 prefix_match = False
580 prefix_match = False
581
581
582 # user: type search
582 # user: type search
583 if not prefix_match:
583 if not prefix_match:
584 users, prefix_match = self._get_user_list(query)
584 users, prefix_match = self._get_user_list(query)
585 if users:
585 if users:
586 has_specialized_search = True
586 has_specialized_search = True
587 for serialized_user in users:
587 for serialized_user in users:
588 res.append(serialized_user)
588 res.append(serialized_user)
589 elif prefix_match:
589 elif prefix_match:
590 has_specialized_search = True
590 has_specialized_search = True
591 res.append(no_match('No matching users found'))
591 res.append(no_match('No matching users found'))
592
592
593 # user_group: type search
593 # user_group: type search
594 if not prefix_match:
594 if not prefix_match:
595 user_groups, prefix_match = self._get_user_groups_list(query)
595 user_groups, prefix_match = self._get_user_groups_list(query)
596 if user_groups:
596 if user_groups:
597 has_specialized_search = True
597 has_specialized_search = True
598 for serialized_user_group in user_groups:
598 for serialized_user_group in user_groups:
599 res.append(serialized_user_group)
599 res.append(serialized_user_group)
600 elif prefix_match:
600 elif prefix_match:
601 has_specialized_search = True
601 has_specialized_search = True
602 res.append(no_match('No matching user groups found'))
602 res.append(no_match('No matching user groups found'))
603
603
604 # FTS commit: type search
604 # FTS commit: type search
605 if not prefix_match:
605 if not prefix_match:
606 commits, prefix_match = self._get_hash_commit_list(
606 commits, prefix_match = self._get_hash_commit_list(
607 c.auth_user, searcher, query, repo, repo_group)
607 c.auth_user, searcher, query, repo, repo_group)
608 if commits:
608 if commits:
609 has_specialized_search = True
609 has_specialized_search = True
610 unique_repos = collections.OrderedDict()
610 unique_repos = collections.OrderedDict()
611 for commit in commits:
611 for commit in commits:
612 repo_name = commit['repo']
612 repo_name = commit['repo']
613 unique_repos.setdefault(repo_name, []).append(commit)
613 unique_repos.setdefault(repo_name, []).append(commit)
614
614
615 for _repo, commits in unique_repos.items():
615 for _repo, commits in unique_repos.items():
616 for commit in commits:
616 for commit in commits:
617 res.append(commit)
617 res.append(commit)
618 elif prefix_match:
618 elif prefix_match:
619 has_specialized_search = True
619 has_specialized_search = True
620 res.append(no_match('No matching commits found'))
620 res.append(no_match('No matching commits found'))
621
621
622 # FTS file: type search
622 # FTS file: type search
623 if not prefix_match:
623 if not prefix_match:
624 paths, prefix_match = self._get_path_list(
624 paths, prefix_match = self._get_path_list(
625 c.auth_user, searcher, query, repo, repo_group)
625 c.auth_user, searcher, query, repo, repo_group)
626 if paths:
626 if paths:
627 has_specialized_search = True
627 has_specialized_search = True
628 unique_repos = collections.OrderedDict()
628 unique_repos = collections.OrderedDict()
629 for path in paths:
629 for path in paths:
630 repo_name = path['repo']
630 repo_name = path['repo']
631 unique_repos.setdefault(repo_name, []).append(path)
631 unique_repos.setdefault(repo_name, []).append(path)
632
632
633 for repo, paths in unique_repos.items():
633 for repo, paths in unique_repos.items():
634 for path in paths:
634 for path in paths:
635 res.append(path)
635 res.append(path)
636 elif prefix_match:
636 elif prefix_match:
637 has_specialized_search = True
637 has_specialized_search = True
638 res.append(no_match('No matching files found'))
638 res.append(no_match('No matching files found'))
639
639
640 # main suggestions
640 # main suggestions
641 if not has_specialized_search:
641 if not has_specialized_search:
642 repo_group_name = ''
642 repo_group_name = ''
643 if repo_group:
643 if repo_group:
644 repo_group_name = repo_group.group_name
644 repo_group_name = repo_group.group_name
645
645
646 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
646 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
647 res.append(_q)
647 res.append(_q)
648
648
649 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
649 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
650 for serialized_repo_group in repo_groups:
650 for serialized_repo_group in repo_groups:
651 res.append(serialized_repo_group)
651 res.append(serialized_repo_group)
652
652
653 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
653 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
654 for serialized_repo in repos:
654 for serialized_repo in repos:
655 res.append(serialized_repo)
655 res.append(serialized_repo)
656
656
657 if not repos and not repo_groups:
657 if not repos and not repo_groups:
658 res.append(no_match('No matches found'))
658 res.append(no_match('No matches found'))
659
659
660 return {'suggestions': res}
660 return {'suggestions': res}
661
661
662 def _get_groups_and_repos(self, repo_group_id=None):
662 def _get_groups_and_repos(self, repo_group_id=None):
663 # repo groups groups
663 # repo groups groups
664 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
664 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
665 _perms = ['group.read', 'group.write', 'group.admin']
665 _perms = ['group.read', 'group.write', 'group.admin']
666 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
666 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
667 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
667 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
668 repo_group_list=repo_group_list_acl, admin=False)
668 repo_group_list=repo_group_list_acl, admin=False)
669
669
670 # repositories
670 # repositories
671 repo_list = Repository.get_all_repos(group_id=repo_group_id)
671 repo_list = Repository.get_all_repos(group_id=repo_group_id)
672 _perms = ['repository.read', 'repository.write', 'repository.admin']
672 _perms = ['repository.read', 'repository.write', 'repository.admin']
673 repo_list_acl = RepoList(repo_list, perm_set=_perms)
673 repo_list_acl = RepoList(repo_list, perm_set=_perms)
674 repo_data = RepoModel().get_repos_as_dict(
674 repo_data = RepoModel().get_repos_as_dict(
675 repo_list=repo_list_acl, admin=False)
675 repo_list=repo_list_acl, admin=False)
676
676
677 return repo_data, repo_group_data
677 return repo_data, repo_group_data
678
678
679 @LoginRequired()
679 @LoginRequired()
680 @view_config(
680 @view_config(
681 route_name='home', request_method='GET',
681 route_name='home', request_method='GET',
682 renderer='rhodecode:templates/index.mako')
682 renderer='rhodecode:templates/index.mako')
683 def main_page(self):
683 def main_page(self):
684 c = self.load_default_context()
684 c = self.load_default_context()
685 c.repo_group = None
685 c.repo_group = None
686
686
687 repo_data, repo_group_data = self._get_groups_and_repos()
687 repo_data, repo_group_data = self._get_groups_and_repos()
688 # json used to render the grids
688 # json used to render the grids
689 c.repos_data = json.dumps(repo_data)
689 c.repos_data = json.dumps(repo_data)
690 c.repo_groups_data = json.dumps(repo_group_data)
690 c.repo_groups_data = json.dumps(repo_group_data)
691
691
692 return self._get_template_context(c)
692 return self._get_template_context(c)
693
693
694 @LoginRequired()
694 @LoginRequired()
695 @HasRepoGroupPermissionAnyDecorator(
695 @HasRepoGroupPermissionAnyDecorator(
696 'group.read', 'group.write', 'group.admin')
696 'group.read', 'group.write', 'group.admin')
697 @view_config(
697 @view_config(
698 route_name='repo_group_home', request_method='GET',
698 route_name='repo_group_home', request_method='GET',
699 renderer='rhodecode:templates/index_repo_group.mako')
699 renderer='rhodecode:templates/index_repo_group.mako')
700 @view_config(
700 @view_config(
701 route_name='repo_group_home_slash', request_method='GET',
701 route_name='repo_group_home_slash', request_method='GET',
702 renderer='rhodecode:templates/index_repo_group.mako')
702 renderer='rhodecode:templates/index_repo_group.mako')
703 def repo_group_main_page(self):
703 def repo_group_main_page(self):
704 c = self.load_default_context()
704 c = self.load_default_context()
705 c.repo_group = self.request.db_repo_group
705 c.repo_group = self.request.db_repo_group
706 repo_data, repo_group_data = self._get_groups_and_repos(
706 repo_data, repo_group_data = self._get_groups_and_repos(
707 c.repo_group.group_id)
707 c.repo_group.group_id)
708
708
709 # json used to render the grids
709 # json used to render the grids
710 c.repos_data = json.dumps(repo_data)
710 c.repos_data = json.dumps(repo_data)
711 c.repo_groups_data = json.dumps(repo_group_data)
711 c.repo_groups_data = json.dumps(repo_group_data)
712
712
713 return self._get_template_context(c)
713 return self._get_template_context(c)
714
714
715 @LoginRequired()
715 @LoginRequired()
716 @CSRFRequired()
716 @CSRFRequired()
717 @view_config(
717 @view_config(
718 route_name='markup_preview', request_method='POST',
718 route_name='markup_preview', request_method='POST',
719 renderer='string', xhr=True)
719 renderer='string', xhr=True)
720 def markup_preview(self):
720 def markup_preview(self):
721 # Technically a CSRF token is not needed as no state changes with this
721 # Technically a CSRF token is not needed as no state changes with this
722 # call. However, as this is a POST is better to have it, so automated
722 # call. However, as this is a POST is better to have it, so automated
723 # tools don't flag it as potential CSRF.
723 # tools don't flag it as potential CSRF.
724 # Post is required because the payload could be bigger than the maximum
724 # Post is required because the payload could be bigger than the maximum
725 # allowed by GET.
725 # allowed by GET.
726
726
727 text = self.request.POST.get('text')
727 text = self.request.POST.get('text')
728 renderer = self.request.POST.get('renderer') or 'rst'
728 renderer = self.request.POST.get('renderer') or 'rst'
729 if text:
729 if text:
730 return h.render(text, renderer=renderer, mentions=True)
730 return h.render(text, renderer=renderer, mentions=True)
731 return ''
731 return ''
732
732
733 @LoginRequired()
733 @LoginRequired()
734 @CSRFRequired()
734 @CSRFRequired()
735 @view_config(
735 @view_config(
736 route_name='store_user_session_value', request_method='POST',
736 route_name='store_user_session_value', request_method='POST',
737 renderer='string', xhr=True)
737 renderer='string', xhr=True)
738 def store_user_session_attr(self):
738 def store_user_session_attr(self):
739 key = self.request.POST.get('key')
739 key = self.request.POST.get('key')
740 val = self.request.POST.get('val')
740 val = self.request.POST.get('val')
741
741
742 existing_value = self.request.session.get(key)
742 existing_value = self.request.session.get(key)
743 if existing_value != val:
743 if existing_value != val:
744 self.request.session[key] = val
744 self.request.session[key] = val
745
745
746 return 'stored:{}'.format(key)
746 return 'stored:{}:{}'.format(key, val)
@@ -1,836 +1,838 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 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 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
84 if (!(this instanceof CommentForm)) {
84 if (!(this instanceof CommentForm)) {
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 }
86 }
87
87
88 // bind the element instance to our Form
88 // bind the element instance to our Form
89 $(formElement).get(0).CommentForm = this;
89 $(formElement).get(0).CommentForm = this;
90
90
91 this.withLineNo = function(selector) {
91 this.withLineNo = function(selector) {
92 var lineNo = this.lineNo;
92 var lineNo = this.lineNo;
93 if (lineNo === undefined) {
93 if (lineNo === undefined) {
94 return selector
94 return selector
95 } else {
95 } else {
96 return selector + '_' + lineNo;
96 return selector + '_' + lineNo;
97 }
97 }
98 };
98 };
99
99
100 this.commitId = commitId;
100 this.commitId = commitId;
101 this.pullRequestId = pullRequestId;
101 this.pullRequestId = pullRequestId;
102 this.lineNo = lineNo;
102 this.lineNo = lineNo;
103 this.initAutocompleteActions = initAutocompleteActions;
103 this.initAutocompleteActions = initAutocompleteActions;
104
104
105 this.previewButton = this.withLineNo('#preview-btn');
105 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewContainer = this.withLineNo('#preview-container');
106 this.previewContainer = this.withLineNo('#preview-container');
107
107
108 this.previewBoxSelector = this.withLineNo('#preview-box');
108 this.previewBoxSelector = this.withLineNo('#preview-box');
109
109
110 this.editButton = this.withLineNo('#edit-btn');
110 this.editButton = this.withLineNo('#edit-btn');
111 this.editContainer = this.withLineNo('#edit-container');
111 this.editContainer = this.withLineNo('#edit-container');
112 this.cancelButton = this.withLineNo('#cancel-btn');
112 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.commentType = this.withLineNo('#comment_type');
113 this.commentType = this.withLineNo('#comment_type');
114
114
115 this.resolvesId = null;
115 this.resolvesId = null;
116 this.resolvesActionId = null;
116 this.resolvesActionId = null;
117
117
118 this.closesPr = '#close_pull_request';
118 this.closesPr = '#close_pull_request';
119
119
120 this.cmBox = this.withLineNo('#text');
120 this.cmBox = this.withLineNo('#text');
121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122
122
123 this.statusChange = this.withLineNo('#change_status');
123 this.statusChange = this.withLineNo('#change_status');
124
124
125 this.submitForm = formElement;
125 this.submitForm = formElement;
126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 this.submitButtonText = this.submitButton.val();
127 this.submitButtonText = this.submitButton.val();
128
128
129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 {'repo_name': templateContext.repo_name,
130 {'repo_name': templateContext.repo_name,
131 'commit_id': templateContext.commit_data.commit_id});
131 'commit_id': templateContext.commit_data.commit_id});
132
132
133 if (resolvesCommentId){
133 if (resolvesCommentId){
134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
136 $(this.commentType).prop('disabled', true);
136 $(this.commentType).prop('disabled', true);
137 $(this.commentType).addClass('disabled');
137 $(this.commentType).addClass('disabled');
138
138
139 // disable select
139 // disable select
140 setTimeout(function() {
140 setTimeout(function() {
141 $(self.statusChange).select2('readonly', true);
141 $(self.statusChange).select2('readonly', true);
142 }, 10);
142 }, 10);
143
143
144 var resolvedInfo = (
144 var resolvedInfo = (
145 '<li class="resolve-action">' +
145 '<li class="resolve-action">' +
146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
148 '</li>'
148 '</li>'
149 ).format(resolvesCommentId, _gettext('resolve comment'));
149 ).format(resolvesCommentId, _gettext('resolve comment'));
150 $(resolvedInfo).insertAfter($(this.commentType).parent());
150 $(resolvedInfo).insertAfter($(this.commentType).parent());
151 }
151 }
152
152
153 // based on commitId, or pullRequestId decide where do we submit
153 // based on commitId, or pullRequestId decide where do we submit
154 // out data
154 // out data
155 if (this.commitId){
155 if (this.commitId){
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
157 {'repo_name': templateContext.repo_name,
157 {'repo_name': templateContext.repo_name,
158 'commit_id': this.commitId});
158 'commit_id': this.commitId});
159 this.selfUrl = pyroutes.url('repo_commit',
159 this.selfUrl = pyroutes.url('repo_commit',
160 {'repo_name': templateContext.repo_name,
160 {'repo_name': templateContext.repo_name,
161 'commit_id': this.commitId});
161 'commit_id': this.commitId});
162
162
163 } else if (this.pullRequestId) {
163 } else if (this.pullRequestId) {
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
165 {'repo_name': templateContext.repo_name,
165 {'repo_name': templateContext.repo_name,
166 'pull_request_id': this.pullRequestId});
166 'pull_request_id': this.pullRequestId});
167 this.selfUrl = pyroutes.url('pullrequest_show',
167 this.selfUrl = pyroutes.url('pullrequest_show',
168 {'repo_name': templateContext.repo_name,
168 {'repo_name': templateContext.repo_name,
169 'pull_request_id': this.pullRequestId});
169 'pull_request_id': this.pullRequestId});
170
170
171 } else {
171 } else {
172 throw new Error(
172 throw new Error(
173 'CommentForm requires pullRequestId, or commitId to be specified.')
173 'CommentForm requires pullRequestId, or commitId to be specified.')
174 }
174 }
175
175
176 // FUNCTIONS and helpers
176 // FUNCTIONS and helpers
177 var self = this;
177 var self = this;
178
178
179 this.isInline = function(){
179 this.isInline = function(){
180 return this.lineNo && this.lineNo != 'general';
180 return this.lineNo && this.lineNo != 'general';
181 };
181 };
182
182
183 this.getCmInstance = function(){
183 this.getCmInstance = function(){
184 return this.cm
184 return this.cm
185 };
185 };
186
186
187 this.setPlaceholder = function(placeholder) {
187 this.setPlaceholder = function(placeholder) {
188 var cm = this.getCmInstance();
188 var cm = this.getCmInstance();
189 if (cm){
189 if (cm){
190 cm.setOption('placeholder', placeholder);
190 cm.setOption('placeholder', placeholder);
191 }
191 }
192 };
192 };
193
193
194 this.getCommentStatus = function() {
194 this.getCommentStatus = function() {
195 return $(this.submitForm).find(this.statusChange).val();
195 return $(this.submitForm).find(this.statusChange).val();
196 };
196 };
197 this.getCommentType = function() {
197 this.getCommentType = function() {
198 return $(this.submitForm).find(this.commentType).val();
198 return $(this.submitForm).find(this.commentType).val();
199 };
199 };
200
200
201 this.getResolvesId = function() {
201 this.getResolvesId = function() {
202 return $(this.submitForm).find(this.resolvesId).val() || null;
202 return $(this.submitForm).find(this.resolvesId).val() || null;
203 };
203 };
204
204
205 this.getClosePr = function() {
205 this.getClosePr = function() {
206 return $(this.submitForm).find(this.closesPr).val() || null;
206 return $(this.submitForm).find(this.closesPr).val() || null;
207 };
207 };
208
208
209 this.markCommentResolved = function(resolvedCommentId){
209 this.markCommentResolved = function(resolvedCommentId){
210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
212 };
212 };
213
213
214 this.isAllowedToSubmit = function() {
214 this.isAllowedToSubmit = function() {
215 return !$(this.submitButton).prop('disabled');
215 return !$(this.submitButton).prop('disabled');
216 };
216 };
217
217
218 this.initStatusChangeSelector = function(){
218 this.initStatusChangeSelector = function(){
219 var formatChangeStatus = function(state, escapeMarkup) {
219 var formatChangeStatus = function(state, escapeMarkup) {
220 var originalOption = state.element;
220 var originalOption = state.element;
221 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
221 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
222 '<span>' + escapeMarkup(state.text) + '</span>';
222 '<span>' + escapeMarkup(state.text) + '</span>';
223 };
223 };
224 var formatResult = function(result, container, query, escapeMarkup) {
224 var formatResult = function(result, container, query, escapeMarkup) {
225 return formatChangeStatus(result, escapeMarkup);
225 return formatChangeStatus(result, escapeMarkup);
226 };
226 };
227
227
228 var formatSelection = function(data, container, escapeMarkup) {
228 var formatSelection = function(data, container, escapeMarkup) {
229 return formatChangeStatus(data, escapeMarkup);
229 return formatChangeStatus(data, escapeMarkup);
230 };
230 };
231
231
232 $(this.submitForm).find(this.statusChange).select2({
232 $(this.submitForm).find(this.statusChange).select2({
233 placeholder: _gettext('Status Review'),
233 placeholder: _gettext('Status Review'),
234 formatResult: formatResult,
234 formatResult: formatResult,
235 formatSelection: formatSelection,
235 formatSelection: formatSelection,
236 containerCssClass: "drop-menu status_box_menu",
236 containerCssClass: "drop-menu status_box_menu",
237 dropdownCssClass: "drop-menu-dropdown",
237 dropdownCssClass: "drop-menu-dropdown",
238 dropdownAutoWidth: true,
238 dropdownAutoWidth: true,
239 minimumResultsForSearch: -1
239 minimumResultsForSearch: -1
240 });
240 });
241 $(this.submitForm).find(this.statusChange).on('change', function() {
241 $(this.submitForm).find(this.statusChange).on('change', function() {
242 var status = self.getCommentStatus();
242 var status = self.getCommentStatus();
243
243
244 if (status && !self.isInline()) {
244 if (status && !self.isInline()) {
245 $(self.submitButton).prop('disabled', false);
245 $(self.submitButton).prop('disabled', false);
246 }
246 }
247
247
248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
249 self.setPlaceholder(placeholderText)
249 self.setPlaceholder(placeholderText)
250 })
250 })
251 };
251 };
252
252
253 // reset the comment form into it's original state
253 // reset the comment form into it's original state
254 this.resetCommentFormState = function(content) {
254 this.resetCommentFormState = function(content) {
255 content = content || '';
255 content = content || '';
256
256
257 $(this.editContainer).show();
257 $(this.editContainer).show();
258 $(this.editButton).parent().addClass('active');
258 $(this.editButton).parent().addClass('active');
259
259
260 $(this.previewContainer).hide();
260 $(this.previewContainer).hide();
261 $(this.previewButton).parent().removeClass('active');
261 $(this.previewButton).parent().removeClass('active');
262
262
263 this.setActionButtonsDisabled(true);
263 this.setActionButtonsDisabled(true);
264 self.cm.setValue(content);
264 self.cm.setValue(content);
265 self.cm.setOption("readOnly", false);
265 self.cm.setOption("readOnly", false);
266
266
267 if (this.resolvesId) {
267 if (this.resolvesId) {
268 // destroy the resolve action
268 // destroy the resolve action
269 $(this.resolvesId).parent().remove();
269 $(this.resolvesId).parent().remove();
270 }
270 }
271 // reset closingPR flag
271 // reset closingPR flag
272 $('.close-pr-input').remove();
272 $('.close-pr-input').remove();
273
273
274 $(this.statusChange).select2('readonly', false);
274 $(this.statusChange).select2('readonly', false);
275 };
275 };
276
276
277 this.globalSubmitSuccessCallback = function(){
277 this.globalSubmitSuccessCallback = function(){
278 // default behaviour is to call GLOBAL hook, if it's registered.
278 // default behaviour is to call GLOBAL hook, if it's registered.
279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 commentFormGlobalSubmitSuccessCallback()
280 commentFormGlobalSubmitSuccessCallback()
281 }
281 }
282 };
282 };
283
283
284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
286 };
286 };
287
287
288 // overwrite a submitHandler, we need to do it for inline comments
288 // overwrite a submitHandler, we need to do it for inline comments
289 this.setHandleFormSubmit = function(callback) {
289 this.setHandleFormSubmit = function(callback) {
290 this.handleFormSubmit = callback;
290 this.handleFormSubmit = callback;
291 };
291 };
292
292
293 // overwrite a submitSuccessHandler
293 // overwrite a submitSuccessHandler
294 this.setGlobalSubmitSuccessCallback = function(callback) {
294 this.setGlobalSubmitSuccessCallback = function(callback) {
295 this.globalSubmitSuccessCallback = callback;
295 this.globalSubmitSuccessCallback = callback;
296 };
296 };
297
297
298 // default handler for for submit for main comments
298 // default handler for for submit for main comments
299 this.handleFormSubmit = function() {
299 this.handleFormSubmit = function() {
300 var text = self.cm.getValue();
300 var text = self.cm.getValue();
301 var status = self.getCommentStatus();
301 var status = self.getCommentStatus();
302 var commentType = self.getCommentType();
302 var commentType = self.getCommentType();
303 var resolvesCommentId = self.getResolvesId();
303 var resolvesCommentId = self.getResolvesId();
304 var closePullRequest = self.getClosePr();
304 var closePullRequest = self.getClosePr();
305
305
306 if (text === "" && !status) {
306 if (text === "" && !status) {
307 return;
307 return;
308 }
308 }
309
309
310 var excludeCancelBtn = false;
310 var excludeCancelBtn = false;
311 var submitEvent = true;
311 var submitEvent = true;
312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
313 self.cm.setOption("readOnly", true);
313 self.cm.setOption("readOnly", true);
314
314
315 var postData = {
315 var postData = {
316 'text': text,
316 'text': text,
317 'changeset_status': status,
317 'changeset_status': status,
318 'comment_type': commentType,
318 'comment_type': commentType,
319 'csrf_token': CSRF_TOKEN
319 'csrf_token': CSRF_TOKEN
320 };
320 };
321
321
322 if (resolvesCommentId) {
322 if (resolvesCommentId) {
323 postData['resolves_comment_id'] = resolvesCommentId;
323 postData['resolves_comment_id'] = resolvesCommentId;
324 }
324 }
325
325
326 if (closePullRequest) {
326 if (closePullRequest) {
327 postData['close_pull_request'] = true;
327 postData['close_pull_request'] = true;
328 }
328 }
329
329
330 var submitSuccessCallback = function(o) {
330 var submitSuccessCallback = function(o) {
331 // reload page if we change status for single commit.
331 // reload page if we change status for single commit.
332 if (status && self.commitId) {
332 if (status && self.commitId) {
333 location.reload(true);
333 location.reload(true);
334 } else {
334 } else {
335 $('#injected_page_comments').append(o.rendered_text);
335 $('#injected_page_comments').append(o.rendered_text);
336 self.resetCommentFormState();
336 self.resetCommentFormState();
337 timeagoActivate();
337 timeagoActivate();
338
338
339 // mark visually which comment was resolved
339 // mark visually which comment was resolved
340 if (resolvesCommentId) {
340 if (resolvesCommentId) {
341 self.markCommentResolved(resolvesCommentId);
341 self.markCommentResolved(resolvesCommentId);
342 }
342 }
343 }
343 }
344
344
345 // run global callback on submit
345 // run global callback on submit
346 self.globalSubmitSuccessCallback();
346 self.globalSubmitSuccessCallback();
347
347
348 };
348 };
349 var submitFailCallback = function(data) {
349 var submitFailCallback = function(data) {
350 alert(
350 alert(
351 "Error while submitting comment.\n" +
351 "Error while submitting comment.\n" +
352 "Error code {0} ({1}).".format(data.status, data.statusText)
352 "Error code {0} ({1}).".format(data.status, data.statusText)
353 );
353 );
354 self.resetCommentFormState(text);
354 self.resetCommentFormState(text);
355 };
355 };
356 self.submitAjaxPOST(
356 self.submitAjaxPOST(
357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
358 };
358 };
359
359
360 this.previewSuccessCallback = function(o) {
360 this.previewSuccessCallback = function(o) {
361 $(self.previewBoxSelector).html(o);
361 $(self.previewBoxSelector).html(o);
362 $(self.previewBoxSelector).removeClass('unloaded');
362 $(self.previewBoxSelector).removeClass('unloaded');
363
363
364 // swap buttons, making preview active
364 // swap buttons, making preview active
365 $(self.previewButton).parent().addClass('active');
365 $(self.previewButton).parent().addClass('active');
366 $(self.editButton).parent().removeClass('active');
366 $(self.editButton).parent().removeClass('active');
367
367
368 // unlock buttons
368 // unlock buttons
369 self.setActionButtonsDisabled(false);
369 self.setActionButtonsDisabled(false);
370 };
370 };
371
371
372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
373 excludeCancelBtn = excludeCancelBtn || false;
373 excludeCancelBtn = excludeCancelBtn || false;
374 submitEvent = submitEvent || false;
374 submitEvent = submitEvent || false;
375
375
376 $(this.editButton).prop('disabled', state);
376 $(this.editButton).prop('disabled', state);
377 $(this.previewButton).prop('disabled', state);
377 $(this.previewButton).prop('disabled', state);
378
378
379 if (!excludeCancelBtn) {
379 if (!excludeCancelBtn) {
380 $(this.cancelButton).prop('disabled', state);
380 $(this.cancelButton).prop('disabled', state);
381 }
381 }
382
382
383 var submitState = state;
383 var submitState = state;
384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
385 // if the value of commit review status is set, we allow
385 // if the value of commit review status is set, we allow
386 // submit button, but only on Main form, isInline means inline
386 // submit button, but only on Main form, isInline means inline
387 submitState = false
387 submitState = false
388 }
388 }
389
389
390 $(this.submitButton).prop('disabled', submitState);
390 $(this.submitButton).prop('disabled', submitState);
391 if (submitEvent) {
391 if (submitEvent) {
392 $(this.submitButton).val(_gettext('Submitting...'));
392 $(this.submitButton).val(_gettext('Submitting...'));
393 } else {
393 } else {
394 $(this.submitButton).val(this.submitButtonText);
394 $(this.submitButton).val(this.submitButtonText);
395 }
395 }
396
396
397 };
397 };
398
398
399 // lock preview/edit/submit buttons on load, but exclude cancel button
399 // lock preview/edit/submit buttons on load, but exclude cancel button
400 var excludeCancelBtn = true;
400 var excludeCancelBtn = true;
401 this.setActionButtonsDisabled(true, excludeCancelBtn);
401 this.setActionButtonsDisabled(true, excludeCancelBtn);
402
402
403 // anonymous users don't have access to initialized CM instance
403 // anonymous users don't have access to initialized CM instance
404 if (this.cm !== undefined){
404 if (this.cm !== undefined){
405 this.cm.on('change', function(cMirror) {
405 this.cm.on('change', function(cMirror) {
406 if (cMirror.getValue() === "") {
406 if (cMirror.getValue() === "") {
407 self.setActionButtonsDisabled(true, excludeCancelBtn)
407 self.setActionButtonsDisabled(true, excludeCancelBtn)
408 } else {
408 } else {
409 self.setActionButtonsDisabled(false, excludeCancelBtn)
409 self.setActionButtonsDisabled(false, excludeCancelBtn)
410 }
410 }
411 });
411 });
412 }
412 }
413
413
414 $(this.editButton).on('click', function(e) {
414 $(this.editButton).on('click', function(e) {
415 e.preventDefault();
415 e.preventDefault();
416
416
417 $(self.previewButton).parent().removeClass('active');
417 $(self.previewButton).parent().removeClass('active');
418 $(self.previewContainer).hide();
418 $(self.previewContainer).hide();
419
419
420 $(self.editButton).parent().addClass('active');
420 $(self.editButton).parent().addClass('active');
421 $(self.editContainer).show();
421 $(self.editContainer).show();
422
422
423 });
423 });
424
424
425 $(this.previewButton).on('click', function(e) {
425 $(this.previewButton).on('click', function(e) {
426 e.preventDefault();
426 e.preventDefault();
427 var text = self.cm.getValue();
427 var text = self.cm.getValue();
428
428
429 if (text === "") {
429 if (text === "") {
430 return;
430 return;
431 }
431 }
432
432
433 var postData = {
433 var postData = {
434 'text': text,
434 'text': text,
435 'renderer': templateContext.visual.default_renderer,
435 'renderer': templateContext.visual.default_renderer,
436 'csrf_token': CSRF_TOKEN
436 'csrf_token': CSRF_TOKEN
437 };
437 };
438
438
439 // lock ALL buttons on preview
439 // lock ALL buttons on preview
440 self.setActionButtonsDisabled(true);
440 self.setActionButtonsDisabled(true);
441
441
442 $(self.previewBoxSelector).addClass('unloaded');
442 $(self.previewBoxSelector).addClass('unloaded');
443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
444
444
445 $(self.editContainer).hide();
445 $(self.editContainer).hide();
446 $(self.previewContainer).show();
446 $(self.previewContainer).show();
447
447
448 // by default we reset state of comment preserving the text
448 // by default we reset state of comment preserving the text
449 var previewFailCallback = function(data){
449 var previewFailCallback = function(data){
450 alert(
450 alert(
451 "Error while preview of comment.\n" +
451 "Error while preview of comment.\n" +
452 "Error code {0} ({1}).".format(data.status, data.statusText)
452 "Error code {0} ({1}).".format(data.status, data.statusText)
453 );
453 );
454 self.resetCommentFormState(text)
454 self.resetCommentFormState(text)
455 };
455 };
456 self.submitAjaxPOST(
456 self.submitAjaxPOST(
457 self.previewUrl, postData, self.previewSuccessCallback,
457 self.previewUrl, postData, self.previewSuccessCallback,
458 previewFailCallback);
458 previewFailCallback);
459
459
460 $(self.previewButton).parent().addClass('active');
460 $(self.previewButton).parent().addClass('active');
461 $(self.editButton).parent().removeClass('active');
461 $(self.editButton).parent().removeClass('active');
462 });
462 });
463
463
464 $(this.submitForm).submit(function(e) {
464 $(this.submitForm).submit(function(e) {
465 e.preventDefault();
465 e.preventDefault();
466 var allowedToSubmit = self.isAllowedToSubmit();
466 var allowedToSubmit = self.isAllowedToSubmit();
467 if (!allowedToSubmit){
467 if (!allowedToSubmit){
468 return false;
468 return false;
469 }
469 }
470 self.handleFormSubmit();
470 self.handleFormSubmit();
471 });
471 });
472
472
473 }
473 }
474
474
475 return CommentForm;
475 return CommentForm;
476 });
476 });
477
477
478 /* comments controller */
478 /* comments controller */
479 var CommentsController = function() {
479 var CommentsController = function() {
480 var mainComment = '#text';
480 var mainComment = '#text';
481 var self = this;
481 var self = this;
482
482
483 this.cancelComment = function(node) {
483 this.cancelComment = function(node) {
484 var $node = $(node);
484 var $node = $(node);
485 var $td = $node.closest('td');
485 var $td = $node.closest('td');
486 $node.closest('.comment-inline-form').remove();
486 $node.closest('.comment-inline-form').remove();
487 return false;
487 return false;
488 };
488 };
489
489
490 this.getLineNumber = function(node) {
490 this.getLineNumber = function(node) {
491 var $node = $(node);
491 var $node = $(node);
492 var lineNo = $node.closest('td').attr('data-line-no');
492 var lineNo = $node.closest('td').attr('data-line-no');
493 if (lineNo === undefined && $node.data('commentInline')){
493 if (lineNo === undefined && $node.data('commentInline')){
494 lineNo = $node.data('commentLineNo')
494 lineNo = $node.data('commentLineNo')
495 }
495 }
496
496
497 return lineNo
497 return lineNo
498 };
498 };
499
499
500 this.scrollToComment = function(node, offset, outdated) {
500 this.scrollToComment = function(node, offset, outdated) {
501 if (offset === undefined) {
501 if (offset === undefined) {
502 offset = 0;
502 offset = 0;
503 }
503 }
504 var outdated = outdated || false;
504 var outdated = outdated || false;
505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
506
506
507 if (!node) {
507 if (!node) {
508 node = $('.comment-selected');
508 node = $('.comment-selected');
509 if (!node.length) {
509 if (!node.length) {
510 node = $('comment-current')
510 node = $('comment-current')
511 }
511 }
512 }
512 }
513 $wrapper = $(node).closest('div.comment');
513 $wrapper = $(node).closest('div.comment');
514 $comment = $(node).closest(klass);
514 $comment = $(node).closest(klass);
515 $comments = $(klass);
515 $comments = $(klass);
516
516
517 // show hidden comment when referenced.
517 // show hidden comment when referenced.
518 if (!$wrapper.is(':visible')){
518 if (!$wrapper.is(':visible')){
519 $wrapper.show();
519 $wrapper.show();
520 }
520 }
521
521
522 $('.comment-selected').removeClass('comment-selected');
522 $('.comment-selected').removeClass('comment-selected');
523
523
524 var nextIdx = $(klass).index($comment) + offset;
524 var nextIdx = $(klass).index($comment) + offset;
525 if (nextIdx >= $comments.length) {
525 if (nextIdx >= $comments.length) {
526 nextIdx = 0;
526 nextIdx = 0;
527 }
527 }
528 var $next = $(klass).eq(nextIdx);
528 var $next = $(klass).eq(nextIdx);
529
529
530 var $cb = $next.closest('.cb');
530 var $cb = $next.closest('.cb');
531 $cb.removeClass('cb-collapsed');
531 $cb.removeClass('cb-collapsed');
532
532
533 var $filediffCollapseState = $cb.closest('.filediff').prev();
533 var $filediffCollapseState = $cb.closest('.filediff').prev();
534 $filediffCollapseState.prop('checked', false);
534 $filediffCollapseState.prop('checked', false);
535 $next.addClass('comment-selected');
535 $next.addClass('comment-selected');
536 scrollToElement($next);
536 scrollToElement($next);
537 return false;
537 return false;
538 };
538 };
539
539
540 this.nextComment = function(node) {
540 this.nextComment = function(node) {
541 return self.scrollToComment(node, 1);
541 return self.scrollToComment(node, 1);
542 };
542 };
543
543
544 this.prevComment = function(node) {
544 this.prevComment = function(node) {
545 return self.scrollToComment(node, -1);
545 return self.scrollToComment(node, -1);
546 };
546 };
547
547
548 this.nextOutdatedComment = function(node) {
548 this.nextOutdatedComment = function(node) {
549 return self.scrollToComment(node, 1, true);
549 return self.scrollToComment(node, 1, true);
550 };
550 };
551
551
552 this.prevOutdatedComment = function(node) {
552 this.prevOutdatedComment = function(node) {
553 return self.scrollToComment(node, -1, true);
553 return self.scrollToComment(node, -1, true);
554 };
554 };
555
555
556 this.deleteComment = function(node) {
556 this.deleteComment = function(node) {
557 if (!confirm(_gettext('Delete this comment?'))) {
557 if (!confirm(_gettext('Delete this comment?'))) {
558 return false;
558 return false;
559 }
559 }
560 var $node = $(node);
560 var $node = $(node);
561 var $td = $node.closest('td');
561 var $td = $node.closest('td');
562 var $comment = $node.closest('.comment');
562 var $comment = $node.closest('.comment');
563 var comment_id = $comment.attr('data-comment-id');
563 var comment_id = $comment.attr('data-comment-id');
564 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
564 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
565 var postData = {
565 var postData = {
566 'csrf_token': CSRF_TOKEN
566 'csrf_token': CSRF_TOKEN
567 };
567 };
568
568
569 $comment.addClass('comment-deleting');
569 $comment.addClass('comment-deleting');
570 $comment.hide('fast');
570 $comment.hide('fast');
571
571
572 var success = function(response) {
572 var success = function(response) {
573 $comment.remove();
573 $comment.remove();
574 return false;
574 return false;
575 };
575 };
576 var failure = function(data, textStatus, xhr) {
576 var failure = function(data, textStatus, xhr) {
577 alert("error processing request: " + textStatus);
577 alert("error processing request: " + textStatus);
578 $comment.show('fast');
578 $comment.show('fast');
579 $comment.removeClass('comment-deleting');
579 $comment.removeClass('comment-deleting');
580 return false;
580 return false;
581 };
581 };
582 ajaxPOST(url, postData, success, failure);
582 ajaxPOST(url, postData, success, failure);
583 };
583 };
584
584
585 this.toggleWideMode = function (node) {
585 this.toggleWideMode = function (node) {
586 if ($('#content').hasClass('wrapper')) {
586 if ($('#content').hasClass('wrapper')) {
587 $('#content').removeClass("wrapper");
587 $('#content').removeClass("wrapper");
588 $('#content').addClass("wide-mode-wrapper");
588 $('#content').addClass("wide-mode-wrapper");
589 $(node).addClass('btn-success');
589 $(node).addClass('btn-success');
590 return true
590 } else {
591 } else {
591 $('#content').removeClass("wide-mode-wrapper");
592 $('#content').removeClass("wide-mode-wrapper");
592 $('#content').addClass("wrapper");
593 $('#content').addClass("wrapper");
593 $(node).removeClass('btn-success');
594 $(node).removeClass('btn-success');
595 return false
594 }
596 }
595 return false;
597
596 };
598 };
597
599
598 this.toggleComments = function(node, show) {
600 this.toggleComments = function(node, show) {
599 var $filediff = $(node).closest('.filediff');
601 var $filediff = $(node).closest('.filediff');
600 if (show === true) {
602 if (show === true) {
601 $filediff.removeClass('hide-comments');
603 $filediff.removeClass('hide-comments');
602 } else if (show === false) {
604 } else if (show === false) {
603 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
605 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
604 $filediff.addClass('hide-comments');
606 $filediff.addClass('hide-comments');
605 } else {
607 } else {
606 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
608 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
607 $filediff.toggleClass('hide-comments');
609 $filediff.toggleClass('hide-comments');
608 }
610 }
609 return false;
611 return false;
610 };
612 };
611
613
612 this.toggleLineComments = function(node) {
614 this.toggleLineComments = function(node) {
613 self.toggleComments(node, true);
615 self.toggleComments(node, true);
614 var $node = $(node);
616 var $node = $(node);
615 // mark outdated comments as visible before the toggle;
617 // mark outdated comments as visible before the toggle;
616 $(node.closest('tr')).find('.comment-outdated').show();
618 $(node.closest('tr')).find('.comment-outdated').show();
617 $node.closest('tr').toggleClass('hide-line-comments');
619 $node.closest('tr').toggleClass('hide-line-comments');
618 };
620 };
619
621
620 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
622 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
621 var pullRequestId = templateContext.pull_request_data.pull_request_id;
623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
622 var commitId = templateContext.commit_data.commit_id;
624 var commitId = templateContext.commit_data.commit_id;
623
625
624 var commentForm = new CommentForm(
626 var commentForm = new CommentForm(
625 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
627 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
626 var cm = commentForm.getCmInstance();
628 var cm = commentForm.getCmInstance();
627
629
628 if (resolvesCommentId){
630 if (resolvesCommentId){
629 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
631 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
630 }
632 }
631
633
632 setTimeout(function() {
634 setTimeout(function() {
633 // callbacks
635 // callbacks
634 if (cm !== undefined) {
636 if (cm !== undefined) {
635 commentForm.setPlaceholder(placeholderText);
637 commentForm.setPlaceholder(placeholderText);
636 if (commentForm.isInline()) {
638 if (commentForm.isInline()) {
637 cm.focus();
639 cm.focus();
638 cm.refresh();
640 cm.refresh();
639 }
641 }
640 }
642 }
641 }, 10);
643 }, 10);
642
644
643 // trigger scrolldown to the resolve comment, since it might be away
645 // trigger scrolldown to the resolve comment, since it might be away
644 // from the clicked
646 // from the clicked
645 if (resolvesCommentId){
647 if (resolvesCommentId){
646 var actionNode = $(commentForm.resolvesActionId).offset();
648 var actionNode = $(commentForm.resolvesActionId).offset();
647
649
648 setTimeout(function() {
650 setTimeout(function() {
649 if (actionNode) {
651 if (actionNode) {
650 $('body, html').animate({scrollTop: actionNode.top}, 10);
652 $('body, html').animate({scrollTop: actionNode.top}, 10);
651 }
653 }
652 }, 100);
654 }, 100);
653 }
655 }
654
656
655 return commentForm;
657 return commentForm;
656 };
658 };
657
659
658 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
660 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
659
661
660 var tmpl = $('#cb-comment-general-form-template').html();
662 var tmpl = $('#cb-comment-general-form-template').html();
661 tmpl = tmpl.format(null, 'general');
663 tmpl = tmpl.format(null, 'general');
662 var $form = $(tmpl);
664 var $form = $(tmpl);
663
665
664 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
666 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
665 var curForm = $formPlaceholder.find('form');
667 var curForm = $formPlaceholder.find('form');
666 if (curForm){
668 if (curForm){
667 curForm.remove();
669 curForm.remove();
668 }
670 }
669 $formPlaceholder.append($form);
671 $formPlaceholder.append($form);
670
672
671 var _form = $($form[0]);
673 var _form = $($form[0]);
672 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
674 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
673 var commentForm = this.createCommentForm(
675 var commentForm = this.createCommentForm(
674 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
676 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
675 commentForm.initStatusChangeSelector();
677 commentForm.initStatusChangeSelector();
676
678
677 return commentForm;
679 return commentForm;
678 };
680 };
679
681
680 this.createComment = function(node, resolutionComment) {
682 this.createComment = function(node, resolutionComment) {
681 var resolvesCommentId = resolutionComment || null;
683 var resolvesCommentId = resolutionComment || null;
682 var $node = $(node);
684 var $node = $(node);
683 var $td = $node.closest('td');
685 var $td = $node.closest('td');
684 var $form = $td.find('.comment-inline-form');
686 var $form = $td.find('.comment-inline-form');
685
687
686 if (!$form.length) {
688 if (!$form.length) {
687
689
688 var $filediff = $node.closest('.filediff');
690 var $filediff = $node.closest('.filediff');
689 $filediff.removeClass('hide-comments');
691 $filediff.removeClass('hide-comments');
690 var f_path = $filediff.attr('data-f-path');
692 var f_path = $filediff.attr('data-f-path');
691 var lineno = self.getLineNumber(node);
693 var lineno = self.getLineNumber(node);
692 // create a new HTML from template
694 // create a new HTML from template
693 var tmpl = $('#cb-comment-inline-form-template').html();
695 var tmpl = $('#cb-comment-inline-form-template').html();
694 tmpl = tmpl.format(escapeHtml(f_path), lineno);
696 tmpl = tmpl.format(escapeHtml(f_path), lineno);
695 $form = $(tmpl);
697 $form = $(tmpl);
696
698
697 var $comments = $td.find('.inline-comments');
699 var $comments = $td.find('.inline-comments');
698 if (!$comments.length) {
700 if (!$comments.length) {
699 $comments = $(
701 $comments = $(
700 $('#cb-comments-inline-container-template').html());
702 $('#cb-comments-inline-container-template').html());
701 $td.append($comments);
703 $td.append($comments);
702 }
704 }
703
705
704 $td.find('.cb-comment-add-button').before($form);
706 $td.find('.cb-comment-add-button').before($form);
705
707
706 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
708 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
707 var _form = $($form[0]).find('form');
709 var _form = $($form[0]).find('form');
708 var autocompleteActions = ['as_note', 'as_todo'];
710 var autocompleteActions = ['as_note', 'as_todo'];
709 var commentForm = this.createCommentForm(
711 var commentForm = this.createCommentForm(
710 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
712 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
711
713
712 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
714 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
713 form: _form,
715 form: _form,
714 parent: $td[0],
716 parent: $td[0],
715 lineno: lineno,
717 lineno: lineno,
716 f_path: f_path}
718 f_path: f_path}
717 );
719 );
718
720
719 // set a CUSTOM submit handler for inline comments.
721 // set a CUSTOM submit handler for inline comments.
720 commentForm.setHandleFormSubmit(function(o) {
722 commentForm.setHandleFormSubmit(function(o) {
721 var text = commentForm.cm.getValue();
723 var text = commentForm.cm.getValue();
722 var commentType = commentForm.getCommentType();
724 var commentType = commentForm.getCommentType();
723 var resolvesCommentId = commentForm.getResolvesId();
725 var resolvesCommentId = commentForm.getResolvesId();
724
726
725 if (text === "") {
727 if (text === "") {
726 return;
728 return;
727 }
729 }
728
730
729 if (lineno === undefined) {
731 if (lineno === undefined) {
730 alert('missing line !');
732 alert('missing line !');
731 return;
733 return;
732 }
734 }
733 if (f_path === undefined) {
735 if (f_path === undefined) {
734 alert('missing file path !');
736 alert('missing file path !');
735 return;
737 return;
736 }
738 }
737
739
738 var excludeCancelBtn = false;
740 var excludeCancelBtn = false;
739 var submitEvent = true;
741 var submitEvent = true;
740 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
742 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
741 commentForm.cm.setOption("readOnly", true);
743 commentForm.cm.setOption("readOnly", true);
742 var postData = {
744 var postData = {
743 'text': text,
745 'text': text,
744 'f_path': f_path,
746 'f_path': f_path,
745 'line': lineno,
747 'line': lineno,
746 'comment_type': commentType,
748 'comment_type': commentType,
747 'csrf_token': CSRF_TOKEN
749 'csrf_token': CSRF_TOKEN
748 };
750 };
749 if (resolvesCommentId){
751 if (resolvesCommentId){
750 postData['resolves_comment_id'] = resolvesCommentId;
752 postData['resolves_comment_id'] = resolvesCommentId;
751 }
753 }
752
754
753 var submitSuccessCallback = function(json_data) {
755 var submitSuccessCallback = function(json_data) {
754 $form.remove();
756 $form.remove();
755 try {
757 try {
756 var html = json_data.rendered_text;
758 var html = json_data.rendered_text;
757 var lineno = json_data.line_no;
759 var lineno = json_data.line_no;
758 var target_id = json_data.target_id;
760 var target_id = json_data.target_id;
759
761
760 $comments.find('.cb-comment-add-button').before(html);
762 $comments.find('.cb-comment-add-button').before(html);
761
763
762 //mark visually which comment was resolved
764 //mark visually which comment was resolved
763 if (resolvesCommentId) {
765 if (resolvesCommentId) {
764 commentForm.markCommentResolved(resolvesCommentId);
766 commentForm.markCommentResolved(resolvesCommentId);
765 }
767 }
766
768
767 // run global callback on submit
769 // run global callback on submit
768 commentForm.globalSubmitSuccessCallback();
770 commentForm.globalSubmitSuccessCallback();
769
771
770 } catch (e) {
772 } catch (e) {
771 console.error(e);
773 console.error(e);
772 }
774 }
773
775
774 // re trigger the linkification of next/prev navigation
776 // re trigger the linkification of next/prev navigation
775 linkifyComments($('.inline-comment-injected'));
777 linkifyComments($('.inline-comment-injected'));
776 timeagoActivate();
778 timeagoActivate();
777
779
778 if (window.updateSticky !== undefined) {
780 if (window.updateSticky !== undefined) {
779 // potentially our comments change the active window size, so we
781 // potentially our comments change the active window size, so we
780 // notify sticky elements
782 // notify sticky elements
781 updateSticky()
783 updateSticky()
782 }
784 }
783
785
784 commentForm.setActionButtonsDisabled(false);
786 commentForm.setActionButtonsDisabled(false);
785
787
786 };
788 };
787 var submitFailCallback = function(data){
789 var submitFailCallback = function(data){
788 alert(
790 alert(
789 "Error while submitting comment.\n" +
791 "Error while submitting comment.\n" +
790 "Error code {0} ({1}).".format(data.status, data.statusText)
792 "Error code {0} ({1}).".format(data.status, data.statusText)
791 );
793 );
792 commentForm.resetCommentFormState(text)
794 commentForm.resetCommentFormState(text)
793 };
795 };
794 commentForm.submitAjaxPOST(
796 commentForm.submitAjaxPOST(
795 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
797 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
796 });
798 });
797 }
799 }
798
800
799 $form.addClass('comment-inline-form-open');
801 $form.addClass('comment-inline-form-open');
800 };
802 };
801
803
802 this.createResolutionComment = function(commentId){
804 this.createResolutionComment = function(commentId){
803 // hide the trigger text
805 // hide the trigger text
804 $('#resolve-comment-{0}'.format(commentId)).hide();
806 $('#resolve-comment-{0}'.format(commentId)).hide();
805
807
806 var comment = $('#comment-'+commentId);
808 var comment = $('#comment-'+commentId);
807 var commentData = comment.data();
809 var commentData = comment.data();
808 if (commentData.commentInline) {
810 if (commentData.commentInline) {
809 this.createComment(comment, commentId)
811 this.createComment(comment, commentId)
810 } else {
812 } else {
811 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
813 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
812 }
814 }
813
815
814 return false;
816 return false;
815 };
817 };
816
818
817 this.submitResolution = function(commentId){
819 this.submitResolution = function(commentId){
818 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
820 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
819 var commentForm = form.get(0).CommentForm;
821 var commentForm = form.get(0).CommentForm;
820
822
821 var cm = commentForm.getCmInstance();
823 var cm = commentForm.getCmInstance();
822 var renderer = templateContext.visual.default_renderer;
824 var renderer = templateContext.visual.default_renderer;
823 if (renderer == 'rst'){
825 if (renderer == 'rst'){
824 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
826 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
825 } else if (renderer == 'markdown') {
827 } else if (renderer == 'markdown') {
826 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
828 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
827 } else {
829 } else {
828 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
830 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
829 }
831 }
830
832
831 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
833 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
832 form.submit();
834 form.submit();
833 return false;
835 return false;
834 };
836 };
835
837
836 };
838 };
@@ -1,1025 +1,1031 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
47 deleted_files_comments=None,
48
48
49 # for cache purpose
49 # for cache purpose
50 inline_comments=None,
50 inline_comments=None,
51
51
52 )">
52 )">
53 %if use_comments:
53 %if use_comments:
54 <div id="cb-comments-inline-container-template" class="js-template">
54 <div id="cb-comments-inline-container-template" class="js-template">
55 ${inline_comments_container([], inline_comments)}
55 ${inline_comments_container([], inline_comments)}
56 </div>
56 </div>
57 <div class="js-template" id="cb-comment-inline-form-template">
57 <div class="js-template" id="cb-comment-inline-form-template">
58 <div class="comment-inline-form ac">
58 <div class="comment-inline-form ac">
59
59
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 ## render template for inline comments
61 ## render template for inline comments
62 ${commentblock.comment_form(form_type='inline')}
62 ${commentblock.comment_form(form_type='inline')}
63 %else:
63 %else:
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 <div class="pull-left">
65 <div class="pull-left">
66 <div class="comment-help pull-right">
66 <div class="comment-help pull-right">
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button pull-right">
70 <div class="comment-button pull-right">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 ${_('Cancel')}
72 ${_('Cancel')}
73 </button>
73 </button>
74 </div>
74 </div>
75 <div class="clearfix"></div>
75 <div class="clearfix"></div>
76 ${h.end_form()}
76 ${h.end_form()}
77 %endif
77 %endif
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 %endif
81 %endif
82 <%
82 <%
83 collapse_all = len(diffset.files) > collapse_when_files_over
83 collapse_all = len(diffset.files) > collapse_when_files_over
84 %>
84 %>
85
85
86 %if c.user_session_attrs["diffmode"] == 'sideside':
86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 <style>
87 <style>
88 .wrapper {
88 .wrapper {
89 max-width: 1600px !important;
89 max-width: 1600px !important;
90 }
90 }
91 </style>
91 </style>
92 %endif
92 %endif
93
93
94 %if ruler_at_chars:
94 %if ruler_at_chars:
95 <style>
95 <style>
96 .diff table.cb .cb-content:after {
96 .diff table.cb .cb-content:after {
97 content: "";
97 content: "";
98 border-left: 1px solid blue;
98 border-left: 1px solid blue;
99 position: absolute;
99 position: absolute;
100 top: 0;
100 top: 0;
101 height: 18px;
101 height: 18px;
102 opacity: .2;
102 opacity: .2;
103 z-index: 10;
103 z-index: 10;
104 //## +5 to account for diff action (+/-)
104 //## +5 to account for diff action (+/-)
105 left: ${ruler_at_chars + 5}ch;
105 left: ${ruler_at_chars + 5}ch;
106 </style>
106 </style>
107 %endif
107 %endif
108
108
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 %if commit:
111 %if commit:
112 <div class="pull-right">
112 <div class="pull-right">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 ${_('Browse Files')}
114 ${_('Browse Files')}
115 </a>
115 </a>
116 </div>
116 </div>
117 %endif
117 %endif
118 <h2 class="clearinner">
118 <h2 class="clearinner">
119 ## invidual commit
119 ## invidual commit
120 % if commit:
120 % if commit:
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 ${h.age_component(commit.date)}
122 ${h.age_component(commit.date)}
123 % if diffset.limited_diff:
123 % if diffset.limited_diff:
124 - ${_('The requested changes are too big and content was truncated.')}
124 - ${_('The requested changes are too big and content was truncated.')}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 ## compare diff, has no file-selector and we want to show stats anyway
128 ## compare diff, has no file-selector and we want to show stats anyway
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 % endif
132 % endif
133 % else:
133 % else:
134 ## pull requests/compare
134 ## pull requests/compare
135 ${_('File Changes')}
135 ${_('File Changes')}
136 % endif
136 % endif
137
137
138 </h2>
138 </h2>
139 </div>
139 </div>
140
140
141 %if diffset.has_hidden_changes:
141 %if diffset.has_hidden_changes:
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 %elif not diffset.files:
143 %elif not diffset.files:
144 <p class="empty_data">${_('No files')}</p>
144 <p class="empty_data">${_('No files')}</p>
145 %endif
145 %endif
146
146
147 <div class="filediffs">
147 <div class="filediffs">
148
148
149 ## initial value could be marked as False later on
149 ## initial value could be marked as False later on
150 <% over_lines_changed_limit = False %>
150 <% over_lines_changed_limit = False %>
151 %for i, filediff in enumerate(diffset.files):
151 %for i, filediff in enumerate(diffset.files):
152
152
153 <%
153 <%
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 over_lines_changed_limit = lines_changed > lines_changed_limit
155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 %>
156 %>
157 ## anchor with support of sticky header
157 ## anchor with support of sticky header
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159
159
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 <div
161 <div
162 class="filediff"
162 class="filediff"
163 data-f-path="${filediff.patch['filename']}"
163 data-f-path="${filediff.patch['filename']}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 >
165 >
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 <div class="filediff-collapse-indicator"></div>
167 <div class="filediff-collapse-indicator"></div>
168 ${diff_ops(filediff)}
168 ${diff_ops(filediff)}
169 </label>
169 </label>
170
170
171 ${diff_menu(filediff, use_comments=use_comments)}
171 ${diff_menu(filediff, use_comments=use_comments)}
172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
173
173
174 ## new/deleted/empty content case
174 ## new/deleted/empty content case
175 % if not filediff.hunks:
175 % if not filediff.hunks:
176 ## Comment container, on "fakes" hunk that contains all data to render comments
176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 % endif
178 % endif
179
179
180 %if filediff.limited_diff:
180 %if filediff.limited_diff:
181 <tr class="cb-warning cb-collapser">
181 <tr class="cb-warning cb-collapser">
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
183 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
184 </td>
184 </td>
185 </tr>
185 </tr>
186 %else:
186 %else:
187 %if over_lines_changed_limit:
187 %if over_lines_changed_limit:
188 <tr class="cb-warning cb-collapser">
188 <tr class="cb-warning cb-collapser">
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 <a href="#" class="cb-expand"
191 <a href="#" class="cb-expand"
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 </a>
193 </a>
194 <a href="#" class="cb-collapse"
194 <a href="#" class="cb-collapse"
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 </a>
196 </a>
197 </td>
197 </td>
198 </tr>
198 </tr>
199 %endif
199 %endif
200 %endif
200 %endif
201
201
202 % for hunk in filediff.hunks:
202 % for hunk in filediff.hunks:
203 <tr class="cb-hunk">
203 <tr class="cb-hunk">
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 ## TODO: dan: add ajax loading of more context here
205 ## TODO: dan: add ajax loading of more context here
206 ## <a href="#">
206 ## <a href="#">
207 <i class="icon-more"></i>
207 <i class="icon-more"></i>
208 ## </a>
208 ## </a>
209 </td>
209 </td>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 @@
211 @@
212 -${hunk.source_start},${hunk.source_length}
212 -${hunk.source_start},${hunk.source_length}
213 +${hunk.target_start},${hunk.target_length}
213 +${hunk.target_start},${hunk.target_length}
214 ${hunk.section_header}
214 ${hunk.section_header}
215 </td>
215 </td>
216 </tr>
216 </tr>
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 % endfor
218 % endfor
219
219
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221
221
222 ## outdated comments that do not fit into currently displayed lines
222 ## outdated comments that do not fit into currently displayed lines
223 % for lineno, comments in unmatched_comments.items():
223 % for lineno, comments in unmatched_comments.items():
224
224
225 %if c.user_session_attrs["diffmode"] == 'unified':
225 %if c.user_session_attrs["diffmode"] == 'unified':
226 % if loop.index == 0:
226 % if loop.index == 0:
227 <tr class="cb-hunk">
227 <tr class="cb-hunk">
228 <td colspan="3"></td>
228 <td colspan="3"></td>
229 <td>
229 <td>
230 <div>
230 <div>
231 ${_('Unmatched inline comments below')}
231 ${_('Unmatched inline comments below')}
232 </div>
232 </div>
233 </td>
233 </td>
234 </tr>
234 </tr>
235 % endif
235 % endif
236 <tr class="cb-line">
236 <tr class="cb-line">
237 <td class="cb-data cb-context"></td>
237 <td class="cb-data cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
240 <td class="cb-content cb-context">
240 <td class="cb-content cb-context">
241 ${inline_comments_container(comments, inline_comments)}
241 ${inline_comments_container(comments, inline_comments)}
242 </td>
242 </td>
243 </tr>
243 </tr>
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 % if loop.index == 0:
245 % if loop.index == 0:
246 <tr class="cb-comment-info">
246 <tr class="cb-comment-info">
247 <td colspan="2"></td>
247 <td colspan="2"></td>
248 <td class="cb-line">
248 <td class="cb-line">
249 <div>
249 <div>
250 ${_('Unmatched inline comments below')}
250 ${_('Unmatched inline comments below')}
251 </div>
251 </div>
252 </td>
252 </td>
253 <td colspan="2"></td>
253 <td colspan="2"></td>
254 <td class="cb-line">
254 <td class="cb-line">
255 <div>
255 <div>
256 ${_('Unmatched comments below')}
256 ${_('Unmatched comments below')}
257 </div>
257 </div>
258 </td>
258 </td>
259 </tr>
259 </tr>
260 % endif
260 % endif
261 <tr class="cb-line">
261 <tr class="cb-line">
262 <td class="cb-data cb-context"></td>
262 <td class="cb-data cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
264 <td class="cb-content cb-context">
264 <td class="cb-content cb-context">
265 % if lineno.startswith('o'):
265 % if lineno.startswith('o'):
266 ${inline_comments_container(comments, inline_comments)}
266 ${inline_comments_container(comments, inline_comments)}
267 % endif
267 % endif
268 </td>
268 </td>
269
269
270 <td class="cb-data cb-context"></td>
270 <td class="cb-data cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
272 <td class="cb-content cb-context">
272 <td class="cb-content cb-context">
273 % if lineno.startswith('n'):
273 % if lineno.startswith('n'):
274 ${inline_comments_container(comments, inline_comments)}
274 ${inline_comments_container(comments, inline_comments)}
275 % endif
275 % endif
276 </td>
276 </td>
277 </tr>
277 </tr>
278 %endif
278 %endif
279
279
280 % endfor
280 % endfor
281
281
282 </table>
282 </table>
283 </div>
283 </div>
284 %endfor
284 %endfor
285
285
286 ## outdated comments that are made for a file that has been deleted
286 ## outdated comments that are made for a file that has been deleted
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 <%
288 <%
289 display_state = 'display: none'
289 display_state = 'display: none'
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 if open_comments_in_file:
291 if open_comments_in_file:
292 display_state = ''
292 display_state = ''
293 %>
293 %>
294 <div class="filediffs filediff-outdated" style="${display_state}">
294 <div class="filediffs filediff-outdated" style="${display_state}">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 <div class="filediff-collapse-indicator"></div>
298 <div class="filediff-collapse-indicator"></div>
299 <span class="pill">
299 <span class="pill">
300 ## file was deleted
300 ## file was deleted
301 <strong>${filename}</strong>
301 <strong>${filename}</strong>
302 </span>
302 </span>
303 <span class="pill-group" style="float: left">
303 <span class="pill-group" style="float: left">
304 ## file op, doesn't need translation
304 ## file op, doesn't need translation
305 <span class="pill" op="removed">removed in this version</span>
305 <span class="pill" op="removed">removed in this version</span>
306 </span>
306 </span>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
308 <span class="pill-group" style="float: right">
308 <span class="pill-group" style="float: right">
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 </span>
310 </span>
311 </label>
311 </label>
312
312
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 <tr>
314 <tr>
315 % if c.user_session_attrs["diffmode"] == 'unified':
315 % if c.user_session_attrs["diffmode"] == 'unified':
316 <td></td>
316 <td></td>
317 %endif
317 %endif
318
318
319 <td></td>
319 <td></td>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 </td>
322 </td>
323 </tr>
323 </tr>
324 %if c.user_session_attrs["diffmode"] == 'unified':
324 %if c.user_session_attrs["diffmode"] == 'unified':
325 <tr class="cb-line">
325 <tr class="cb-line">
326 <td class="cb-data cb-context"></td>
326 <td class="cb-data cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
329 <td class="cb-content cb-context">
329 <td class="cb-content cb-context">
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 </td>
331 </td>
332 </tr>
332 </tr>
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 <tr class="cb-line">
334 <tr class="cb-line">
335 <td class="cb-data cb-context"></td>
335 <td class="cb-data cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
337 <td class="cb-content cb-context"></td>
337 <td class="cb-content cb-context"></td>
338
338
339 <td class="cb-data cb-context"></td>
339 <td class="cb-data cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
341 <td class="cb-content cb-context">
341 <td class="cb-content cb-context">
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 </td>
343 </td>
344 </tr>
344 </tr>
345 %endif
345 %endif
346 </table>
346 </table>
347 </div>
347 </div>
348 </div>
348 </div>
349 % endfor
349 % endfor
350
350
351 </div>
351 </div>
352 </div>
352 </div>
353 </%def>
353 </%def>
354
354
355 <%def name="diff_ops(filediff)">
355 <%def name="diff_ops(filediff)">
356 <%
356 <%
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 %>
359 %>
360 <span class="pill">
360 <span class="pill">
361 %if filediff.source_file_path and filediff.target_file_path:
361 %if filediff.source_file_path and filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
363 ## file was renamed, or copied
363 ## file was renamed, or copied
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
366 <% final_path = filediff.target_file_path %>
366 <% final_path = filediff.target_file_path %>
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
369 <% final_path = filediff.target_file_path %>
369 <% final_path = filediff.target_file_path %>
370 %endif
370 %endif
371 %else:
371 %else:
372 ## file was modified
372 ## file was modified
373 <strong>${filediff.source_file_path}</strong>
373 <strong>${filediff.source_file_path}</strong>
374 <% final_path = filediff.source_file_path %>
374 <% final_path = filediff.source_file_path %>
375 %endif
375 %endif
376 %else:
376 %else:
377 %if filediff.source_file_path:
377 %if filediff.source_file_path:
378 ## file was deleted
378 ## file was deleted
379 <strong>${filediff.source_file_path}</strong>
379 <strong>${filediff.source_file_path}</strong>
380 <% final_path = filediff.source_file_path %>
380 <% final_path = filediff.source_file_path %>
381 %else:
381 %else:
382 ## file was added
382 ## file was added
383 <strong>${filediff.target_file_path}</strong>
383 <strong>${filediff.target_file_path}</strong>
384 <% final_path = filediff.target_file_path %>
384 <% final_path = filediff.target_file_path %>
385 %endif
385 %endif
386 %endif
386 %endif
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 </span>
388 </span>
389 ## anchor link
389 ## anchor link
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
391
391
392 <span class="pill-group" style="float: right">
392 <span class="pill-group" style="float: right">
393
393
394 ## ops pills
394 ## ops pills
395 %if filediff.limited_diff:
395 %if filediff.limited_diff:
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 %endif
397 %endif
398
398
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 <span class="pill" op="created">created</span>
400 <span class="pill" op="created">created</span>
401 %if filediff['target_mode'].startswith('120'):
401 %if filediff['target_mode'].startswith('120'):
402 <span class="pill" op="symlink">symlink</span>
402 <span class="pill" op="symlink">symlink</span>
403 %else:
403 %else:
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 %endif
405 %endif
406 %endif
406 %endif
407
407
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 <span class="pill" op="renamed">renamed</span>
409 <span class="pill" op="renamed">renamed</span>
410 %endif
410 %endif
411
411
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 <span class="pill" op="copied">copied</span>
413 <span class="pill" op="copied">copied</span>
414 %endif
414 %endif
415
415
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 <span class="pill" op="removed">removed</span>
417 <span class="pill" op="removed">removed</span>
418 %endif
418 %endif
419
419
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 <span class="pill" op="mode">
421 <span class="pill" op="mode">
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
423 </span>
423 </span>
424 %endif
424 %endif
425
425
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 <span class="pill" op="binary">binary</span>
427 <span class="pill" op="binary">binary</span>
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 <span class="pill" op="modified">modified</span>
429 <span class="pill" op="modified">modified</span>
430 %endif
430 %endif
431 %endif
431 %endif
432
432
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435
435
436 </span>
436 </span>
437
437
438 </%def>
438 </%def>
439
439
440 <%def name="nice_mode(filemode)">
440 <%def name="nice_mode(filemode)">
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 </%def>
442 </%def>
443
443
444 <%def name="diff_menu(filediff, use_comments=False)">
444 <%def name="diff_menu(filediff, use_comments=False)">
445 <div class="filediff-menu">
445 <div class="filediff-menu">
446
446
447 %if filediff.diffset.source_ref:
447 %if filediff.diffset.source_ref:
448
448
449 ## FILE BEFORE CHANGES
449 ## FILE BEFORE CHANGES
450 %if filediff.operation in ['D', 'M']:
450 %if filediff.operation in ['D', 'M']:
451 <a
451 <a
452 class="tooltip"
452 class="tooltip"
453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
455 >
455 >
456 ${_('Show file before')}
456 ${_('Show file before')}
457 </a> |
457 </a> |
458 %else:
458 %else:
459 <span
459 <span
460 class="tooltip"
460 class="tooltip"
461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
462 >
462 >
463 ${_('Show file before')}
463 ${_('Show file before')}
464 </span> |
464 </span> |
465 %endif
465 %endif
466
466
467 ## FILE AFTER CHANGES
467 ## FILE AFTER CHANGES
468 %if filediff.operation in ['A', 'M']:
468 %if filediff.operation in ['A', 'M']:
469 <a
469 <a
470 class="tooltip"
470 class="tooltip"
471 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
471 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
473 >
473 >
474 ${_('Show file after')}
474 ${_('Show file after')}
475 </a>
475 </a>
476 %else:
476 %else:
477 <span
477 <span
478 class="tooltip"
478 class="tooltip"
479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
480 >
480 >
481 ${_('Show file after')}
481 ${_('Show file after')}
482 </span>
482 </span>
483 %endif
483 %endif
484
484
485 % if use_comments:
485 % if use_comments:
486 |
486 |
487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
489 </a>
489 </a>
490 % endif
490 % endif
491
491
492 %endif
492 %endif
493
493
494 </div>
494 </div>
495 </%def>
495 </%def>
496
496
497
497
498 <%def name="inline_comments_container(comments, inline_comments)">
498 <%def name="inline_comments_container(comments, inline_comments)">
499 <div class="inline-comments">
499 <div class="inline-comments">
500 %for comment in comments:
500 %for comment in comments:
501 ${commentblock.comment_block(comment, inline=True)}
501 ${commentblock.comment_block(comment, inline=True)}
502 %endfor
502 %endfor
503 % if comments and comments[-1].outdated:
503 % if comments and comments[-1].outdated:
504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
505 style="display: none;}">
505 style="display: none;}">
506 ${_('Add another comment')}
506 ${_('Add another comment')}
507 </span>
507 </span>
508 % else:
508 % else:
509 <span onclick="return Rhodecode.comments.createComment(this)"
509 <span onclick="return Rhodecode.comments.createComment(this)"
510 class="btn btn-secondary cb-comment-add-button">
510 class="btn btn-secondary cb-comment-add-button">
511 ${_('Add another comment')}
511 ${_('Add another comment')}
512 </span>
512 </span>
513 % endif
513 % endif
514
514
515 </div>
515 </div>
516 </%def>
516 </%def>
517
517
518 <%!
518 <%!
519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
520 if hasattr(filename, 'unicode_path'):
520 if hasattr(filename, 'unicode_path'):
521 filename = filename.unicode_path
521 filename = filename.unicode_path
522
522
523 if not isinstance(filename, (unicode, str)):
523 if not isinstance(filename, (unicode, str)):
524 return None
524 return None
525
525
526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
527
527
528 if comments and filename in comments:
528 if comments and filename in comments:
529 file_comments = comments[filename]
529 file_comments = comments[filename]
530 if line_key in file_comments:
530 if line_key in file_comments:
531 data = file_comments.pop(line_key)
531 data = file_comments.pop(line_key)
532 return data
532 return data
533 %>
533 %>
534
534
535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
536 %for i, line in enumerate(hunk.sideside):
536 %for i, line in enumerate(hunk.sideside):
537 <%
537 <%
538 old_line_anchor, new_line_anchor = None, None
538 old_line_anchor, new_line_anchor = None, None
539
539
540 if line.original.lineno:
540 if line.original.lineno:
541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
542 if line.modified.lineno:
542 if line.modified.lineno:
543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
544 %>
544 %>
545
545
546 <tr class="cb-line">
546 <tr class="cb-line">
547 <td class="cb-data ${action_class(line.original.action)}"
547 <td class="cb-data ${action_class(line.original.action)}"
548 data-line-no="${line.original.lineno}"
548 data-line-no="${line.original.lineno}"
549 >
549 >
550 <div>
550 <div>
551
551
552 <% line_old_comments = None %>
552 <% line_old_comments = None %>
553 %if line.original.get_comment_args:
553 %if line.original.get_comment_args:
554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
555 %endif
555 %endif
556 %if line_old_comments:
556 %if line_old_comments:
557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
558 % if has_outdated:
558 % if has_outdated:
559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
560 % else:
560 % else:
561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
562 % endif
562 % endif
563 %endif
563 %endif
564 </div>
564 </div>
565 </td>
565 </td>
566 <td class="cb-lineno ${action_class(line.original.action)}"
566 <td class="cb-lineno ${action_class(line.original.action)}"
567 data-line-no="${line.original.lineno}"
567 data-line-no="${line.original.lineno}"
568 %if old_line_anchor:
568 %if old_line_anchor:
569 id="${old_line_anchor}"
569 id="${old_line_anchor}"
570 %endif
570 %endif
571 >
571 >
572 %if line.original.lineno:
572 %if line.original.lineno:
573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
574 %endif
574 %endif
575 </td>
575 </td>
576 <td class="cb-content ${action_class(line.original.action)}"
576 <td class="cb-content ${action_class(line.original.action)}"
577 data-line-no="o${line.original.lineno}"
577 data-line-no="o${line.original.lineno}"
578 >
578 >
579 %if use_comments and line.original.lineno:
579 %if use_comments and line.original.lineno:
580 ${render_add_comment_button()}
580 ${render_add_comment_button()}
581 %endif
581 %endif
582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
583
583
584 %if use_comments and line.original.lineno and line_old_comments:
584 %if use_comments and line.original.lineno and line_old_comments:
585 ${inline_comments_container(line_old_comments, inline_comments)}
585 ${inline_comments_container(line_old_comments, inline_comments)}
586 %endif
586 %endif
587
587
588 </td>
588 </td>
589 <td class="cb-data ${action_class(line.modified.action)}"
589 <td class="cb-data ${action_class(line.modified.action)}"
590 data-line-no="${line.modified.lineno}"
590 data-line-no="${line.modified.lineno}"
591 >
591 >
592 <div>
592 <div>
593
593
594 %if line.modified.get_comment_args:
594 %if line.modified.get_comment_args:
595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
596 %else:
596 %else:
597 <% line_new_comments = None%>
597 <% line_new_comments = None%>
598 %endif
598 %endif
599 %if line_new_comments:
599 %if line_new_comments:
600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
601 % if has_outdated:
601 % if has_outdated:
602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
603 % else:
603 % else:
604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
605 % endif
605 % endif
606 %endif
606 %endif
607 </div>
607 </div>
608 </td>
608 </td>
609 <td class="cb-lineno ${action_class(line.modified.action)}"
609 <td class="cb-lineno ${action_class(line.modified.action)}"
610 data-line-no="${line.modified.lineno}"
610 data-line-no="${line.modified.lineno}"
611 %if new_line_anchor:
611 %if new_line_anchor:
612 id="${new_line_anchor}"
612 id="${new_line_anchor}"
613 %endif
613 %endif
614 >
614 >
615 %if line.modified.lineno:
615 %if line.modified.lineno:
616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
617 %endif
617 %endif
618 </td>
618 </td>
619 <td class="cb-content ${action_class(line.modified.action)}"
619 <td class="cb-content ${action_class(line.modified.action)}"
620 data-line-no="n${line.modified.lineno}"
620 data-line-no="n${line.modified.lineno}"
621 >
621 >
622 %if use_comments and line.modified.lineno:
622 %if use_comments and line.modified.lineno:
623 ${render_add_comment_button()}
623 ${render_add_comment_button()}
624 %endif
624 %endif
625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
626 %if use_comments and line.modified.lineno and line_new_comments:
626 %if use_comments and line.modified.lineno and line_new_comments:
627 ${inline_comments_container(line_new_comments, inline_comments)}
627 ${inline_comments_container(line_new_comments, inline_comments)}
628 %endif
628 %endif
629 </td>
629 </td>
630 </tr>
630 </tr>
631 %endfor
631 %endfor
632 </%def>
632 </%def>
633
633
634
634
635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
637
637
638 <%
638 <%
639 old_line_anchor, new_line_anchor = None, None
639 old_line_anchor, new_line_anchor = None, None
640 if old_line_no:
640 if old_line_no:
641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
642 if new_line_no:
642 if new_line_no:
643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
644 %>
644 %>
645 <tr class="cb-line">
645 <tr class="cb-line">
646 <td class="cb-data ${action_class(action)}">
646 <td class="cb-data ${action_class(action)}">
647 <div>
647 <div>
648
648
649 %if comments_args:
649 %if comments_args:
650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
651 %else:
651 %else:
652 <% comments = None %>
652 <% comments = None %>
653 %endif
653 %endif
654
654
655 % if comments:
655 % if comments:
656 <% has_outdated = any([x.outdated for x in comments]) %>
656 <% has_outdated = any([x.outdated for x in comments]) %>
657 % if has_outdated:
657 % if has_outdated:
658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
659 % else:
659 % else:
660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
661 % endif
661 % endif
662 % endif
662 % endif
663 </div>
663 </div>
664 </td>
664 </td>
665 <td class="cb-lineno ${action_class(action)}"
665 <td class="cb-lineno ${action_class(action)}"
666 data-line-no="${old_line_no}"
666 data-line-no="${old_line_no}"
667 %if old_line_anchor:
667 %if old_line_anchor:
668 id="${old_line_anchor}"
668 id="${old_line_anchor}"
669 %endif
669 %endif
670 >
670 >
671 %if old_line_anchor:
671 %if old_line_anchor:
672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
673 %endif
673 %endif
674 </td>
674 </td>
675 <td class="cb-lineno ${action_class(action)}"
675 <td class="cb-lineno ${action_class(action)}"
676 data-line-no="${new_line_no}"
676 data-line-no="${new_line_no}"
677 %if new_line_anchor:
677 %if new_line_anchor:
678 id="${new_line_anchor}"
678 id="${new_line_anchor}"
679 %endif
679 %endif
680 >
680 >
681 %if new_line_anchor:
681 %if new_line_anchor:
682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
683 %endif
683 %endif
684 </td>
684 </td>
685 <td class="cb-content ${action_class(action)}"
685 <td class="cb-content ${action_class(action)}"
686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
687 >
687 >
688 %if use_comments:
688 %if use_comments:
689 ${render_add_comment_button()}
689 ${render_add_comment_button()}
690 %endif
690 %endif
691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
692 %if use_comments and comments:
692 %if use_comments and comments:
693 ${inline_comments_container(comments, inline_comments)}
693 ${inline_comments_container(comments, inline_comments)}
694 %endif
694 %endif
695 </td>
695 </td>
696 </tr>
696 </tr>
697 %endfor
697 %endfor
698 </%def>
698 </%def>
699
699
700
700
701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
702 % if diff_mode == 'unified':
702 % if diff_mode == 'unified':
703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
704 % elif diff_mode == 'sideside':
704 % elif diff_mode == 'sideside':
705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
706 % else:
706 % else:
707 <tr class="cb-line">
707 <tr class="cb-line">
708 <td>unknown diff mode</td>
708 <td>unknown diff mode</td>
709 </tr>
709 </tr>
710 % endif
710 % endif
711 </%def>file changes
711 </%def>file changes
712
712
713
713
714 <%def name="render_add_comment_button()">
714 <%def name="render_add_comment_button()">
715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
716 <span><i class="icon-comment"></i></span>
716 <span><i class="icon-comment"></i></span>
717 </button>
717 </button>
718 </%def>
718 </%def>
719
719
720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
721
721
722 <div id="diff-file-sticky" class="diffset-menu clearinner">
722 <div id="diff-file-sticky" class="diffset-menu clearinner">
723 ## auto adjustable
723 ## auto adjustable
724 <div class="sidebar__inner">
724 <div class="sidebar__inner">
725 <div class="sidebar__bar">
725 <div class="sidebar__bar">
726 <div class="pull-right">
726 <div class="pull-right">
727 <div class="btn-group">
727 <div class="btn-group">
728
728
729 ## DIFF OPTIONS via Select2
729 ## DIFF OPTIONS via Select2
730 <div class="pull-left">
730 <div class="pull-left">
731 ${h.hidden('diff_menu')}
731 ${h.hidden('diff_menu')}
732 </div>
732 </div>
733
733
734 <a
734 <a
735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
736 title="${h.tooltip(_('View side by side'))}"
736 title="${h.tooltip(_('View side by side'))}"
737 href="${h.current_route_path(request, diffmode='sideside')}">
737 href="${h.current_route_path(request, diffmode='sideside')}">
738 <span>${_('Side by Side')}</span>
738 <span>${_('Side by Side')}</span>
739 </a>
739 </a>
740
740
741 <a
741 <a
742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
744 <span>${_('Unified')}</span>
744 <span>${_('Unified')}</span>
745 </a>
745 </a>
746
746
747 % if range_diff_on is True:
747 % if range_diff_on is True:
748 <a
748 <a
749 title="${_('Turn off: Show the diff as commit range')}"
749 title="${_('Turn off: Show the diff as commit range')}"
750 class="btn btn-primary"
750 class="btn btn-primary"
751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
752 <span>${_('Range Diff')}</span>
752 <span>${_('Range Diff')}</span>
753 </a>
753 </a>
754 % elif range_diff_on is False:
754 % elif range_diff_on is False:
755 <a
755 <a
756 title="${_('Show the diff as commit range')}"
756 title="${_('Show the diff as commit range')}"
757 class="btn"
757 class="btn"
758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
759 <span>${_('Range Diff')}</span>
759 <span>${_('Range Diff')}</span>
760 </a>
760 </a>
761 % endif
761 % endif
762 </div>
762 </div>
763 </div>
763 </div>
764 <div class="pull-left">
764 <div class="pull-left">
765 <div class="btn-group">
765 <div class="btn-group">
766 <div class="pull-left">
766 <div class="pull-left">
767 ${h.hidden('file_filter')}
767 ${h.hidden('file_filter')}
768 </div>
768 </div>
769 <a
769 <a
770 class="btn"
770 class="btn"
771 href="#"
771 href="#"
772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
773 <a
773 <a
774 class="btn"
774 class="btn"
775 href="#"
775 href="#"
776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
777 </div>
777 </div>
778 </div>
778 </div>
779 </div>
779 </div>
780 <div class="fpath-placeholder">
780 <div class="fpath-placeholder">
781 <i class="icon-file-text"></i>
781 <i class="icon-file-text"></i>
782 <strong class="fpath-placeholder-text">
782 <strong class="fpath-placeholder-text">
783 Context file:
783 Context file:
784 </strong>
784 </strong>
785 </div>
785 </div>
786 <div class="sidebar_inner_shadow"></div>
786 <div class="sidebar_inner_shadow"></div>
787 </div>
787 </div>
788 </div>
788 </div>
789
789
790 % if diffset:
790 % if diffset:
791
791
792 %if diffset.limited_diff:
792 %if diffset.limited_diff:
793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
794 %else:
794 %else:
795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
796 %endif
796 %endif
797 ## case on range-diff placeholder needs to be updated
797 ## case on range-diff placeholder needs to be updated
798 % if range_diff_on is True:
798 % if range_diff_on is True:
799 <% file_placeholder = _('Disabled on range diff') %>
799 <% file_placeholder = _('Disabled on range diff') %>
800 % endif
800 % endif
801
801
802 <script>
802 <script>
803
803
804 var feedFilesOptions = function (query, initialData) {
804 var feedFilesOptions = function (query, initialData) {
805 var data = {results: []};
805 var data = {results: []};
806 var isQuery = typeof query.term !== 'undefined';
806 var isQuery = typeof query.term !== 'undefined';
807
807
808 var section = _gettext('Changed files');
808 var section = _gettext('Changed files');
809 var filteredData = [];
809 var filteredData = [];
810
810
811 //filter results
811 //filter results
812 $.each(initialData.results, function (idx, value) {
812 $.each(initialData.results, function (idx, value) {
813
813
814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
815 filteredData.push({
815 filteredData.push({
816 'id': this.id,
816 'id': this.id,
817 'text': this.text,
817 'text': this.text,
818 "ops": this.ops,
818 "ops": this.ops,
819 })
819 })
820 }
820 }
821
821
822 });
822 });
823
823
824 data.results = filteredData;
824 data.results = filteredData;
825
825
826 query.callback(data);
826 query.callback(data);
827 };
827 };
828
828
829 var formatFileResult = function(result, container, query, escapeMarkup) {
829 var formatFileResult = function(result, container, query, escapeMarkup) {
830 return function(data, escapeMarkup) {
830 return function(data, escapeMarkup) {
831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
834 '<span class="pill" op="added">{0}</span>' +
834 '<span class="pill" op="added">{0}</span>' +
835 '<span class="pill" op="deleted">{1}</span>' +
835 '<span class="pill" op="deleted">{1}</span>' +
836 '</span>'
836 '</span>'
837 ;
837 ;
838 var added = data['ops']['added'];
838 var added = data['ops']['added'];
839 if (added === 0) {
839 if (added === 0) {
840 // don't show +0
840 // don't show +0
841 added = 0;
841 added = 0;
842 } else {
842 } else {
843 added = '+' + added;
843 added = '+' + added;
844 }
844 }
845
845
846 var deleted = -1*data['ops']['deleted'];
846 var deleted = -1*data['ops']['deleted'];
847
847
848 tmpl += pill.format(added, deleted);
848 tmpl += pill.format(added, deleted);
849 return container.format(tmpl);
849 return container.format(tmpl);
850
850
851 }(result, escapeMarkup);
851 }(result, escapeMarkup);
852 };
852 };
853
853
854 var preloadFileFilterData = {
854 var preloadFileFilterData = {
855 results: [
855 results: [
856 % for filediff in diffset.files:
856 % for filediff in diffset.files:
857 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
857 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
858 text:"${filediff.patch['filename']}",
858 text:"${filediff.patch['filename']}",
859 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
859 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
860 % endfor
860 % endfor
861 ]
861 ]
862 };
862 };
863
863
864 $(document).ready(function () {
864 $(document).ready(function () {
865
865
866 var fileFilter = $("#file_filter").select2({
866 var fileFilter = $("#file_filter").select2({
867 'dropdownAutoWidth': true,
867 'dropdownAutoWidth': true,
868 'width': 'auto',
868 'width': 'auto',
869 'placeholder': "${file_placeholder}",
869 'placeholder': "${file_placeholder}",
870 containerCssClass: "drop-menu",
870 containerCssClass: "drop-menu",
871 dropdownCssClass: "drop-menu-dropdown",
871 dropdownCssClass: "drop-menu-dropdown",
872 data: preloadFileFilterData,
872 data: preloadFileFilterData,
873 query: function(query) {
873 query: function(query) {
874 feedFilesOptions(query, preloadFileFilterData);
874 feedFilesOptions(query, preloadFileFilterData);
875 },
875 },
876 formatResult: formatFileResult
876 formatResult: formatFileResult
877 });
877 });
878
878
879 % if range_diff_on is True:
879 % if range_diff_on is True:
880 fileFilter.select2("enable", false);
880 fileFilter.select2("enable", false);
881 % endif
881 % endif
882
882
883 $("#file_filter").on('click', function (e) {
883 $("#file_filter").on('click', function (e) {
884 e.preventDefault();
884 e.preventDefault();
885 var selected = $('#file_filter').select2('data');
885 var selected = $('#file_filter').select2('data');
886 var idSelector = "#"+selected.id;
886 var idSelector = "#"+selected.id;
887 window.location.hash = idSelector;
887 window.location.hash = idSelector;
888 // expand the container if we quick-select the field
888 // expand the container if we quick-select the field
889 $(idSelector).next().prop('checked', false);
889 $(idSelector).next().prop('checked', false);
890 updateSticky()
890 updateSticky()
891 });
891 });
892
892
893 var contextPrefix = _gettext('Context file: ');
893 var contextPrefix = _gettext('Context file: ');
894 ## sticky sidebar
894 ## sticky sidebar
895 var sidebarElement = document.getElementById('diff-file-sticky');
895 var sidebarElement = document.getElementById('diff-file-sticky');
896 sidebar = new StickySidebar(sidebarElement, {
896 sidebar = new StickySidebar(sidebarElement, {
897 topSpacing: 0,
897 topSpacing: 0,
898 bottomSpacing: 0,
898 bottomSpacing: 0,
899 innerWrapperSelector: '.sidebar__inner'
899 innerWrapperSelector: '.sidebar__inner'
900 });
900 });
901 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
901 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
902 // reset our file so it's not holding new value
902 // reset our file so it's not holding new value
903 $('.fpath-placeholder-text').html(contextPrefix)
903 $('.fpath-placeholder-text').html(contextPrefix)
904 });
904 });
905
905
906 updateSticky = function () {
906 updateSticky = function () {
907 sidebar.updateSticky();
907 sidebar.updateSticky();
908 Waypoint.refreshAll();
908 Waypoint.refreshAll();
909 };
909 };
910
910
911 var animateText = $.debounce(100, function(fPath, anchorId) {
911 var animateText = $.debounce(100, function(fPath, anchorId) {
912 fPath = Select2.util.escapeMarkup(fPath);
912 fPath = Select2.util.escapeMarkup(fPath);
913
913
914 // animate setting the text
914 // animate setting the text
915 var callback = function () {
915 var callback = function () {
916 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
916 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
917 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
917 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
918 };
918 };
919 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
919 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
920 });
920 });
921
921
922 ## dynamic file waypoints
922 ## dynamic file waypoints
923 var setFPathInfo = function(fPath, anchorId){
923 var setFPathInfo = function(fPath, anchorId){
924 animateText(fPath, anchorId)
924 animateText(fPath, anchorId)
925 };
925 };
926
926
927 var codeBlock = $('.filediff');
927 var codeBlock = $('.filediff');
928 // forward waypoint
928 // forward waypoint
929 codeBlock.waypoint(
929 codeBlock.waypoint(
930 function(direction) {
930 function(direction) {
931 if (direction === "down"){
931 if (direction === "down"){
932 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
932 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
933 }
933 }
934 }, {
934 }, {
935 offset: 70,
935 offset: 70,
936 context: '.fpath-placeholder'
936 context: '.fpath-placeholder'
937 }
937 }
938 );
938 );
939
939
940 // backward waypoint
940 // backward waypoint
941 codeBlock.waypoint(
941 codeBlock.waypoint(
942 function(direction) {
942 function(direction) {
943 if (direction === "up"){
943 if (direction === "up"){
944 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
944 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
945 }
945 }
946 }, {
946 }, {
947 offset: function () {
947 offset: function () {
948 return -this.element.clientHeight + 90
948 return -this.element.clientHeight + 90
949 },
949 },
950 context: '.fpath-placeholder'
950 context: '.fpath-placeholder'
951 }
951 }
952 );
952 );
953
953
954 var preloadDiffMenuData = {
954 var preloadDiffMenuData = {
955 results: [
955 results: [
956 ## Wide diff mode
956 ## Wide diff mode
957 {
957 {
958 id: 1,
958 id: 1,
959 text: _gettext('Toggle Wide Mode diff'),
959 text: _gettext('Toggle Wide Mode diff'),
960 action: function () {
960 action: function () {
961 updateSticky();
961 updateSticky();
962 Rhodecode.comments.toggleWideMode(this);
962 var wide = Rhodecode.comments.toggleWideMode(this);
963 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
963 return null;
964 return null;
964 },
965 },
965 url: null,
966 url: null,
966 },
967 },
967
968
968 ## Whitespace change
969 ## Whitespace change
969 % if request.GET.get('ignorews', '') == '1':
970 % if request.GET.get('ignorews', '') == '1':
970 {
971 {
971 id: 2,
972 id: 2,
972 text: _gettext('Show whitespace changes'),
973 text: _gettext('Show whitespace changes'),
973 action: function () {},
974 action: function () {},
974 url: "${h.current_route_path(request, ignorews=0)|n}"
975 url: "${h.current_route_path(request, ignorews=0)|n}"
975 },
976 },
976 % else:
977 % else:
977 {
978 {
978 id: 2,
979 id: 2,
979 text: _gettext('Hide whitespace changes'),
980 text: _gettext('Hide whitespace changes'),
980 action: function () {},
981 action: function () {},
981 url: "${h.current_route_path(request, ignorews=1)|n}"
982 url: "${h.current_route_path(request, ignorews=1)|n}"
982 },
983 },
983 % endif
984 % endif
984
985
985 ## FULL CONTEXT
986 ## FULL CONTEXT
986 % if request.GET.get('fullcontext', '') == '1':
987 % if request.GET.get('fullcontext', '') == '1':
987 {
988 {
988 id: 3,
989 id: 3,
989 text: _gettext('Hide full context diff'),
990 text: _gettext('Hide full context diff'),
990 action: function () {},
991 action: function () {},
991 url: "${h.current_route_path(request, fullcontext=0)|n}"
992 url: "${h.current_route_path(request, fullcontext=0)|n}"
992 },
993 },
993 % else:
994 % else:
994 {
995 {
995 id: 3,
996 id: 3,
996 text: _gettext('Show full context diff'),
997 text: _gettext('Show full context diff'),
997 action: function () {},
998 action: function () {},
998 url: "${h.current_route_path(request, fullcontext=1)|n}"
999 url: "${h.current_route_path(request, fullcontext=1)|n}"
999 },
1000 },
1000 % endif
1001 % endif
1001
1002
1002 ]
1003 ]
1003 };
1004 };
1004
1005
1006 // get stored diff mode and pre-enable it
1007 if (templateContext.session_attrs.wide_diff_mode === "true") {
1008 Rhodecode.comments.toggleWideMode(null);
1009 }
1010
1005 $("#diff_menu").select2({
1011 $("#diff_menu").select2({
1006 minimumResultsForSearch: -1,
1012 minimumResultsForSearch: -1,
1007 containerCssClass: "drop-menu",
1013 containerCssClass: "drop-menu",
1008 dropdownCssClass: "drop-menu-dropdown",
1014 dropdownCssClass: "drop-menu-dropdown",
1009 dropdownAutoWidth: true,
1015 dropdownAutoWidth: true,
1010 data: preloadDiffMenuData,
1016 data: preloadDiffMenuData,
1011 placeholder: "${_('Diff Options')}",
1017 placeholder: "${_('Diff Options')}",
1012 });
1018 });
1013 $("#diff_menu").on('select2-selecting', function (e) {
1019 $("#diff_menu").on('select2-selecting', function (e) {
1014 e.choice.action();
1020 e.choice.action();
1015 if (e.choice.url !== null) {
1021 if (e.choice.url !== null) {
1016 window.location = e.choice.url
1022 window.location = e.choice.url
1017 }
1023 }
1018 });
1024 });
1019
1025
1020 });
1026 });
1021
1027
1022 </script>
1028 </script>
1023 % endif
1029 % endif
1024
1030
1025 </%def>
1031 </%def>
General Comments 0
You need to be logged in to leave comments. Login now