##// END OF EJS Templates
datagrids: save permanently the state if sorting for pull-request grids.
milka -
r4558:c3119a6d default
parent child Browse files
Show More
@@ -1,823 +1,826 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode import forms
32 from rhodecode import forms
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.lib import audit_logger
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.auth import (
36 from rhodecode.lib.auth import (
37 LoginRequired, NotAnonymous, CSRFRequired,
37 LoginRequired, NotAnonymous, CSRFRequired,
38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 from rhodecode.lib.channelstream import (
39 from rhodecode.lib.channelstream import (
40 channelstream_request, ChannelstreamException)
40 channelstream_request, ChannelstreamException)
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.auth_token import AuthTokenModel
43 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.comment import CommentsModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 IntegrityError, or_, in_filter_generator,
45 IntegrityError, or_, in_filter_generator,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 PullRequest, UserBookmark, RepoGroup)
47 PullRequest, UserBookmark, RepoGroup)
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
50 from rhodecode.model.user import UserModel
50 from rhodecode.model.user import UserModel
51 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.validation_schema.schemas import user_schema
52 from rhodecode.model.validation_schema.schemas import user_schema
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class MyAccountView(BaseAppView, DataGridAppView):
57 class MyAccountView(BaseAppView, DataGridAppView):
58 ALLOW_SCOPED_TOKENS = False
58 ALLOW_SCOPED_TOKENS = False
59 """
59 """
60 This view has alternative version inside EE, if modified please take a look
60 This view has alternative version inside EE, if modified please take a look
61 in there as well.
61 in there as well.
62 """
62 """
63
63
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context()
65 c = self._get_local_tmpl_context()
66 c.user = c.auth_user.get_instance()
66 c.user = c.auth_user.get_instance()
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68
68
69 return c
69 return c
70
70
71 @LoginRequired()
71 @LoginRequired()
72 @NotAnonymous()
72 @NotAnonymous()
73 @view_config(
73 @view_config(
74 route_name='my_account_profile', request_method='GET',
74 route_name='my_account_profile', request_method='GET',
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 def my_account_profile(self):
76 def my_account_profile(self):
77 c = self.load_default_context()
77 c = self.load_default_context()
78 c.active = 'profile'
78 c.active = 'profile'
79 c.extern_type = c.user.extern_type
79 c.extern_type = c.user.extern_type
80 return self._get_template_context(c)
80 return self._get_template_context(c)
81
81
82 @LoginRequired()
82 @LoginRequired()
83 @NotAnonymous()
83 @NotAnonymous()
84 @view_config(
84 @view_config(
85 route_name='my_account_password', request_method='GET',
85 route_name='my_account_password', request_method='GET',
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
87 def my_account_password(self):
87 def my_account_password(self):
88 c = self.load_default_context()
88 c = self.load_default_context()
89 c.active = 'password'
89 c.active = 'password'
90 c.extern_type = c.user.extern_type
90 c.extern_type = c.user.extern_type
91
91
92 schema = user_schema.ChangePasswordSchema().bind(
92 schema = user_schema.ChangePasswordSchema().bind(
93 username=c.user.username)
93 username=c.user.username)
94
94
95 form = forms.Form(
95 form = forms.Form(
96 schema,
96 schema,
97 action=h.route_path('my_account_password_update'),
97 action=h.route_path('my_account_password_update'),
98 buttons=(forms.buttons.save, forms.buttons.reset))
98 buttons=(forms.buttons.save, forms.buttons.reset))
99
99
100 c.form = form
100 c.form = form
101 return self._get_template_context(c)
101 return self._get_template_context(c)
102
102
103 @LoginRequired()
103 @LoginRequired()
104 @NotAnonymous()
104 @NotAnonymous()
105 @CSRFRequired()
105 @CSRFRequired()
106 @view_config(
106 @view_config(
107 route_name='my_account_password_update', request_method='POST',
107 route_name='my_account_password_update', request_method='POST',
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 def my_account_password_update(self):
109 def my_account_password_update(self):
110 _ = self.request.translate
110 _ = self.request.translate
111 c = self.load_default_context()
111 c = self.load_default_context()
112 c.active = 'password'
112 c.active = 'password'
113 c.extern_type = c.user.extern_type
113 c.extern_type = c.user.extern_type
114
114
115 schema = user_schema.ChangePasswordSchema().bind(
115 schema = user_schema.ChangePasswordSchema().bind(
116 username=c.user.username)
116 username=c.user.username)
117
117
118 form = forms.Form(
118 form = forms.Form(
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
119 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120
120
121 if c.extern_type != 'rhodecode':
121 if c.extern_type != 'rhodecode':
122 raise HTTPFound(self.request.route_path('my_account_password'))
122 raise HTTPFound(self.request.route_path('my_account_password'))
123
123
124 controls = self.request.POST.items()
124 controls = self.request.POST.items()
125 try:
125 try:
126 valid_data = form.validate(controls)
126 valid_data = form.validate(controls)
127 UserModel().update_user(c.user.user_id, **valid_data)
127 UserModel().update_user(c.user.user_id, **valid_data)
128 c.user.update_userdata(force_password_change=False)
128 c.user.update_userdata(force_password_change=False)
129 Session().commit()
129 Session().commit()
130 except forms.ValidationFailure as e:
130 except forms.ValidationFailure as e:
131 c.form = e
131 c.form = e
132 return self._get_template_context(c)
132 return self._get_template_context(c)
133
133
134 except Exception:
134 except Exception:
135 log.exception("Exception updating password")
135 log.exception("Exception updating password")
136 h.flash(_('Error occurred during update of user password'),
136 h.flash(_('Error occurred during update of user password'),
137 category='error')
137 category='error')
138 else:
138 else:
139 instance = c.auth_user.get_instance()
139 instance = c.auth_user.get_instance()
140 self.session.setdefault('rhodecode_user', {}).update(
140 self.session.setdefault('rhodecode_user', {}).update(
141 {'password': md5(instance.password)})
141 {'password': md5(instance.password)})
142 self.session.save()
142 self.session.save()
143 h.flash(_("Successfully updated password"), category='success')
143 h.flash(_("Successfully updated password"), category='success')
144
144
145 raise HTTPFound(self.request.route_path('my_account_password'))
145 raise HTTPFound(self.request.route_path('my_account_password'))
146
146
147 @LoginRequired()
147 @LoginRequired()
148 @NotAnonymous()
148 @NotAnonymous()
149 @view_config(
149 @view_config(
150 route_name='my_account_auth_tokens', request_method='GET',
150 route_name='my_account_auth_tokens', request_method='GET',
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 def my_account_auth_tokens(self):
152 def my_account_auth_tokens(self):
153 _ = self.request.translate
153 _ = self.request.translate
154
154
155 c = self.load_default_context()
155 c = self.load_default_context()
156 c.active = 'auth_tokens'
156 c.active = 'auth_tokens'
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
157 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 c.role_values = [
158 c.role_values = [
159 (x, AuthTokenModel.cls._get_role_name(x))
159 (x, AuthTokenModel.cls._get_role_name(x))
160 for x in AuthTokenModel.cls.ROLES]
160 for x in AuthTokenModel.cls.ROLES]
161 c.role_options = [(c.role_values, _("Role"))]
161 c.role_options = [(c.role_values, _("Role"))]
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
162 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 c.user.user_id, show_expired=True)
163 c.user.user_id, show_expired=True)
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
164 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 return self._get_template_context(c)
165 return self._get_template_context(c)
166
166
167 @LoginRequired()
167 @LoginRequired()
168 @NotAnonymous()
168 @NotAnonymous()
169 @CSRFRequired()
169 @CSRFRequired()
170 @view_config(
170 @view_config(
171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
172 renderer='json_ext')
172 renderer='json_ext')
173 def my_account_auth_tokens_view(self):
173 def my_account_auth_tokens_view(self):
174 _ = self.request.translate
174 _ = self.request.translate
175 c = self.load_default_context()
175 c = self.load_default_context()
176
176
177 auth_token_id = self.request.POST.get('auth_token_id')
177 auth_token_id = self.request.POST.get('auth_token_id')
178
178
179 if auth_token_id:
179 if auth_token_id:
180 token = UserApiKeys.get_or_404(auth_token_id)
180 token = UserApiKeys.get_or_404(auth_token_id)
181 if token.user.user_id != c.user.user_id:
181 if token.user.user_id != c.user.user_id:
182 raise HTTPNotFound()
182 raise HTTPNotFound()
183
183
184 return {
184 return {
185 'auth_token': token.api_key
185 'auth_token': token.api_key
186 }
186 }
187
187
188 def maybe_attach_token_scope(self, token):
188 def maybe_attach_token_scope(self, token):
189 # implemented in EE edition
189 # implemented in EE edition
190 pass
190 pass
191
191
192 @LoginRequired()
192 @LoginRequired()
193 @NotAnonymous()
193 @NotAnonymous()
194 @CSRFRequired()
194 @CSRFRequired()
195 @view_config(
195 @view_config(
196 route_name='my_account_auth_tokens_add', request_method='POST',)
196 route_name='my_account_auth_tokens_add', request_method='POST',)
197 def my_account_auth_tokens_add(self):
197 def my_account_auth_tokens_add(self):
198 _ = self.request.translate
198 _ = self.request.translate
199 c = self.load_default_context()
199 c = self.load_default_context()
200
200
201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
201 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 description = self.request.POST.get('description')
202 description = self.request.POST.get('description')
203 role = self.request.POST.get('role')
203 role = self.request.POST.get('role')
204
204
205 token = UserModel().add_auth_token(
205 token = UserModel().add_auth_token(
206 user=c.user.user_id,
206 user=c.user.user_id,
207 lifetime_minutes=lifetime, role=role, description=description,
207 lifetime_minutes=lifetime, role=role, description=description,
208 scope_callback=self.maybe_attach_token_scope)
208 scope_callback=self.maybe_attach_token_scope)
209 token_data = token.get_api_data()
209 token_data = token.get_api_data()
210
210
211 audit_logger.store_web(
211 audit_logger.store_web(
212 'user.edit.token.add', action_data={
212 'user.edit.token.add', action_data={
213 'data': {'token': token_data, 'user': 'self'}},
213 'data': {'token': token_data, 'user': 'self'}},
214 user=self._rhodecode_user, )
214 user=self._rhodecode_user, )
215 Session().commit()
215 Session().commit()
216
216
217 h.flash(_("Auth token successfully created"), category='success')
217 h.flash(_("Auth token successfully created"), category='success')
218 return HTTPFound(h.route_path('my_account_auth_tokens'))
218 return HTTPFound(h.route_path('my_account_auth_tokens'))
219
219
220 @LoginRequired()
220 @LoginRequired()
221 @NotAnonymous()
221 @NotAnonymous()
222 @CSRFRequired()
222 @CSRFRequired()
223 @view_config(
223 @view_config(
224 route_name='my_account_auth_tokens_delete', request_method='POST')
224 route_name='my_account_auth_tokens_delete', request_method='POST')
225 def my_account_auth_tokens_delete(self):
225 def my_account_auth_tokens_delete(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 del_auth_token = self.request.POST.get('del_auth_token')
229 del_auth_token = self.request.POST.get('del_auth_token')
230
230
231 if del_auth_token:
231 if del_auth_token:
232 token = UserApiKeys.get_or_404(del_auth_token)
232 token = UserApiKeys.get_or_404(del_auth_token)
233 token_data = token.get_api_data()
233 token_data = token.get_api_data()
234
234
235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
235 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 audit_logger.store_web(
236 audit_logger.store_web(
237 'user.edit.token.delete', action_data={
237 'user.edit.token.delete', action_data={
238 'data': {'token': token_data, 'user': 'self'}},
238 'data': {'token': token_data, 'user': 'self'}},
239 user=self._rhodecode_user,)
239 user=self._rhodecode_user,)
240 Session().commit()
240 Session().commit()
241 h.flash(_("Auth token successfully deleted"), category='success')
241 h.flash(_("Auth token successfully deleted"), category='success')
242
242
243 return HTTPFound(h.route_path('my_account_auth_tokens'))
243 return HTTPFound(h.route_path('my_account_auth_tokens'))
244
244
245 @LoginRequired()
245 @LoginRequired()
246 @NotAnonymous()
246 @NotAnonymous()
247 @view_config(
247 @view_config(
248 route_name='my_account_emails', request_method='GET',
248 route_name='my_account_emails', request_method='GET',
249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
250 def my_account_emails(self):
250 def my_account_emails(self):
251 _ = self.request.translate
251 _ = self.request.translate
252
252
253 c = self.load_default_context()
253 c = self.load_default_context()
254 c.active = 'emails'
254 c.active = 'emails'
255
255
256 c.user_email_map = UserEmailMap.query()\
256 c.user_email_map = UserEmailMap.query()\
257 .filter(UserEmailMap.user == c.user).all()
257 .filter(UserEmailMap.user == c.user).all()
258
258
259 schema = user_schema.AddEmailSchema().bind(
259 schema = user_schema.AddEmailSchema().bind(
260 username=c.user.username, user_emails=c.user.emails)
260 username=c.user.username, user_emails=c.user.emails)
261
261
262 form = forms.RcForm(schema,
262 form = forms.RcForm(schema,
263 action=h.route_path('my_account_emails_add'),
263 action=h.route_path('my_account_emails_add'),
264 buttons=(forms.buttons.save, forms.buttons.reset))
264 buttons=(forms.buttons.save, forms.buttons.reset))
265
265
266 c.form = form
266 c.form = form
267 return self._get_template_context(c)
267 return self._get_template_context(c)
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @NotAnonymous()
270 @NotAnonymous()
271 @CSRFRequired()
271 @CSRFRequired()
272 @view_config(
272 @view_config(
273 route_name='my_account_emails_add', request_method='POST',
273 route_name='my_account_emails_add', request_method='POST',
274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
275 def my_account_emails_add(self):
275 def my_account_emails_add(self):
276 _ = self.request.translate
276 _ = self.request.translate
277 c = self.load_default_context()
277 c = self.load_default_context()
278 c.active = 'emails'
278 c.active = 'emails'
279
279
280 schema = user_schema.AddEmailSchema().bind(
280 schema = user_schema.AddEmailSchema().bind(
281 username=c.user.username, user_emails=c.user.emails)
281 username=c.user.username, user_emails=c.user.emails)
282
282
283 form = forms.RcForm(
283 form = forms.RcForm(
284 schema, action=h.route_path('my_account_emails_add'),
284 schema, action=h.route_path('my_account_emails_add'),
285 buttons=(forms.buttons.save, forms.buttons.reset))
285 buttons=(forms.buttons.save, forms.buttons.reset))
286
286
287 controls = self.request.POST.items()
287 controls = self.request.POST.items()
288 try:
288 try:
289 valid_data = form.validate(controls)
289 valid_data = form.validate(controls)
290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
290 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
291 audit_logger.store_web(
291 audit_logger.store_web(
292 'user.edit.email.add', action_data={
292 'user.edit.email.add', action_data={
293 'data': {'email': valid_data['email'], 'user': 'self'}},
293 'data': {'email': valid_data['email'], 'user': 'self'}},
294 user=self._rhodecode_user,)
294 user=self._rhodecode_user,)
295 Session().commit()
295 Session().commit()
296 except formencode.Invalid as error:
296 except formencode.Invalid as error:
297 h.flash(h.escape(error.error_dict['email']), category='error')
297 h.flash(h.escape(error.error_dict['email']), category='error')
298 except forms.ValidationFailure as e:
298 except forms.ValidationFailure as e:
299 c.user_email_map = UserEmailMap.query() \
299 c.user_email_map = UserEmailMap.query() \
300 .filter(UserEmailMap.user == c.user).all()
300 .filter(UserEmailMap.user == c.user).all()
301 c.form = e
301 c.form = e
302 return self._get_template_context(c)
302 return self._get_template_context(c)
303 except Exception:
303 except Exception:
304 log.exception("Exception adding email")
304 log.exception("Exception adding email")
305 h.flash(_('Error occurred during adding email'),
305 h.flash(_('Error occurred during adding email'),
306 category='error')
306 category='error')
307 else:
307 else:
308 h.flash(_("Successfully added email"), category='success')
308 h.flash(_("Successfully added email"), category='success')
309
309
310 raise HTTPFound(self.request.route_path('my_account_emails'))
310 raise HTTPFound(self.request.route_path('my_account_emails'))
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @NotAnonymous()
313 @NotAnonymous()
314 @CSRFRequired()
314 @CSRFRequired()
315 @view_config(
315 @view_config(
316 route_name='my_account_emails_delete', request_method='POST')
316 route_name='my_account_emails_delete', request_method='POST')
317 def my_account_emails_delete(self):
317 def my_account_emails_delete(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 c = self.load_default_context()
319 c = self.load_default_context()
320
320
321 del_email_id = self.request.POST.get('del_email_id')
321 del_email_id = self.request.POST.get('del_email_id')
322 if del_email_id:
322 if del_email_id:
323 email = UserEmailMap.get_or_404(del_email_id).email
323 email = UserEmailMap.get_or_404(del_email_id).email
324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
324 UserModel().delete_extra_email(c.user.user_id, del_email_id)
325 audit_logger.store_web(
325 audit_logger.store_web(
326 'user.edit.email.delete', action_data={
326 'user.edit.email.delete', action_data={
327 'data': {'email': email, 'user': 'self'}},
327 'data': {'email': email, 'user': 'self'}},
328 user=self._rhodecode_user,)
328 user=self._rhodecode_user,)
329 Session().commit()
329 Session().commit()
330 h.flash(_("Email successfully deleted"),
330 h.flash(_("Email successfully deleted"),
331 category='success')
331 category='success')
332 return HTTPFound(h.route_path('my_account_emails'))
332 return HTTPFound(h.route_path('my_account_emails'))
333
333
334 @LoginRequired()
334 @LoginRequired()
335 @NotAnonymous()
335 @NotAnonymous()
336 @CSRFRequired()
336 @CSRFRequired()
337 @view_config(
337 @view_config(
338 route_name='my_account_notifications_test_channelstream',
338 route_name='my_account_notifications_test_channelstream',
339 request_method='POST', renderer='json_ext')
339 request_method='POST', renderer='json_ext')
340 def my_account_notifications_test_channelstream(self):
340 def my_account_notifications_test_channelstream(self):
341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
341 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
342 self._rhodecode_user.username, datetime.datetime.now())
342 self._rhodecode_user.username, datetime.datetime.now())
343 payload = {
343 payload = {
344 # 'channel': 'broadcast',
344 # 'channel': 'broadcast',
345 'type': 'message',
345 'type': 'message',
346 'timestamp': datetime.datetime.utcnow(),
346 'timestamp': datetime.datetime.utcnow(),
347 'user': 'system',
347 'user': 'system',
348 'pm_users': [self._rhodecode_user.username],
348 'pm_users': [self._rhodecode_user.username],
349 'message': {
349 'message': {
350 'message': message,
350 'message': message,
351 'level': 'info',
351 'level': 'info',
352 'topic': '/notifications'
352 'topic': '/notifications'
353 }
353 }
354 }
354 }
355
355
356 registry = self.request.registry
356 registry = self.request.registry
357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
357 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 channelstream_config = rhodecode_plugins.get('channelstream', {})
358 channelstream_config = rhodecode_plugins.get('channelstream', {})
359
359
360 try:
360 try:
361 channelstream_request(channelstream_config, [payload], '/message')
361 channelstream_request(channelstream_config, [payload], '/message')
362 except ChannelstreamException as e:
362 except ChannelstreamException as e:
363 log.exception('Failed to send channelstream data')
363 log.exception('Failed to send channelstream data')
364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
364 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
365 return {"response": 'Channelstream data sent. '
365 return {"response": 'Channelstream data sent. '
366 'You should see a new live message now.'}
366 'You should see a new live message now.'}
367
367
368 def _load_my_repos_data(self, watched=False):
368 def _load_my_repos_data(self, watched=False):
369
369
370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
370 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
371
371
372 if watched:
372 if watched:
373 # repos user watch
373 # repos user watch
374 repo_list = Session().query(
374 repo_list = Session().query(
375 Repository
375 Repository
376 ) \
376 ) \
377 .join(
377 .join(
378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
378 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
379 ) \
379 ) \
380 .filter(
380 .filter(
381 UserFollowing.user_id == self._rhodecode_user.user_id
381 UserFollowing.user_id == self._rhodecode_user.user_id
382 ) \
382 ) \
383 .filter(or_(
383 .filter(or_(
384 # generate multiple IN to fix limitation problems
384 # generate multiple IN to fix limitation problems
385 *in_filter_generator(Repository.repo_id, allowed_ids))
385 *in_filter_generator(Repository.repo_id, allowed_ids))
386 ) \
386 ) \
387 .order_by(Repository.repo_name) \
387 .order_by(Repository.repo_name) \
388 .all()
388 .all()
389
389
390 else:
390 else:
391 # repos user is owner of
391 # repos user is owner of
392 repo_list = Session().query(
392 repo_list = Session().query(
393 Repository
393 Repository
394 ) \
394 ) \
395 .filter(
395 .filter(
396 Repository.user_id == self._rhodecode_user.user_id
396 Repository.user_id == self._rhodecode_user.user_id
397 ) \
397 ) \
398 .filter(or_(
398 .filter(or_(
399 # generate multiple IN to fix limitation problems
399 # generate multiple IN to fix limitation problems
400 *in_filter_generator(Repository.repo_id, allowed_ids))
400 *in_filter_generator(Repository.repo_id, allowed_ids))
401 ) \
401 ) \
402 .order_by(Repository.repo_name) \
402 .order_by(Repository.repo_name) \
403 .all()
403 .all()
404
404
405 _render = self.request.get_partial_renderer(
405 _render = self.request.get_partial_renderer(
406 'rhodecode:templates/data_table/_dt_elements.mako')
406 'rhodecode:templates/data_table/_dt_elements.mako')
407
407
408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
408 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
409 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
410 short_name=False, admin=False)
410 short_name=False, admin=False)
411
411
412 repos_data = []
412 repos_data = []
413 for repo in repo_list:
413 for repo in repo_list:
414 row = {
414 row = {
415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
415 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
416 repo.private, repo.archived, repo.fork),
416 repo.private, repo.archived, repo.fork),
417 "name_raw": repo.repo_name.lower(),
417 "name_raw": repo.repo_name.lower(),
418 }
418 }
419
419
420 repos_data.append(row)
420 repos_data.append(row)
421
421
422 # json used to render the grid
422 # json used to render the grid
423 return json.dumps(repos_data)
423 return json.dumps(repos_data)
424
424
425 @LoginRequired()
425 @LoginRequired()
426 @NotAnonymous()
426 @NotAnonymous()
427 @view_config(
427 @view_config(
428 route_name='my_account_repos', request_method='GET',
428 route_name='my_account_repos', request_method='GET',
429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
430 def my_account_repos(self):
430 def my_account_repos(self):
431 c = self.load_default_context()
431 c = self.load_default_context()
432 c.active = 'repos'
432 c.active = 'repos'
433
433
434 # json used to render the grid
434 # json used to render the grid
435 c.data = self._load_my_repos_data()
435 c.data = self._load_my_repos_data()
436 return self._get_template_context(c)
436 return self._get_template_context(c)
437
437
438 @LoginRequired()
438 @LoginRequired()
439 @NotAnonymous()
439 @NotAnonymous()
440 @view_config(
440 @view_config(
441 route_name='my_account_watched', request_method='GET',
441 route_name='my_account_watched', request_method='GET',
442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 def my_account_watched(self):
443 def my_account_watched(self):
444 c = self.load_default_context()
444 c = self.load_default_context()
445 c.active = 'watched'
445 c.active = 'watched'
446
446
447 # json used to render the grid
447 # json used to render the grid
448 c.data = self._load_my_repos_data(watched=True)
448 c.data = self._load_my_repos_data(watched=True)
449 return self._get_template_context(c)
449 return self._get_template_context(c)
450
450
451 @LoginRequired()
451 @LoginRequired()
452 @NotAnonymous()
452 @NotAnonymous()
453 @view_config(
453 @view_config(
454 route_name='my_account_bookmarks', request_method='GET',
454 route_name='my_account_bookmarks', request_method='GET',
455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
456 def my_account_bookmarks(self):
456 def my_account_bookmarks(self):
457 c = self.load_default_context()
457 c = self.load_default_context()
458 c.active = 'bookmarks'
458 c.active = 'bookmarks'
459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
459 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
460 self._rhodecode_db_user.user_id, cache=False)
460 self._rhodecode_db_user.user_id, cache=False)
461 return self._get_template_context(c)
461 return self._get_template_context(c)
462
462
463 def _process_bookmark_entry(self, entry, user_id):
463 def _process_bookmark_entry(self, entry, user_id):
464 position = safe_int(entry.get('position'))
464 position = safe_int(entry.get('position'))
465 cur_position = safe_int(entry.get('cur_position'))
465 cur_position = safe_int(entry.get('cur_position'))
466 if position is None:
466 if position is None:
467 return
467 return
468
468
469 # check if this is an existing entry
469 # check if this is an existing entry
470 is_new = False
470 is_new = False
471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
471 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
472
472
473 if db_entry and str2bool(entry.get('remove')):
473 if db_entry and str2bool(entry.get('remove')):
474 log.debug('Marked bookmark %s for deletion', db_entry)
474 log.debug('Marked bookmark %s for deletion', db_entry)
475 Session().delete(db_entry)
475 Session().delete(db_entry)
476 return
476 return
477
477
478 if not db_entry:
478 if not db_entry:
479 # new
479 # new
480 db_entry = UserBookmark()
480 db_entry = UserBookmark()
481 is_new = True
481 is_new = True
482
482
483 should_save = False
483 should_save = False
484 default_redirect_url = ''
484 default_redirect_url = ''
485
485
486 # save repo
486 # save repo
487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
487 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
488 repo = Repository.get(entry['bookmark_repo'])
488 repo = Repository.get(entry['bookmark_repo'])
489 perm_check = HasRepoPermissionAny(
489 perm_check = HasRepoPermissionAny(
490 'repository.read', 'repository.write', 'repository.admin')
490 'repository.read', 'repository.write', 'repository.admin')
491 if repo and perm_check(repo_name=repo.repo_name):
491 if repo and perm_check(repo_name=repo.repo_name):
492 db_entry.repository = repo
492 db_entry.repository = repo
493 should_save = True
493 should_save = True
494 default_redirect_url = '${repo_url}'
494 default_redirect_url = '${repo_url}'
495 # save repo group
495 # save repo group
496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
496 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
497 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
498 perm_check = HasRepoGroupPermissionAny(
498 perm_check = HasRepoGroupPermissionAny(
499 'group.read', 'group.write', 'group.admin')
499 'group.read', 'group.write', 'group.admin')
500
500
501 if repo_group and perm_check(group_name=repo_group.group_name):
501 if repo_group and perm_check(group_name=repo_group.group_name):
502 db_entry.repository_group = repo_group
502 db_entry.repository_group = repo_group
503 should_save = True
503 should_save = True
504 default_redirect_url = '${repo_group_url}'
504 default_redirect_url = '${repo_group_url}'
505 # save generic info
505 # save generic info
506 elif entry.get('title') and entry.get('redirect_url'):
506 elif entry.get('title') and entry.get('redirect_url'):
507 should_save = True
507 should_save = True
508
508
509 if should_save:
509 if should_save:
510 # mark user and position
510 # mark user and position
511 db_entry.user_id = user_id
511 db_entry.user_id = user_id
512 db_entry.position = position
512 db_entry.position = position
513 db_entry.title = entry.get('title')
513 db_entry.title = entry.get('title')
514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
514 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
515 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
516
516
517 Session().add(db_entry)
517 Session().add(db_entry)
518
518
519 @LoginRequired()
519 @LoginRequired()
520 @NotAnonymous()
520 @NotAnonymous()
521 @CSRFRequired()
521 @CSRFRequired()
522 @view_config(
522 @view_config(
523 route_name='my_account_bookmarks_update', request_method='POST')
523 route_name='my_account_bookmarks_update', request_method='POST')
524 def my_account_bookmarks_update(self):
524 def my_account_bookmarks_update(self):
525 _ = self.request.translate
525 _ = self.request.translate
526 c = self.load_default_context()
526 c = self.load_default_context()
527 c.active = 'bookmarks'
527 c.active = 'bookmarks'
528
528
529 controls = peppercorn.parse(self.request.POST.items())
529 controls = peppercorn.parse(self.request.POST.items())
530 user_id = c.user.user_id
530 user_id = c.user.user_id
531
531
532 # validate positions
532 # validate positions
533 positions = {}
533 positions = {}
534 for entry in controls.get('bookmarks', []):
534 for entry in controls.get('bookmarks', []):
535 position = safe_int(entry['position'])
535 position = safe_int(entry['position'])
536 if position is None:
536 if position is None:
537 continue
537 continue
538
538
539 if position in positions:
539 if position in positions:
540 h.flash(_("Position {} is defined twice. "
540 h.flash(_("Position {} is defined twice. "
541 "Please correct this error.").format(position), category='error')
541 "Please correct this error.").format(position), category='error')
542 return HTTPFound(h.route_path('my_account_bookmarks'))
542 return HTTPFound(h.route_path('my_account_bookmarks'))
543
543
544 entry['position'] = position
544 entry['position'] = position
545 entry['cur_position'] = safe_int(entry.get('cur_position'))
545 entry['cur_position'] = safe_int(entry.get('cur_position'))
546 positions[position] = entry
546 positions[position] = entry
547
547
548 try:
548 try:
549 for entry in positions.values():
549 for entry in positions.values():
550 self._process_bookmark_entry(entry, user_id)
550 self._process_bookmark_entry(entry, user_id)
551
551
552 Session().commit()
552 Session().commit()
553 h.flash(_("Update Bookmarks"), category='success')
553 h.flash(_("Update Bookmarks"), category='success')
554 except IntegrityError:
554 except IntegrityError:
555 h.flash(_("Failed to update bookmarks. "
555 h.flash(_("Failed to update bookmarks. "
556 "Make sure an unique position is used."), category='error')
556 "Make sure an unique position is used."), category='error')
557
557
558 return HTTPFound(h.route_path('my_account_bookmarks'))
558 return HTTPFound(h.route_path('my_account_bookmarks'))
559
559
560 @LoginRequired()
560 @LoginRequired()
561 @NotAnonymous()
561 @NotAnonymous()
562 @view_config(
562 @view_config(
563 route_name='my_account_goto_bookmark', request_method='GET',
563 route_name='my_account_goto_bookmark', request_method='GET',
564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
565 def my_account_goto_bookmark(self):
565 def my_account_goto_bookmark(self):
566
566
567 bookmark_id = self.request.matchdict['bookmark_id']
567 bookmark_id = self.request.matchdict['bookmark_id']
568 user_bookmark = UserBookmark().query()\
568 user_bookmark = UserBookmark().query()\
569 .filter(UserBookmark.user_id == self.request.user.user_id) \
569 .filter(UserBookmark.user_id == self.request.user.user_id) \
570 .filter(UserBookmark.position == bookmark_id).scalar()
570 .filter(UserBookmark.position == bookmark_id).scalar()
571
571
572 redirect_url = h.route_path('my_account_bookmarks')
572 redirect_url = h.route_path('my_account_bookmarks')
573 if not user_bookmark:
573 if not user_bookmark:
574 raise HTTPFound(redirect_url)
574 raise HTTPFound(redirect_url)
575
575
576 # repository set
576 # repository set
577 if user_bookmark.repository:
577 if user_bookmark.repository:
578 repo_name = user_bookmark.repository.repo_name
578 repo_name = user_bookmark.repository.repo_name
579 base_redirect_url = h.route_path(
579 base_redirect_url = h.route_path(
580 'repo_summary', repo_name=repo_name)
580 'repo_summary', repo_name=repo_name)
581 if user_bookmark.redirect_url and \
581 if user_bookmark.redirect_url and \
582 '${repo_url}' in user_bookmark.redirect_url:
582 '${repo_url}' in user_bookmark.redirect_url:
583 redirect_url = string.Template(user_bookmark.redirect_url)\
583 redirect_url = string.Template(user_bookmark.redirect_url)\
584 .safe_substitute({'repo_url': base_redirect_url})
584 .safe_substitute({'repo_url': base_redirect_url})
585 else:
585 else:
586 redirect_url = base_redirect_url
586 redirect_url = base_redirect_url
587 # repository group set
587 # repository group set
588 elif user_bookmark.repository_group:
588 elif user_bookmark.repository_group:
589 repo_group_name = user_bookmark.repository_group.group_name
589 repo_group_name = user_bookmark.repository_group.group_name
590 base_redirect_url = h.route_path(
590 base_redirect_url = h.route_path(
591 'repo_group_home', repo_group_name=repo_group_name)
591 'repo_group_home', repo_group_name=repo_group_name)
592 if user_bookmark.redirect_url and \
592 if user_bookmark.redirect_url and \
593 '${repo_group_url}' in user_bookmark.redirect_url:
593 '${repo_group_url}' in user_bookmark.redirect_url:
594 redirect_url = string.Template(user_bookmark.redirect_url)\
594 redirect_url = string.Template(user_bookmark.redirect_url)\
595 .safe_substitute({'repo_group_url': base_redirect_url})
595 .safe_substitute({'repo_group_url': base_redirect_url})
596 else:
596 else:
597 redirect_url = base_redirect_url
597 redirect_url = base_redirect_url
598 # custom URL set
598 # custom URL set
599 elif user_bookmark.redirect_url:
599 elif user_bookmark.redirect_url:
600 server_url = h.route_url('home').rstrip('/')
600 server_url = h.route_url('home').rstrip('/')
601 redirect_url = string.Template(user_bookmark.redirect_url) \
601 redirect_url = string.Template(user_bookmark.redirect_url) \
602 .safe_substitute({'server_url': server_url})
602 .safe_substitute({'server_url': server_url})
603
603
604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
604 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
605 raise HTTPFound(redirect_url)
605 raise HTTPFound(redirect_url)
606
606
607 @LoginRequired()
607 @LoginRequired()
608 @NotAnonymous()
608 @NotAnonymous()
609 @view_config(
609 @view_config(
610 route_name='my_account_perms', request_method='GET',
610 route_name='my_account_perms', request_method='GET',
611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
612 def my_account_perms(self):
612 def my_account_perms(self):
613 c = self.load_default_context()
613 c = self.load_default_context()
614 c.active = 'perms'
614 c.active = 'perms'
615
615
616 c.perm_user = c.auth_user
616 c.perm_user = c.auth_user
617 return self._get_template_context(c)
617 return self._get_template_context(c)
618
618
619 @LoginRequired()
619 @LoginRequired()
620 @NotAnonymous()
620 @NotAnonymous()
621 @view_config(
621 @view_config(
622 route_name='my_account_notifications', request_method='GET',
622 route_name='my_account_notifications', request_method='GET',
623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
624 def my_notifications(self):
624 def my_notifications(self):
625 c = self.load_default_context()
625 c = self.load_default_context()
626 c.active = 'notifications'
626 c.active = 'notifications'
627
627
628 return self._get_template_context(c)
628 return self._get_template_context(c)
629
629
630 @LoginRequired()
630 @LoginRequired()
631 @NotAnonymous()
631 @NotAnonymous()
632 @CSRFRequired()
632 @CSRFRequired()
633 @view_config(
633 @view_config(
634 route_name='my_account_notifications_toggle_visibility',
634 route_name='my_account_notifications_toggle_visibility',
635 request_method='POST', renderer='json_ext')
635 request_method='POST', renderer='json_ext')
636 def my_notifications_toggle_visibility(self):
636 def my_notifications_toggle_visibility(self):
637 user = self._rhodecode_db_user
637 user = self._rhodecode_db_user
638 new_status = not user.user_data.get('notification_status', True)
638 new_status = not user.user_data.get('notification_status', True)
639 user.update_userdata(notification_status=new_status)
639 user.update_userdata(notification_status=new_status)
640 Session().commit()
640 Session().commit()
641 return user.user_data['notification_status']
641 return user.user_data['notification_status']
642
642
643 @LoginRequired()
643 @LoginRequired()
644 @NotAnonymous()
644 @NotAnonymous()
645 @view_config(
645 @view_config(
646 route_name='my_account_edit',
646 route_name='my_account_edit',
647 request_method='GET',
647 request_method='GET',
648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
649 def my_account_edit(self):
649 def my_account_edit(self):
650 c = self.load_default_context()
650 c = self.load_default_context()
651 c.active = 'profile_edit'
651 c.active = 'profile_edit'
652 c.extern_type = c.user.extern_type
652 c.extern_type = c.user.extern_type
653 c.extern_name = c.user.extern_name
653 c.extern_name = c.user.extern_name
654
654
655 schema = user_schema.UserProfileSchema().bind(
655 schema = user_schema.UserProfileSchema().bind(
656 username=c.user.username, user_emails=c.user.emails)
656 username=c.user.username, user_emails=c.user.emails)
657 appstruct = {
657 appstruct = {
658 'username': c.user.username,
658 'username': c.user.username,
659 'email': c.user.email,
659 'email': c.user.email,
660 'firstname': c.user.firstname,
660 'firstname': c.user.firstname,
661 'lastname': c.user.lastname,
661 'lastname': c.user.lastname,
662 'description': c.user.description,
662 'description': c.user.description,
663 }
663 }
664 c.form = forms.RcForm(
664 c.form = forms.RcForm(
665 schema, appstruct=appstruct,
665 schema, appstruct=appstruct,
666 action=h.route_path('my_account_update'),
666 action=h.route_path('my_account_update'),
667 buttons=(forms.buttons.save, forms.buttons.reset))
667 buttons=(forms.buttons.save, forms.buttons.reset))
668
668
669 return self._get_template_context(c)
669 return self._get_template_context(c)
670
670
671 @LoginRequired()
671 @LoginRequired()
672 @NotAnonymous()
672 @NotAnonymous()
673 @CSRFRequired()
673 @CSRFRequired()
674 @view_config(
674 @view_config(
675 route_name='my_account_update',
675 route_name='my_account_update',
676 request_method='POST',
676 request_method='POST',
677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
678 def my_account_update(self):
678 def my_account_update(self):
679 _ = self.request.translate
679 _ = self.request.translate
680 c = self.load_default_context()
680 c = self.load_default_context()
681 c.active = 'profile_edit'
681 c.active = 'profile_edit'
682 c.perm_user = c.auth_user
682 c.perm_user = c.auth_user
683 c.extern_type = c.user.extern_type
683 c.extern_type = c.user.extern_type
684 c.extern_name = c.user.extern_name
684 c.extern_name = c.user.extern_name
685
685
686 schema = user_schema.UserProfileSchema().bind(
686 schema = user_schema.UserProfileSchema().bind(
687 username=c.user.username, user_emails=c.user.emails)
687 username=c.user.username, user_emails=c.user.emails)
688 form = forms.RcForm(
688 form = forms.RcForm(
689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
690
690
691 controls = self.request.POST.items()
691 controls = self.request.POST.items()
692 try:
692 try:
693 valid_data = form.validate(controls)
693 valid_data = form.validate(controls)
694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
695 'new_password', 'password_confirmation']
695 'new_password', 'password_confirmation']
696 if c.extern_type != "rhodecode":
696 if c.extern_type != "rhodecode":
697 # forbid updating username for external accounts
697 # forbid updating username for external accounts
698 skip_attrs.append('username')
698 skip_attrs.append('username')
699 old_email = c.user.email
699 old_email = c.user.email
700 UserModel().update_user(
700 UserModel().update_user(
701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
702 **valid_data)
702 **valid_data)
703 if old_email != valid_data['email']:
703 if old_email != valid_data['email']:
704 old = UserEmailMap.query() \
704 old = UserEmailMap.query() \
705 .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first()
705 .filter(UserEmailMap.user == c.user)\
706 .filter(UserEmailMap.email == valid_data['email'])\
707 .first()
706 old.email = old_email
708 old.email = old_email
707 h.flash(_('Your account was updated successfully'), category='success')
709 h.flash(_('Your account was updated successfully'), category='success')
708 Session().commit()
710 Session().commit()
709 except forms.ValidationFailure as e:
711 except forms.ValidationFailure as e:
710 c.form = e
712 c.form = e
711 return self._get_template_context(c)
713 return self._get_template_context(c)
712 except Exception:
714 except Exception:
713 log.exception("Exception updating user")
715 log.exception("Exception updating user")
714 h.flash(_('Error occurred during update of user'),
716 h.flash(_('Error occurred during update of user'),
715 category='error')
717 category='error')
716 raise HTTPFound(h.route_path('my_account_profile'))
718 raise HTTPFound(h.route_path('my_account_profile'))
717
719
718 def _get_pull_requests_list(self, statuses):
720 def _get_pull_requests_list(self, statuses):
719 draw, start, limit = self._extract_chunk(self.request)
721 draw, start, limit = self._extract_chunk(self.request)
720 search_q, order_by, order_dir = self._extract_ordering(self.request)
722 search_q, order_by, order_dir = self._extract_ordering(self.request)
723
721 _render = self.request.get_partial_renderer(
724 _render = self.request.get_partial_renderer(
722 'rhodecode:templates/data_table/_dt_elements.mako')
725 'rhodecode:templates/data_table/_dt_elements.mako')
723
726
724 pull_requests = PullRequestModel().get_im_participating_in(
727 pull_requests = PullRequestModel().get_im_participating_in(
725 user_id=self._rhodecode_user.user_id,
728 user_id=self._rhodecode_user.user_id,
726 statuses=statuses, query=search_q,
729 statuses=statuses, query=search_q,
727 offset=start, length=limit, order_by=order_by,
730 offset=start, length=limit, order_by=order_by,
728 order_dir=order_dir)
731 order_dir=order_dir)
729
732
730 pull_requests_total_count = PullRequestModel().count_im_participating_in(
733 pull_requests_total_count = PullRequestModel().count_im_participating_in(
731 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
734 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
732
735
733 data = []
736 data = []
734 comments_model = CommentsModel()
737 comments_model = CommentsModel()
735 for pr in pull_requests:
738 for pr in pull_requests:
736 repo_id = pr.target_repo_id
739 repo_id = pr.target_repo_id
737 comments_count = comments_model.get_all_comments(
740 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr, include_drafts=False, count_only=True)
741 repo_id, pull_request=pr, include_drafts=False, count_only=True)
739 owned = pr.user_id == self._rhodecode_user.user_id
742 owned = pr.user_id == self._rhodecode_user.user_id
740
743
741 data.append({
744 data.append({
742 'target_repo': _render('pullrequest_target_repo',
745 'target_repo': _render('pullrequest_target_repo',
743 pr.target_repo.repo_name),
746 pr.target_repo.repo_name),
744 'name': _render('pullrequest_name',
747 'name': _render('pullrequest_name',
745 pr.pull_request_id, pr.pull_request_state,
748 pr.pull_request_id, pr.pull_request_state,
746 pr.work_in_progress, pr.target_repo.repo_name,
749 pr.work_in_progress, pr.target_repo.repo_name,
747 short=True),
750 short=True),
748 'name_raw': pr.pull_request_id,
751 'name_raw': pr.pull_request_id,
749 'status': _render('pullrequest_status',
752 'status': _render('pullrequest_status',
750 pr.calculated_review_status()),
753 pr.calculated_review_status()),
751 'title': _render('pullrequest_title', pr.title, pr.description),
754 'title': _render('pullrequest_title', pr.title, pr.description),
752 'description': h.escape(pr.description),
755 'description': h.escape(pr.description),
753 'updated_on': _render('pullrequest_updated_on',
756 'updated_on': _render('pullrequest_updated_on',
754 h.datetime_to_time(pr.updated_on),
757 h.datetime_to_time(pr.updated_on),
755 pr.versions_count),
758 pr.versions_count),
756 'updated_on_raw': h.datetime_to_time(pr.updated_on),
759 'updated_on_raw': h.datetime_to_time(pr.updated_on),
757 'created_on': _render('pullrequest_updated_on',
760 'created_on': _render('pullrequest_updated_on',
758 h.datetime_to_time(pr.created_on)),
761 h.datetime_to_time(pr.created_on)),
759 'created_on_raw': h.datetime_to_time(pr.created_on),
762 'created_on_raw': h.datetime_to_time(pr.created_on),
760 'state': pr.pull_request_state,
763 'state': pr.pull_request_state,
761 'author': _render('pullrequest_author',
764 'author': _render('pullrequest_author',
762 pr.author.full_contact, ),
765 pr.author.full_contact, ),
763 'author_raw': pr.author.full_name,
766 'author_raw': pr.author.full_name,
764 'comments': _render('pullrequest_comments', comments_count),
767 'comments': _render('pullrequest_comments', comments_count),
765 'comments_raw': comments_count,
768 'comments_raw': comments_count,
766 'closed': pr.is_closed(),
769 'closed': pr.is_closed(),
767 'owned': owned
770 'owned': owned
768 })
771 })
769
772
770 # json used to render the grid
773 # json used to render the grid
771 data = ({
774 data = ({
772 'draw': draw,
775 'draw': draw,
773 'data': data,
776 'data': data,
774 'recordsTotal': pull_requests_total_count,
777 'recordsTotal': pull_requests_total_count,
775 'recordsFiltered': pull_requests_total_count,
778 'recordsFiltered': pull_requests_total_count,
776 })
779 })
777 return data
780 return data
778
781
779 @LoginRequired()
782 @LoginRequired()
780 @NotAnonymous()
783 @NotAnonymous()
781 @view_config(
784 @view_config(
782 route_name='my_account_pullrequests',
785 route_name='my_account_pullrequests',
783 request_method='GET',
786 request_method='GET',
784 renderer='rhodecode:templates/admin/my_account/my_account.mako')
787 renderer='rhodecode:templates/admin/my_account/my_account.mako')
785 def my_account_pullrequests(self):
788 def my_account_pullrequests(self):
786 c = self.load_default_context()
789 c = self.load_default_context()
787 c.active = 'pullrequests'
790 c.active = 'pullrequests'
788 req_get = self.request.GET
791 req_get = self.request.GET
789
792
790 c.closed = str2bool(req_get.get('pr_show_closed'))
793 c.closed = str2bool(req_get.get('pr_show_closed'))
791
794
792 return self._get_template_context(c)
795 return self._get_template_context(c)
793
796
794 @LoginRequired()
797 @LoginRequired()
795 @NotAnonymous()
798 @NotAnonymous()
796 @view_config(
799 @view_config(
797 route_name='my_account_pullrequests_data',
800 route_name='my_account_pullrequests_data',
798 request_method='GET', renderer='json_ext')
801 request_method='GET', renderer='json_ext')
799 def my_account_pullrequests_data(self):
802 def my_account_pullrequests_data(self):
800 self.load_default_context()
803 self.load_default_context()
801 req_get = self.request.GET
804 req_get = self.request.GET
802 closed = str2bool(req_get.get('closed'))
805 closed = str2bool(req_get.get('closed'))
803
806
804 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
807 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
805 if closed:
808 if closed:
806 statuses += [PullRequest.STATUS_CLOSED]
809 statuses += [PullRequest.STATUS_CLOSED]
807
810
808 data = self._get_pull_requests_list(statuses=statuses)
811 data = self._get_pull_requests_list(statuses=statuses)
809 return data
812 return data
810
813
811 @LoginRequired()
814 @LoginRequired()
812 @NotAnonymous()
815 @NotAnonymous()
813 @view_config(
816 @view_config(
814 route_name='my_account_user_group_membership',
817 route_name='my_account_user_group_membership',
815 request_method='GET',
818 request_method='GET',
816 renderer='rhodecode:templates/admin/my_account/my_account.mako')
819 renderer='rhodecode:templates/admin/my_account/my_account.mako')
817 def my_account_user_group_membership(self):
820 def my_account_user_group_membership(self):
818 c = self.load_default_context()
821 c = self.load_default_context()
819 c.active = 'user_group_membership'
822 c.active = 'user_group_membership'
820 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
823 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
821 for group in self._rhodecode_db_user.group_member]
824 for group in self._rhodecode_db_user.group_member]
822 c.user_groups = json.dumps(groups)
825 c.user_groups = json.dumps(groups)
823 return self._get_template_context(c)
826 return self._get_template_context(c)
@@ -1,706 +1,716 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 /**
19 /**
20 RhodeCode JS Files
20 RhodeCode JS Files
21 **/
21 **/
22
22
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 console = { log: function() {} }
24 console = { log: function() {} }
25 }
25 }
26
26
27 // TODO: move the following function to submodules
27 // TODO: move the following function to submodules
28
28
29 /**
29 /**
30 * show more
30 * show more
31 */
31 */
32 var show_more_event = function(){
32 var show_more_event = function(){
33 $('table .show_more').click(function(e) {
33 $('table .show_more').click(function(e) {
34 var cid = e.target.id.substring(1);
34 var cid = e.target.id.substring(1);
35 var button = $(this);
35 var button = $(this);
36 if (button.hasClass('open')) {
36 if (button.hasClass('open')) {
37 $('#'+cid).hide();
37 $('#'+cid).hide();
38 button.removeClass('open');
38 button.removeClass('open');
39 } else {
39 } else {
40 $('#'+cid).show();
40 $('#'+cid).show();
41 button.addClass('open one');
41 button.addClass('open one');
42 }
42 }
43 });
43 });
44 };
44 };
45
45
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 $('#compare_action').on('click', function(e){
47 $('#compare_action').on('click', function(e){
48 e.preventDefault();
48 e.preventDefault();
49
49
50 var source = $('input[name=compare_source]:checked').val();
50 var source = $('input[name=compare_source]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
52 if(source && target){
52 if(source && target){
53 var url_data = {
53 var url_data = {
54 repo_name: repo_name,
54 repo_name: repo_name,
55 source_ref: source,
55 source_ref: source,
56 source_ref_type: compare_ref_type,
56 source_ref_type: compare_ref_type,
57 target_ref: target,
57 target_ref: target,
58 target_ref_type: compare_ref_type,
58 target_ref_type: compare_ref_type,
59 merge: 1
59 merge: 1
60 };
60 };
61 window.location = pyroutes.url('repo_compare', url_data);
61 window.location = pyroutes.url('repo_compare', url_data);
62 }
62 }
63 });
63 });
64 $('.compare-radio-button').on('click', function(e){
64 $('.compare-radio-button').on('click', function(e){
65 var source = $('input[name=compare_source]:checked').val();
65 var source = $('input[name=compare_source]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
67 if(source && target){
67 if(source && target){
68 $('#compare_action').removeAttr("disabled");
68 $('#compare_action').removeAttr("disabled");
69 $('#compare_action').removeClass("disabled");
69 $('#compare_action').removeClass("disabled");
70 }
70 }
71 })
71 })
72 };
72 };
73
73
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 var container = $('#' + target);
75 var container = $('#' + target);
76 var url = pyroutes.url('repo_stats',
76 var url = pyroutes.url('repo_stats',
77 {"repo_name": repo_name, "commit_id": commit_id});
77 {"repo_name": repo_name, "commit_id": commit_id});
78
78
79 container.show();
79 container.show();
80 if (!container.hasClass('loaded')) {
80 if (!container.hasClass('loaded')) {
81 $.ajax({url: url})
81 $.ajax({url: url})
82 .complete(function (data) {
82 .complete(function (data) {
83 var responseJSON = data.responseJSON;
83 var responseJSON = data.responseJSON;
84 container.addClass('loaded');
84 container.addClass('loaded');
85 container.html(responseJSON.size);
85 container.html(responseJSON.size);
86 callback(responseJSON.code_stats)
86 callback(responseJSON.code_stats)
87 })
87 })
88 .fail(function (data) {
88 .fail(function (data) {
89 console.log('failed to load repo stats');
89 console.log('failed to load repo stats');
90 });
90 });
91 }
91 }
92
92
93 };
93 };
94
94
95 var showRepoStats = function(target, data){
95 var showRepoStats = function(target, data){
96 var container = $('#' + target);
96 var container = $('#' + target);
97
97
98 if (container.hasClass('loaded')) {
98 if (container.hasClass('loaded')) {
99 return
99 return
100 }
100 }
101
101
102 var total = 0;
102 var total = 0;
103 var no_data = true;
103 var no_data = true;
104 var tbl = document.createElement('table');
104 var tbl = document.createElement('table');
105 tbl.setAttribute('class', 'trending_language_tbl rctable');
105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106
106
107 $.each(data, function(key, val){
107 $.each(data, function(key, val){
108 total += val.count;
108 total += val.count;
109 });
109 });
110
110
111 var sortedStats = [];
111 var sortedStats = [];
112 for (var obj in data){
112 for (var obj in data){
113 sortedStats.push([obj, data[obj]])
113 sortedStats.push([obj, data[obj]])
114 }
114 }
115 var sortedData = sortedStats.sort(function (a, b) {
115 var sortedData = sortedStats.sort(function (a, b) {
116 return b[1].count - a[1].count
116 return b[1].count - a[1].count
117 });
117 });
118 var cnt = 0;
118 var cnt = 0;
119 $.each(sortedData, function(idx, val){
119 $.each(sortedData, function(idx, val){
120 cnt += 1;
120 cnt += 1;
121 no_data = false;
121 no_data = false;
122
122
123 var tr = document.createElement('tr');
123 var tr = document.createElement('tr');
124
124
125 var key = val[0];
125 var key = val[0];
126 var obj = {"desc": val[1].desc, "count": val[1].count};
126 var obj = {"desc": val[1].desc, "count": val[1].count};
127
127
128 // meta language names
128 // meta language names
129 var td1 = document.createElement('td');
129 var td1 = document.createElement('td');
130 var trending_language_label = document.createElement('div');
130 var trending_language_label = document.createElement('div');
131 trending_language_label.innerHTML = obj.desc;
131 trending_language_label.innerHTML = obj.desc;
132 td1.appendChild(trending_language_label);
132 td1.appendChild(trending_language_label);
133
133
134 // extensions
134 // extensions
135 var td2 = document.createElement('td');
135 var td2 = document.createElement('td');
136 var extension = document.createElement('div');
136 var extension = document.createElement('div');
137 extension.innerHTML = ".{0}".format(key)
137 extension.innerHTML = ".{0}".format(key)
138 td2.appendChild(extension);
138 td2.appendChild(extension);
139
139
140 // number of files
140 // number of files
141 var td3 = document.createElement('td');
141 var td3 = document.createElement('td');
142 var file_count = document.createElement('div');
142 var file_count = document.createElement('div');
143 var percentage_num = Math.round((obj.count / total * 100), 2);
143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 var label = _ngettext('file', 'files', obj.count);
144 var label = _ngettext('file', 'files', obj.count);
145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 td3.appendChild(file_count);
146 td3.appendChild(file_count);
147
147
148 // percentage
148 // percentage
149 var td4 = document.createElement('td');
149 var td4 = document.createElement('td');
150 td4.setAttribute("class", 'trending_language');
150 td4.setAttribute("class", 'trending_language');
151
151
152 var percentage = document.createElement('div');
152 var percentage = document.createElement('div');
153 percentage.setAttribute('class', 'lang-bar');
153 percentage.setAttribute('class', 'lang-bar');
154 percentage.innerHTML = "&nbsp;";
154 percentage.innerHTML = "&nbsp;";
155 percentage.style.width = percentage_num + '%';
155 percentage.style.width = percentage_num + '%';
156 td4.appendChild(percentage);
156 td4.appendChild(percentage);
157
157
158 tr.appendChild(td1);
158 tr.appendChild(td1);
159 tr.appendChild(td2);
159 tr.appendChild(td2);
160 tr.appendChild(td3);
160 tr.appendChild(td3);
161 tr.appendChild(td4);
161 tr.appendChild(td4);
162 tbl.appendChild(tr);
162 tbl.appendChild(tr);
163
163
164 });
164 });
165
165
166 $(container).html(tbl);
166 $(container).html(tbl);
167 $(container).addClass('loaded');
167 $(container).addClass('loaded');
168
168
169 $('#code_stats_show_more').on('click', function (e) {
169 $('#code_stats_show_more').on('click', function (e) {
170 e.preventDefault();
170 e.preventDefault();
171 $('.stats_hidden').each(function (idx) {
171 $('.stats_hidden').each(function (idx) {
172 $(this).css("display", "");
172 $(this).css("display", "");
173 });
173 });
174 $('#code_stats_show_more').hide();
174 $('#code_stats_show_more').hide();
175 });
175 });
176
176
177 };
177 };
178
178
179 // returns a node from given html;
179 // returns a node from given html;
180 var fromHTML = function(html){
180 var fromHTML = function(html){
181 var _html = document.createElement('element');
181 var _html = document.createElement('element');
182 _html.innerHTML = html;
182 _html.innerHTML = html;
183 return _html;
183 return _html;
184 };
184 };
185
185
186 // Toggle Collapsable Content
186 // Toggle Collapsable Content
187 function collapsableContent() {
187 function collapsableContent() {
188
188
189 $('.collapsable-content').not('.no-hide').hide();
189 $('.collapsable-content').not('.no-hide').hide();
190
190
191 $('.btn-collapse').unbind(); //in case we've been here before
191 $('.btn-collapse').unbind(); //in case we've been here before
192 $('.btn-collapse').click(function() {
192 $('.btn-collapse').click(function() {
193 var button = $(this);
193 var button = $(this);
194 var togglename = $(this).data("toggle");
194 var togglename = $(this).data("toggle");
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 if ($(this).html()=="Show Less")
196 if ($(this).html()=="Show Less")
197 $(this).html("Show More");
197 $(this).html("Show More");
198 else
198 else
199 $(this).html("Show Less");
199 $(this).html("Show Less");
200 });
200 });
201 };
201 };
202
202
203 var timeagoActivate = function() {
203 var timeagoActivate = function() {
204 $("time.timeago").timeago();
204 $("time.timeago").timeago();
205 };
205 };
206
206
207
207
208 var clipboardActivate = function() {
208 var clipboardActivate = function() {
209 /*
209 /*
210 *
210 *
211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 * */
212 * */
213 var clipboard = new ClipboardJS('.clipboard-action');
213 var clipboard = new ClipboardJS('.clipboard-action');
214
214
215 clipboard.on('success', function(e) {
215 clipboard.on('success', function(e) {
216 var callback = function () {
216 var callback = function () {
217 $(e.trigger).animate({'opacity': 1.00}, 200)
217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 };
218 };
219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 e.clearSelection();
220 e.clearSelection();
221 });
221 });
222 };
222 };
223
223
224 var tooltipActivate = function () {
224 var tooltipActivate = function () {
225 var delay = 50;
225 var delay = 50;
226 var animation = 'fade';
226 var animation = 'fade';
227 var theme = 'tooltipster-shadow';
227 var theme = 'tooltipster-shadow';
228 var debug = false;
228 var debug = false;
229
229
230 $('.tooltip').tooltipster({
230 $('.tooltip').tooltipster({
231 debug: debug,
231 debug: debug,
232 theme: theme,
232 theme: theme,
233 animation: animation,
233 animation: animation,
234 delay: delay,
234 delay: delay,
235 contentCloning: true,
235 contentCloning: true,
236 contentAsHTML: true,
236 contentAsHTML: true,
237
237
238 functionBefore: function (instance, helper) {
238 functionBefore: function (instance, helper) {
239 var $origin = $(helper.origin);
239 var $origin = $(helper.origin);
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 instance.content(data);
241 instance.content(data);
242 }
242 }
243 });
243 });
244 var hovercardCache = {};
244 var hovercardCache = {};
245
245
246 var loadHoverCard = function (url, altHovercard, callback) {
246 var loadHoverCard = function (url, altHovercard, callback) {
247 var id = url;
247 var id = url;
248
248
249 if (hovercardCache[id] !== undefined) {
249 if (hovercardCache[id] !== undefined) {
250 callback(hovercardCache[id]);
250 callback(hovercardCache[id]);
251 return true;
251 return true;
252 }
252 }
253
253
254 hovercardCache[id] = undefined;
254 hovercardCache[id] = undefined;
255 $.get(url, function (data) {
255 $.get(url, function (data) {
256 hovercardCache[id] = data;
256 hovercardCache[id] = data;
257 callback(hovercardCache[id]);
257 callback(hovercardCache[id]);
258 return true;
258 return true;
259 }).fail(function (data, textStatus, errorThrown) {
259 }).fail(function (data, textStatus, errorThrown) {
260
260
261 if (parseInt(data.status) === 404) {
261 if (parseInt(data.status) === 404) {
262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
263 } else {
263 } else {
264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
265 }
265 }
266 callback(msg);
266 callback(msg);
267 return false
267 return false
268 });
268 });
269 };
269 };
270
270
271 $('.tooltip-hovercard').tooltipster({
271 $('.tooltip-hovercard').tooltipster({
272 debug: debug,
272 debug: debug,
273 theme: theme,
273 theme: theme,
274 animation: animation,
274 animation: animation,
275 delay: delay,
275 delay: delay,
276 interactive: true,
276 interactive: true,
277 contentCloning: true,
277 contentCloning: true,
278
278
279 trigger: 'custom',
279 trigger: 'custom',
280 triggerOpen: {
280 triggerOpen: {
281 mouseenter: true,
281 mouseenter: true,
282 },
282 },
283 triggerClose: {
283 triggerClose: {
284 mouseleave: true,
284 mouseleave: true,
285 originClick: true,
285 originClick: true,
286 touchleave: true
286 touchleave: true
287 },
287 },
288 content: _gettext('Loading...'),
288 content: _gettext('Loading...'),
289 contentAsHTML: true,
289 contentAsHTML: true,
290 updateAnimation: null,
290 updateAnimation: null,
291
291
292 functionBefore: function (instance, helper) {
292 functionBefore: function (instance, helper) {
293
293
294 var $origin = $(helper.origin);
294 var $origin = $(helper.origin);
295
295
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
297 if ($origin.data('loaded') !== true) {
297 if ($origin.data('loaded') !== true) {
298 var hovercardUrl = $origin.data('hovercardUrl');
298 var hovercardUrl = $origin.data('hovercardUrl');
299 var altHovercard = $origin.data('hovercardAlt');
299 var altHovercard = $origin.data('hovercardAlt');
300
300
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 var urlLoad = true;
302 var urlLoad = true;
303 if (hovercardUrl.substr(0, 12) === 'pyroutes.url') {
303 if (hovercardUrl.substr(0, 12) === 'pyroutes.url') {
304 hovercardUrl = eval(hovercardUrl)
304 hovercardUrl = eval(hovercardUrl)
305 } else if (hovercardUrl.substr(0, 11) === 'javascript:') {
305 } else if (hovercardUrl.substr(0, 11) === 'javascript:') {
306 var jsFunc = hovercardUrl.substr(11);
306 var jsFunc = hovercardUrl.substr(11);
307 urlLoad = false;
307 urlLoad = false;
308 loaded = true;
308 loaded = true;
309 instance.content(eval(jsFunc))
309 instance.content(eval(jsFunc))
310 }
310 }
311
311
312 if (urlLoad) {
312 if (urlLoad) {
313 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
313 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
314 instance.content(data);
314 instance.content(data);
315 })
315 })
316 }
316 }
317
317
318 } else {
318 } else {
319 if ($origin.data('hovercardAltHtml')) {
319 if ($origin.data('hovercardAltHtml')) {
320 var data = atob($origin.data('hovercardAltHtml'));
320 var data = atob($origin.data('hovercardAltHtml'));
321 } else {
321 } else {
322 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
322 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
323 }
323 }
324 var loaded = true;
324 var loaded = true;
325 instance.content(data);
325 instance.content(data);
326 }
326 }
327
327
328 // to remember that the data has been loaded
328 // to remember that the data has been loaded
329 $origin.data('loaded', loaded);
329 $origin.data('loaded', loaded);
330 }
330 }
331 }
331 }
332 })
332 })
333 };
333 };
334
334
335 // Formatting values in a Select2 dropdown of commit references
335 // Formatting values in a Select2 dropdown of commit references
336 var formatSelect2SelectionRefs = function(commit_ref){
336 var formatSelect2SelectionRefs = function(commit_ref){
337 var tmpl = '';
337 var tmpl = '';
338 if (!commit_ref.text || commit_ref.type === 'sha'){
338 if (!commit_ref.text || commit_ref.type === 'sha'){
339 return commit_ref.text;
339 return commit_ref.text;
340 }
340 }
341 if (commit_ref.type === 'branch'){
341 if (commit_ref.type === 'branch'){
342 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
342 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
343 } else if (commit_ref.type === 'tag'){
343 } else if (commit_ref.type === 'tag'){
344 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
344 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
345 } else if (commit_ref.type === 'book'){
345 } else if (commit_ref.type === 'book'){
346 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
346 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
347 }
347 }
348 return tmpl.concat(escapeHtml(commit_ref.text));
348 return tmpl.concat(escapeHtml(commit_ref.text));
349 };
349 };
350
350
351 // takes a given html element and scrolls it down offset pixels
351 // takes a given html element and scrolls it down offset pixels
352 function offsetScroll(element, offset) {
352 function offsetScroll(element, offset) {
353 setTimeout(function() {
353 setTimeout(function() {
354 var location = element.offset().top;
354 var location = element.offset().top;
355 // some browsers use body, some use html
355 // some browsers use body, some use html
356 $('html, body').animate({ scrollTop: (location - offset) });
356 $('html, body').animate({ scrollTop: (location - offset) });
357 }, 100);
357 }, 100);
358 }
358 }
359
359
360 // scroll an element `percent`% from the top of page in `time` ms
360 // scroll an element `percent`% from the top of page in `time` ms
361 function scrollToElement(element, percent, time) {
361 function scrollToElement(element, percent, time) {
362 percent = (percent === undefined ? 25 : percent);
362 percent = (percent === undefined ? 25 : percent);
363 time = (time === undefined ? 100 : time);
363 time = (time === undefined ? 100 : time);
364
364
365 var $element = $(element);
365 var $element = $(element);
366 if ($element.length == 0) {
366 if ($element.length == 0) {
367 throw('Cannot scroll to {0}'.format(element))
367 throw('Cannot scroll to {0}'.format(element))
368 }
368 }
369 var elOffset = $element.offset().top;
369 var elOffset = $element.offset().top;
370 var elHeight = $element.height();
370 var elHeight = $element.height();
371 var windowHeight = $(window).height();
371 var windowHeight = $(window).height();
372 var offset = elOffset;
372 var offset = elOffset;
373 if (elHeight < windowHeight) {
373 if (elHeight < windowHeight) {
374 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
374 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
375 }
375 }
376 setTimeout(function() {
376 setTimeout(function() {
377 $('html, body').animate({ scrollTop: offset});
377 $('html, body').animate({ scrollTop: offset});
378 }, time);
378 }, time);
379 }
379 }
380
380
381 /**
381 /**
382 * global hooks after DOM is loaded
382 * global hooks after DOM is loaded
383 */
383 */
384 $(document).ready(function() {
384 $(document).ready(function() {
385 firefoxAnchorFix();
385 firefoxAnchorFix();
386
386
387 $('.navigation a.menulink').on('click', function(e){
387 $('.navigation a.menulink').on('click', function(e){
388 var menuitem = $(this).parent('li');
388 var menuitem = $(this).parent('li');
389 if (menuitem.hasClass('open')) {
389 if (menuitem.hasClass('open')) {
390 menuitem.removeClass('open');
390 menuitem.removeClass('open');
391 } else {
391 } else {
392 menuitem.addClass('open');
392 menuitem.addClass('open');
393 $(document).on('click', function(event) {
393 $(document).on('click', function(event) {
394 if (!$(event.target).closest(menuitem).length) {
394 if (!$(event.target).closest(menuitem).length) {
395 menuitem.removeClass('open');
395 menuitem.removeClass('open');
396 }
396 }
397 });
397 });
398 }
398 }
399 });
399 });
400
400
401 $('body').on('click', '.cb-lineno a', function(event) {
401 $('body').on('click', '.cb-lineno a', function(event) {
402 function sortNumber(a,b) {
402 function sortNumber(a,b) {
403 return a - b;
403 return a - b;
404 }
404 }
405
405
406 var lineNo = $(this).data('lineNo');
406 var lineNo = $(this).data('lineNo');
407 var lineName = $(this).attr('name');
407 var lineName = $(this).attr('name');
408
408
409 if (lineNo) {
409 if (lineNo) {
410 var prevLine = $('.cb-line-selected a').data('lineNo');
410 var prevLine = $('.cb-line-selected a').data('lineNo');
411
411
412 // on shift, we do a range selection, if we got previous line
412 // on shift, we do a range selection, if we got previous line
413 if (event.shiftKey && prevLine !== undefined) {
413 if (event.shiftKey && prevLine !== undefined) {
414 var prevLine = parseInt(prevLine);
414 var prevLine = parseInt(prevLine);
415 var nextLine = parseInt(lineNo);
415 var nextLine = parseInt(lineNo);
416 var pos = [prevLine, nextLine].sort(sortNumber);
416 var pos = [prevLine, nextLine].sort(sortNumber);
417 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
417 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
418
418
419 // single click
419 // single click
420 } else {
420 } else {
421 var nextLine = parseInt(lineNo);
421 var nextLine = parseInt(lineNo);
422 var pos = [nextLine, nextLine];
422 var pos = [nextLine, nextLine];
423 var anchor = '#L{0}'.format(pos[0]);
423 var anchor = '#L{0}'.format(pos[0]);
424
424
425 }
425 }
426 // highlight
426 // highlight
427 var range = [];
427 var range = [];
428 for (var i = pos[0]; i <= pos[1]; i++) {
428 for (var i = pos[0]; i <= pos[1]; i++) {
429 range.push(i);
429 range.push(i);
430 }
430 }
431 // clear old selected lines
431 // clear old selected lines
432 $('.cb-line-selected').removeClass('cb-line-selected');
432 $('.cb-line-selected').removeClass('cb-line-selected');
433
433
434 $.each(range, function (i, lineNo) {
434 $.each(range, function (i, lineNo) {
435 var line_td = $('td.cb-lineno#L' + lineNo);
435 var line_td = $('td.cb-lineno#L' + lineNo);
436
436
437 if (line_td.length) {
437 if (line_td.length) {
438 line_td.addClass('cb-line-selected'); // line number td
438 line_td.addClass('cb-line-selected'); // line number td
439 line_td.prev().addClass('cb-line-selected'); // line data
439 line_td.prev().addClass('cb-line-selected'); // line data
440 line_td.next().addClass('cb-line-selected'); // line content
440 line_td.next().addClass('cb-line-selected'); // line content
441 }
441 }
442 });
442 });
443
443
444 } else if (lineName !== undefined) { // lineName only occurs in diffs
444 } else if (lineName !== undefined) { // lineName only occurs in diffs
445 // clear old selected lines
445 // clear old selected lines
446 $('td.cb-line-selected').removeClass('cb-line-selected');
446 $('td.cb-line-selected').removeClass('cb-line-selected');
447 var anchor = '#{0}'.format(lineName);
447 var anchor = '#{0}'.format(lineName);
448 var diffmode = templateContext.session_attrs.diffmode || "sideside";
448 var diffmode = templateContext.session_attrs.diffmode || "sideside";
449
449
450 if (diffmode === "unified") {
450 if (diffmode === "unified") {
451 $(this).closest('tr').find('td').addClass('cb-line-selected');
451 $(this).closest('tr').find('td').addClass('cb-line-selected');
452 } else {
452 } else {
453 var activeTd = $(this).closest('td');
453 var activeTd = $(this).closest('td');
454 activeTd.addClass('cb-line-selected');
454 activeTd.addClass('cb-line-selected');
455 activeTd.next('td').addClass('cb-line-selected');
455 activeTd.next('td').addClass('cb-line-selected');
456 }
456 }
457
457
458 }
458 }
459
459
460 // Replace URL without jumping to it if browser supports.
460 // Replace URL without jumping to it if browser supports.
461 // Default otherwise
461 // Default otherwise
462 if (history.pushState && anchor !== undefined) {
462 if (history.pushState && anchor !== undefined) {
463 var new_location = location.href.rstrip('#');
463 var new_location = location.href.rstrip('#');
464 if (location.hash) {
464 if (location.hash) {
465 // location without hash
465 // location without hash
466 new_location = new_location.replace(location.hash, "");
466 new_location = new_location.replace(location.hash, "");
467 }
467 }
468
468
469 // Make new anchor url
469 // Make new anchor url
470 new_location = new_location + anchor;
470 new_location = new_location + anchor;
471 history.pushState(true, document.title, new_location);
471 history.pushState(true, document.title, new_location);
472
472
473 return false;
473 return false;
474 }
474 }
475
475
476 });
476 });
477
477
478 $('.collapse_file').on('click', function(e) {
478 $('.collapse_file').on('click', function(e) {
479 e.stopPropagation();
479 e.stopPropagation();
480 if ($(e.target).is('a')) { return; }
480 if ($(e.target).is('a')) { return; }
481 var node = $(e.delegateTarget).first();
481 var node = $(e.delegateTarget).first();
482 var icon = $($(node.children().first()).children().first());
482 var icon = $($(node.children().first()).children().first());
483 var id = node.attr('fid');
483 var id = node.attr('fid');
484 var target = $('#'+id);
484 var target = $('#'+id);
485 var tr = $('#tr_'+id);
485 var tr = $('#tr_'+id);
486 var diff = $('#diff_'+id);
486 var diff = $('#diff_'+id);
487 if(node.hasClass('expand_file')){
487 if(node.hasClass('expand_file')){
488 node.removeClass('expand_file');
488 node.removeClass('expand_file');
489 icon.removeClass('expand_file_icon');
489 icon.removeClass('expand_file_icon');
490 node.addClass('collapse_file');
490 node.addClass('collapse_file');
491 icon.addClass('collapse_file_icon');
491 icon.addClass('collapse_file_icon');
492 diff.show();
492 diff.show();
493 tr.show();
493 tr.show();
494 target.show();
494 target.show();
495 } else {
495 } else {
496 node.removeClass('collapse_file');
496 node.removeClass('collapse_file');
497 icon.removeClass('collapse_file_icon');
497 icon.removeClass('collapse_file_icon');
498 node.addClass('expand_file');
498 node.addClass('expand_file');
499 icon.addClass('expand_file_icon');
499 icon.addClass('expand_file_icon');
500 diff.hide();
500 diff.hide();
501 tr.hide();
501 tr.hide();
502 target.hide();
502 target.hide();
503 }
503 }
504 });
504 });
505
505
506 $('#expand_all_files').click(function() {
506 $('#expand_all_files').click(function() {
507 $('.expand_file').each(function() {
507 $('.expand_file').each(function() {
508 var node = $(this);
508 var node = $(this);
509 var icon = $($(node.children().first()).children().first());
509 var icon = $($(node.children().first()).children().first());
510 var id = $(this).attr('fid');
510 var id = $(this).attr('fid');
511 var target = $('#'+id);
511 var target = $('#'+id);
512 var tr = $('#tr_'+id);
512 var tr = $('#tr_'+id);
513 var diff = $('#diff_'+id);
513 var diff = $('#diff_'+id);
514 node.removeClass('expand_file');
514 node.removeClass('expand_file');
515 icon.removeClass('expand_file_icon');
515 icon.removeClass('expand_file_icon');
516 node.addClass('collapse_file');
516 node.addClass('collapse_file');
517 icon.addClass('collapse_file_icon');
517 icon.addClass('collapse_file_icon');
518 diff.show();
518 diff.show();
519 tr.show();
519 tr.show();
520 target.show();
520 target.show();
521 });
521 });
522 });
522 });
523
523
524 $('#collapse_all_files').click(function() {
524 $('#collapse_all_files').click(function() {
525 $('.collapse_file').each(function() {
525 $('.collapse_file').each(function() {
526 var node = $(this);
526 var node = $(this);
527 var icon = $($(node.children().first()).children().first());
527 var icon = $($(node.children().first()).children().first());
528 var id = $(this).attr('fid');
528 var id = $(this).attr('fid');
529 var target = $('#'+id);
529 var target = $('#'+id);
530 var tr = $('#tr_'+id);
530 var tr = $('#tr_'+id);
531 var diff = $('#diff_'+id);
531 var diff = $('#diff_'+id);
532 node.removeClass('collapse_file');
532 node.removeClass('collapse_file');
533 icon.removeClass('collapse_file_icon');
533 icon.removeClass('collapse_file_icon');
534 node.addClass('expand_file');
534 node.addClass('expand_file');
535 icon.addClass('expand_file_icon');
535 icon.addClass('expand_file_icon');
536 diff.hide();
536 diff.hide();
537 tr.hide();
537 tr.hide();
538 target.hide();
538 target.hide();
539 });
539 });
540 });
540 });
541
541
542 // Mouse over behavior for comments and line selection
542 // Mouse over behavior for comments and line selection
543
543
544 // Select the line that comes from the url anchor
544 // Select the line that comes from the url anchor
545 // At the time of development, Chrome didn't seem to support jquery's :target
545 // At the time of development, Chrome didn't seem to support jquery's :target
546 // element, so I had to scroll manually
546 // element, so I had to scroll manually
547
547
548 if (location.hash) {
548 if (location.hash) {
549 var result = splitDelimitedHash(location.hash);
549 var result = splitDelimitedHash(location.hash);
550
550
551 var loc = result.loc;
551 var loc = result.loc;
552
552
553 if (loc.length > 1) {
553 if (loc.length > 1) {
554
554
555 var highlightable_line_tds = [];
555 var highlightable_line_tds = [];
556
556
557 // source code line format
557 // source code line format
558 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
558 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
559
559
560 // multi-line HL, for files
560 // multi-line HL, for files
561 if (page_highlights.length > 1) {
561 if (page_highlights.length > 1) {
562 var highlight_ranges = page_highlights[1].split(",");
562 var highlight_ranges = page_highlights[1].split(",");
563 var h_lines = [];
563 var h_lines = [];
564 for (var pos in highlight_ranges) {
564 for (var pos in highlight_ranges) {
565 var _range = highlight_ranges[pos].split('-');
565 var _range = highlight_ranges[pos].split('-');
566 if (_range.length === 2) {
566 if (_range.length === 2) {
567 var start = parseInt(_range[0]);
567 var start = parseInt(_range[0]);
568 var end = parseInt(_range[1]);
568 var end = parseInt(_range[1]);
569 if (start < end) {
569 if (start < end) {
570 for (var i = start; i <= end; i++) {
570 for (var i = start; i <= end; i++) {
571 h_lines.push(i);
571 h_lines.push(i);
572 }
572 }
573 }
573 }
574 } else {
574 } else {
575 h_lines.push(parseInt(highlight_ranges[pos]));
575 h_lines.push(parseInt(highlight_ranges[pos]));
576 }
576 }
577 }
577 }
578 for (pos in h_lines) {
578 for (pos in h_lines) {
579 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
579 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
580 if (line_td.length) {
580 if (line_td.length) {
581 highlightable_line_tds.push(line_td);
581 highlightable_line_tds.push(line_td);
582 }
582 }
583 }
583 }
584 }
584 }
585
585
586 // now check a direct id reference of line in diff / pull-request page)
586 // now check a direct id reference of line in diff / pull-request page)
587 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
587 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
588 highlightable_line_tds.push($(loc));
588 highlightable_line_tds.push($(loc));
589 }
589 }
590
590
591 // mark diff lines as selected
591 // mark diff lines as selected
592 $.each(highlightable_line_tds, function (i, $td) {
592 $.each(highlightable_line_tds, function (i, $td) {
593 $td.addClass('cb-line-selected'); // line number td
593 $td.addClass('cb-line-selected'); // line number td
594 $td.prev().addClass('cb-line-selected'); // line data
594 $td.prev().addClass('cb-line-selected'); // line data
595 $td.next().addClass('cb-line-selected'); // line content
595 $td.next().addClass('cb-line-selected'); // line content
596 });
596 });
597
597
598 if (highlightable_line_tds.length > 0) {
598 if (highlightable_line_tds.length > 0) {
599 var $first_line_td = highlightable_line_tds[0];
599 var $first_line_td = highlightable_line_tds[0];
600 scrollToElement($first_line_td);
600 scrollToElement($first_line_td);
601 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
601 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
602 td: $first_line_td,
602 td: $first_line_td,
603 remainder: result.remainder
603 remainder: result.remainder
604 });
604 });
605 } else {
605 } else {
606 // case for direct anchor to comments
606 // case for direct anchor to comments
607 var $line = $(loc);
607 var $line = $(loc);
608
608
609 if ($line.hasClass('comment-general')) {
609 if ($line.hasClass('comment-general')) {
610 $line.show();
610 $line.show();
611 } else if ($line.hasClass('comment-inline')) {
611 } else if ($line.hasClass('comment-inline')) {
612 $line.show();
612 $line.show();
613 var $cb = $line.closest('.cb');
613 var $cb = $line.closest('.cb');
614 $cb.removeClass('cb-collapsed')
614 $cb.removeClass('cb-collapsed')
615 }
615 }
616 if ($line.length > 0) {
616 if ($line.length > 0) {
617 $line.addClass('comment-selected-hl');
617 $line.addClass('comment-selected-hl');
618 offsetScroll($line, 70);
618 offsetScroll($line, 70);
619 }
619 }
620 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
620 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
621 $line.nextAll('.cb-comment-add-button').trigger('click');
621 $line.nextAll('.cb-comment-add-button').trigger('click');
622 }
622 }
623 }
623 }
624
624
625 }
625 }
626 }
626 }
627 collapsableContent();
627 collapsableContent();
628 });
628 });
629
629
630 var feedLifetimeOptions = function(query, initialData){
630 var feedLifetimeOptions = function(query, initialData){
631 var data = {results: []};
631 var data = {results: []};
632 var isQuery = typeof query.term !== 'undefined';
632 var isQuery = typeof query.term !== 'undefined';
633
633
634 var section = _gettext('Lifetime');
634 var section = _gettext('Lifetime');
635 var children = [];
635 var children = [];
636
636
637 //filter results
637 //filter results
638 $.each(initialData.results, function(idx, value) {
638 $.each(initialData.results, function(idx, value) {
639
639
640 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
640 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
641 children.push({
641 children.push({
642 'id': this.id,
642 'id': this.id,
643 'text': this.text
643 'text': this.text
644 })
644 })
645 }
645 }
646
646
647 });
647 });
648 data.results.push({
648 data.results.push({
649 'text': section,
649 'text': section,
650 'children': children
650 'children': children
651 });
651 });
652
652
653 if (isQuery) {
653 if (isQuery) {
654
654
655 var now = moment.utc();
655 var now = moment.utc();
656
656
657 var parseQuery = function(entry, now){
657 var parseQuery = function(entry, now){
658 var fmt = 'DD/MM/YYYY H:mm';
658 var fmt = 'DD/MM/YYYY H:mm';
659 var parsed = moment.utc(entry, fmt);
659 var parsed = moment.utc(entry, fmt);
660 var diffInMin = parsed.diff(now, 'minutes');
660 var diffInMin = parsed.diff(now, 'minutes');
661
661
662 if (diffInMin > 0){
662 if (diffInMin > 0){
663 return {
663 return {
664 id: diffInMin,
664 id: diffInMin,
665 text: parsed.format(fmt)
665 text: parsed.format(fmt)
666 }
666 }
667 } else {
667 } else {
668 return {
668 return {
669 id: undefined,
669 id: undefined,
670 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
670 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
671 }
671 }
672 }
672 }
673
673
674
674
675 };
675 };
676
676
677 data.results.push({
677 data.results.push({
678 'text': _gettext('Specified expiration date'),
678 'text': _gettext('Specified expiration date'),
679 'children': [{
679 'children': [{
680 'id': parseQuery(query.term, now).id,
680 'id': parseQuery(query.term, now).id,
681 'text': parseQuery(query.term, now).text
681 'text': parseQuery(query.term, now).text
682 }]
682 }]
683 });
683 });
684 }
684 }
685
685
686 query.callback(data);
686 query.callback(data);
687 };
687 };
688
688
689 /*
689 /*
690 * Retrievew via templateContext.session_attrs.key
690 * Retrievew via templateContext.session_attrs.key
691 * */
691 * */
692 var storeUserSessionAttr = function (key, val) {
692 var storeUserSessionAttr = function (key, val) {
693
693
694 var postData = {
694 var postData = {
695 'key': key,
695 'key': key,
696 'val': val,
696 'val': val,
697 'csrf_token': CSRF_TOKEN
697 'csrf_token': CSRF_TOKEN
698 };
698 };
699
699
700 var success = function(o) {
700 var success = function(o) {
701 return true
701 return true
702 };
702 };
703
703
704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
704 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
705 return false;
705 return false;
706 };
706 };
707
708
709 var getUserSessionAttr = function(key) {
710 var storeKey = templateContext.session_attrs;
711 var val = storeKey[key]
712 if (val !== undefined) {
713 return JSON.parse(val)
714 }
715 return null
716 }
@@ -1,144 +1,151 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-body">
5 <div style="height: 35px">
5 <div style="height: 35px">
6 <%
6 <%
7 selected_filter = 'all'
7 selected_filter = 'all'
8 if c.closed:
8 if c.closed:
9 selected_filter = 'all_closed'
9 selected_filter = 'all_closed'
10 %>
10 %>
11
11
12 <ul class="button-links">
12 <ul class="button-links">
13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
15 </ul>
15 </ul>
16
16
17 <div class="grid-quick-filter">
17 <div class="grid-quick-filter">
18 <ul class="grid-filter-box">
18 <ul class="grid-filter-box">
19 <li class="grid-filter-box-icon">
19 <li class="grid-filter-box-icon">
20 <i class="icon-search"></i>
20 <i class="icon-search"></i>
21 </li>
21 </li>
22 <li class="grid-filter-box-input">
22 <li class="grid-filter-box-input">
23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
24 </li>
24 </li>
25 </ul>
25 </ul>
26 </div>
26 </div>
27 </div>
27 </div>
28 </div>
28 </div>
29 </div>
29 </div>
30
30
31 <div class="panel panel-default">
31 <div class="panel panel-default">
32 <div class="panel-heading">
32 <div class="panel-heading">
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 </div>
34 </div>
35 <div class="panel-body panel-body-min-height">
35 <div class="panel-body panel-body-min-height">
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 </div>
37 </div>
38 </div>
38 </div>
39
39
40 <script type="text/javascript">
40 <script type="text/javascript">
41 $(document).ready(function () {
41 $(document).ready(function () {
42
42
43 var $pullRequestListTable = $('#pull_request_list_table');
43 var $pullRequestListTable = $('#pull_request_list_table');
44
44
45 // participating object list
45 // participating object list
46 $pullRequestListTable.DataTable({
46 $pullRequestListTable.DataTable({
47 processing: true,
47 processing: true,
48 serverSide: true,
48 serverSide: true,
49 stateSave: true,
50 stateDuration: -1,
49 ajax: {
51 ajax: {
50 "url": "${h.route_path('my_account_pullrequests_data')}",
52 "url": "${h.route_path('my_account_pullrequests_data')}",
51 "data": function (d) {
53 "data": function (d) {
52 d.closed = "${c.closed}";
54 d.closed = "${c.closed}";
53 },
55 },
54 "dataSrc": function (json) {
56 "dataSrc": function (json) {
55 return json.data;
57 return json.data;
56 }
58 }
57 },
59 },
58
60
59 dom: 'rtp',
61 dom: 'rtp',
60 pageLength: ${c.visual.dashboard_items},
62 pageLength: ${c.visual.dashboard_items},
61 order: [[1, "desc"]],
63 order: [[1, "desc"]],
62 columns: [
64 columns: [
63 {
65 {
64 data: {
66 data: {
65 "_": "status",
67 "_": "status",
66 "sort": "status"
68 "sort": "status"
67 }, title: "", className: "td-status", orderable: false
69 }, title: "", className: "td-status", orderable: false
68 },
70 },
69 {
71 {
70 data: {
72 data: {
71 "_": "name",
73 "_": "name",
72 "sort": "name_raw"
74 "sort": "name_raw"
73 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
75 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
74 },
76 },
75 {
77 {
76 data: {
78 data: {
77 "_": "title",
79 "_": "title",
78 "sort": "title"
80 "sort": "title"
79 }, title: "${_('Title')}", className: "td-description"
81 }, title: "${_('Title')}", className: "td-description"
80 },
82 },
81 {
83 {
82 data: {
84 data: {
83 "_": "author",
85 "_": "author",
84 "sort": "author_raw"
86 "sort": "author_raw"
85 }, title: "${_('Author')}", className: "td-user", orderable: false
87 }, title: "${_('Author')}", className: "td-user", orderable: false
86 },
88 },
87 {
89 {
88 data: {
90 data: {
89 "_": "comments",
91 "_": "comments",
90 "sort": "comments_raw"
92 "sort": "comments_raw"
91 }, title: "", className: "td-comments", orderable: false
93 }, title: "", className: "td-comments", orderable: false
92 },
94 },
93 {
95 {
94 data: {
96 data: {
95 "_": "updated_on",
97 "_": "updated_on",
96 "sort": "updated_on_raw"
98 "sort": "updated_on_raw"
97 }, title: "${_('Last Update')}", className: "td-time"
99 }, title: "${_('Last Update')}", className: "td-time"
98 },
100 },
99 {
101 {
100 data: {
102 data: {
101 "_": "target_repo",
103 "_": "target_repo",
102 "sort": "target_repo"
104 "sort": "target_repo"
103 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
105 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
104 },
106 },
105 ],
107 ],
106 language: {
108 language: {
107 paginate: DEFAULT_GRID_PAGINATION,
109 paginate: DEFAULT_GRID_PAGINATION,
108 sProcessing: _gettext('loading...'),
110 sProcessing: _gettext('loading...'),
109 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
111 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
110 },
112 },
111 "drawCallback": function (settings, json) {
113 "drawCallback": function (settings, json) {
112 timeagoActivate();
114 timeagoActivate();
113 tooltipActivate();
115 tooltipActivate();
114 },
116 },
115 "createdRow": function (row, data, index) {
117 "createdRow": function (row, data, index) {
116 if (data['closed']) {
118 if (data['closed']) {
117 $(row).addClass('closed');
119 $(row).addClass('closed');
118 }
120 }
119 if (data['owned']) {
121 if (data['owned']) {
120 $(row).addClass('owned');
122 $(row).addClass('owned');
121 }
123 }
124 },
125 "stateSaveParams": function (settings, data) {
126 data.search.search = ""; // Don't save search
127 data.start = 0; // don't save pagination
122 }
128 }
123 });
129 });
124 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
130 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
125 $pullRequestListTable.css('opacity', 1);
131 $pullRequestListTable.css('opacity', 1);
126 });
132 });
127
133
128 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
134 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
129 $pullRequestListTable.css('opacity', 0.3);
135 $pullRequestListTable.css('opacity', 0.3);
130 });
136 });
131
137
138
132 // filter
139 // filter
133 $('#q_filter').on('keyup',
140 $('#q_filter').on('keyup',
134 $.debounce(250, function () {
141 $.debounce(250, function () {
135 $pullRequestListTable.DataTable().search(
142 $pullRequestListTable.DataTable().search(
136 $('#q_filter').val()
143 $('#q_filter').val()
137 ).draw();
144 ).draw();
138 })
145 })
139 );
146 );
140
147
141 });
148 });
142
149
143
150
144 </script>
151 </script>
@@ -1,140 +1,146 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Pull Requests').format(c.repo_name)}
4 ${_('{} Pull Requests').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()"></%def>
10 <%def name="breadcrumbs_links()"></%def>
11
11
12 <%def name="menu_bar_nav()">
12 <%def name="menu_bar_nav()">
13 ${self.menu_items(active='repositories')}
13 ${self.menu_items(active='repositories')}
14 </%def>
14 </%def>
15
15
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='showpullrequest')}
18 ${self.repo_menu(active='showpullrequest')}
19 </%def>
19 </%def>
20
20
21
21
22 <%def name="main()">
22 <%def name="main()">
23
23
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26 <ul class="button-links">
26 <ul class="button-links">
27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 </ul>
33 </ul>
34
34
35 <ul class="links">
35 <ul class="links">
36 % if c.rhodecode_user.username != h.DEFAULT_USER:
36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 <li>
37 <li>
38 <span>
38 <span>
39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 ${_('Open new Pull Request')}
40 ${_('Open new Pull Request')}
41 </a>
41 </a>
42 </span>
42 </span>
43 </li>
43 </li>
44 % endif
44 % endif
45
45
46 <li>
46 <li>
47 <div class="grid-quick-filter">
47 <div class="grid-quick-filter">
48 <ul class="grid-filter-box">
48 <ul class="grid-filter-box">
49 <li class="grid-filter-box-icon">
49 <li class="grid-filter-box-icon">
50 <i class="icon-search"></i>
50 <i class="icon-search"></i>
51 </li>
51 </li>
52 <li class="grid-filter-box-input">
52 <li class="grid-filter-box-input">
53 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
53 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 </li>
54 </li>
55 </ul>
55 </ul>
56 </div>
56 </div>
57 </li>
57 </li>
58
58
59 </ul>
59 </ul>
60
60
61 </div>
61 </div>
62
62
63 <div class="main-content-full-width">
63 <div class="main-content-full-width">
64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 </div>
65 </div>
66
66
67 </div>
67 </div>
68
68
69 <script type="text/javascript">
69 <script type="text/javascript">
70 $(document).ready(function() {
70 $(document).ready(function() {
71 var $pullRequestListTable = $('#pull_request_list_table');
71 var $pullRequestListTable = $('#pull_request_list_table');
72
72
73 // object list
73 // object list
74 $pullRequestListTable.DataTable({
74 $pullRequestListTable.DataTable({
75 processing: true,
75 processing: true,
76 serverSide: true,
76 serverSide: true,
77 stateSave: true,
78 stateDuration: -1,
77 ajax: {
79 ajax: {
78 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
80 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
79 "data": function (d) {
81 "data": function (d) {
80 d.source = "${c.source}";
82 d.source = "${c.source}";
81 d.closed = "${c.closed}";
83 d.closed = "${c.closed}";
82 d.my = "${c.my}";
84 d.my = "${c.my}";
83 d.awaiting_review = "${c.awaiting_review}";
85 d.awaiting_review = "${c.awaiting_review}";
84 d.awaiting_my_review = "${c.awaiting_my_review}";
86 d.awaiting_my_review = "${c.awaiting_my_review}";
85 }
87 }
86 },
88 },
87 dom: 'rtp',
89 dom: 'rtp',
88 pageLength: ${c.visual.dashboard_items},
90 pageLength: ${c.visual.dashboard_items},
89 order: [[ 1, "desc" ]],
91 order: [[ 1, "desc" ]],
90 columns: [
92 columns: [
91 { data: {"_": "status",
93 { data: {"_": "status",
92 "sort": "status"}, title: "", className: "td-status", orderable: false},
94 "sort": "status"}, title: "", className: "td-status", orderable: false},
93 { data: {"_": "name",
95 { data: {"_": "name",
94 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
96 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
95 { data: {"_": "title",
97 { data: {"_": "title",
96 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
98 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
97 { data: {"_": "author",
99 { data: {"_": "author",
98 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
100 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
99 { data: {"_": "comments",
101 { data: {"_": "comments",
100 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
102 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
101 { data: {"_": "updated_on",
103 { data: {"_": "updated_on",
102 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
104 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
103 ],
105 ],
104 language: {
106 language: {
105 paginate: DEFAULT_GRID_PAGINATION,
107 paginate: DEFAULT_GRID_PAGINATION,
106 sProcessing: _gettext('loading...'),
108 sProcessing: _gettext('loading...'),
107 emptyTable: _gettext("No pull requests available yet.")
109 emptyTable: _gettext("No pull requests available yet.")
108 },
110 },
109 "drawCallback": function( settings, json ) {
111 "drawCallback": function( settings, json ) {
110 timeagoActivate();
112 timeagoActivate();
111 tooltipActivate();
113 tooltipActivate();
112 },
114 },
113 "createdRow": function ( row, data, index ) {
115 "createdRow": function ( row, data, index ) {
114 if (data['closed']) {
116 if (data['closed']) {
115 $(row).addClass('closed');
117 $(row).addClass('closed');
116 }
118 }
119 },
120 "stateSaveParams": function (settings, data) {
121 data.search.search = ""; // Don't save search
122 data.start = 0; // don't save pagination
117 }
123 }
118 });
124 });
119
125
120 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
126 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
121 $pullRequestListTable.css('opacity', 1);
127 $pullRequestListTable.css('opacity', 1);
122 });
128 });
123
129
124 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
130 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
125 $pullRequestListTable.css('opacity', 0.3);
131 $pullRequestListTable.css('opacity', 0.3);
126 });
132 });
127
133
128 // filter
134 // filter
129 $('#q_filter').on('keyup',
135 $('#q_filter').on('keyup',
130 $.debounce(250, function() {
136 $.debounce(250, function() {
131 $pullRequestListTable.DataTable().search(
137 $pullRequestListTable.DataTable().search(
132 $('#q_filter').val()
138 $('#q_filter').val()
133 ).draw();
139 ).draw();
134 })
140 })
135 );
141 );
136
142
137 });
143 });
138
144
139 </script>
145 </script>
140 </%def>
146 </%def>
General Comments 0
You need to be logged in to leave comments. Login now