##// END OF EJS Templates
partial-renderer: use package resource format for templates....
marcink -
r2313:d272914f default
parent child Browse files
Show More
@@ -1,248 +1,248 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.response import Response
28 from pyramid.response import Response
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
33 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
34 from rhodecode.lib import helpers as h, audit_logger
34 from rhodecode.lib import helpers as h, audit_logger
35 from rhodecode.lib.utils2 import safe_unicode
35 from rhodecode.lib.utils2 import safe_unicode
36
36
37 from rhodecode.model.forms import UserGroupForm
37 from rhodecode.model.forms import UserGroupForm
38 from rhodecode.model.permission import PermissionModel
38 from rhodecode.model.permission import PermissionModel
39 from rhodecode.model.scm import UserGroupList
39 from rhodecode.model.scm import UserGroupList
40 from rhodecode.model.db import (
40 from rhodecode.model.db import (
41 or_, count, User, UserGroup, UserGroupMember)
41 or_, count, User, UserGroup, UserGroupMember)
42 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
43 from rhodecode.model.user_group import UserGroupModel
43 from rhodecode.model.user_group import UserGroupModel
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class AdminUserGroupsView(BaseAppView, DataGridAppView):
48 class AdminUserGroupsView(BaseAppView, DataGridAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 c = self._get_local_tmpl_context()
51 c = self._get_local_tmpl_context()
52
52
53 PermissionModel().set_global_permission_choices(
53 PermissionModel().set_global_permission_choices(
54 c, gettext_translator=self.request.translate)
54 c, gettext_translator=self.request.translate)
55
55
56 self._register_global_c(c)
56 self._register_global_c(c)
57 return c
57 return c
58
58
59 # permission check in data loading of
59 # permission check in data loading of
60 # `user_groups_list_data` via UserGroupList
60 # `user_groups_list_data` via UserGroupList
61 @LoginRequired()
61 @LoginRequired()
62 @NotAnonymous()
62 @NotAnonymous()
63 @view_config(
63 @view_config(
64 route_name='user_groups', request_method='GET',
64 route_name='user_groups', request_method='GET',
65 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
65 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
66 def user_groups_list(self):
66 def user_groups_list(self):
67 c = self.load_default_context()
67 c = self.load_default_context()
68 return self._get_template_context(c)
68 return self._get_template_context(c)
69
69
70 # permission check inside
70 # permission check inside
71 @LoginRequired()
71 @LoginRequired()
72 @NotAnonymous()
72 @NotAnonymous()
73 @view_config(
73 @view_config(
74 route_name='user_groups_data', request_method='GET',
74 route_name='user_groups_data', request_method='GET',
75 renderer='json_ext', xhr=True)
75 renderer='json_ext', xhr=True)
76 def user_groups_list_data(self):
76 def user_groups_list_data(self):
77 self.load_default_context()
77 self.load_default_context()
78 column_map = {
78 column_map = {
79 'active': 'users_group_active',
79 'active': 'users_group_active',
80 'description': 'user_group_description',
80 'description': 'user_group_description',
81 'members': 'members_total',
81 'members': 'members_total',
82 'owner': 'user_username',
82 'owner': 'user_username',
83 'sync': 'group_data'
83 'sync': 'group_data'
84 }
84 }
85 draw, start, limit = self._extract_chunk(self.request)
85 draw, start, limit = self._extract_chunk(self.request)
86 search_q, order_by, order_dir = self._extract_ordering(
86 search_q, order_by, order_dir = self._extract_ordering(
87 self.request, column_map=column_map)
87 self.request, column_map=column_map)
88
88
89 _render = self.request.get_partial_renderer(
89 _render = self.request.get_partial_renderer(
90 'data_table/_dt_elements.mako')
90 'rhodecode:templates/data_table/_dt_elements.mako')
91
91
92 def user_group_name(user_group_id, user_group_name):
92 def user_group_name(user_group_id, user_group_name):
93 return _render("user_group_name", user_group_id, user_group_name)
93 return _render("user_group_name", user_group_id, user_group_name)
94
94
95 def user_group_actions(user_group_id, user_group_name):
95 def user_group_actions(user_group_id, user_group_name):
96 return _render("user_group_actions", user_group_id, user_group_name)
96 return _render("user_group_actions", user_group_id, user_group_name)
97
97
98 def user_profile(username):
98 def user_profile(username):
99 return _render('user_profile', username)
99 return _render('user_profile', username)
100
100
101 auth_user_group_list = UserGroupList(
101 auth_user_group_list = UserGroupList(
102 UserGroup.query().all(), perm_set=['usergroup.admin'])
102 UserGroup.query().all(), perm_set=['usergroup.admin'])
103
103
104 allowed_ids = [-1]
104 allowed_ids = [-1]
105 for user_group in auth_user_group_list:
105 for user_group in auth_user_group_list:
106 allowed_ids.append(user_group.users_group_id)
106 allowed_ids.append(user_group.users_group_id)
107
107
108 user_groups_data_total_count = UserGroup.query()\
108 user_groups_data_total_count = UserGroup.query()\
109 .filter(UserGroup.users_group_id.in_(allowed_ids))\
109 .filter(UserGroup.users_group_id.in_(allowed_ids))\
110 .count()
110 .count()
111
111
112 member_count = count(UserGroupMember.user_id)
112 member_count = count(UserGroupMember.user_id)
113 base_q = Session.query(
113 base_q = Session.query(
114 UserGroup.users_group_name,
114 UserGroup.users_group_name,
115 UserGroup.user_group_description,
115 UserGroup.user_group_description,
116 UserGroup.users_group_active,
116 UserGroup.users_group_active,
117 UserGroup.users_group_id,
117 UserGroup.users_group_id,
118 UserGroup.group_data,
118 UserGroup.group_data,
119 User,
119 User,
120 member_count.label('member_count')
120 member_count.label('member_count')
121 ) \
121 ) \
122 .filter(UserGroup.users_group_id.in_(allowed_ids)) \
122 .filter(UserGroup.users_group_id.in_(allowed_ids)) \
123 .outerjoin(UserGroupMember) \
123 .outerjoin(UserGroupMember) \
124 .join(User, User.user_id == UserGroup.user_id) \
124 .join(User, User.user_id == UserGroup.user_id) \
125 .group_by(UserGroup, User)
125 .group_by(UserGroup, User)
126
126
127 if search_q:
127 if search_q:
128 like_expression = u'%{}%'.format(safe_unicode(search_q))
128 like_expression = u'%{}%'.format(safe_unicode(search_q))
129 base_q = base_q.filter(or_(
129 base_q = base_q.filter(or_(
130 UserGroup.users_group_name.ilike(like_expression),
130 UserGroup.users_group_name.ilike(like_expression),
131 ))
131 ))
132
132
133 user_groups_data_total_filtered_count = base_q.count()
133 user_groups_data_total_filtered_count = base_q.count()
134
134
135 if order_by == 'members_total':
135 if order_by == 'members_total':
136 sort_col = member_count
136 sort_col = member_count
137 elif order_by == 'user_username':
137 elif order_by == 'user_username':
138 sort_col = User.username
138 sort_col = User.username
139 else:
139 else:
140 sort_col = getattr(UserGroup, order_by, None)
140 sort_col = getattr(UserGroup, order_by, None)
141
141
142 if isinstance(sort_col, count) or sort_col:
142 if isinstance(sort_col, count) or sort_col:
143 if order_dir == 'asc':
143 if order_dir == 'asc':
144 sort_col = sort_col.asc()
144 sort_col = sort_col.asc()
145 else:
145 else:
146 sort_col = sort_col.desc()
146 sort_col = sort_col.desc()
147
147
148 base_q = base_q.order_by(sort_col)
148 base_q = base_q.order_by(sort_col)
149 base_q = base_q.offset(start).limit(limit)
149 base_q = base_q.offset(start).limit(limit)
150
150
151 # authenticated access to user groups
151 # authenticated access to user groups
152 auth_user_group_list = base_q.all()
152 auth_user_group_list = base_q.all()
153
153
154 user_groups_data = []
154 user_groups_data = []
155 for user_gr in auth_user_group_list:
155 for user_gr in auth_user_group_list:
156 user_groups_data.append({
156 user_groups_data.append({
157 "users_group_name": user_group_name(
157 "users_group_name": user_group_name(
158 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
158 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
159 "name_raw": h.escape(user_gr.users_group_name),
159 "name_raw": h.escape(user_gr.users_group_name),
160 "description": h.escape(user_gr.user_group_description),
160 "description": h.escape(user_gr.user_group_description),
161 "members": user_gr.member_count,
161 "members": user_gr.member_count,
162 # NOTE(marcink): because of advanced query we
162 # NOTE(marcink): because of advanced query we
163 # need to load it like that
163 # need to load it like that
164 "sync": UserGroup._load_group_data(
164 "sync": UserGroup._load_group_data(
165 user_gr.group_data).get('extern_type'),
165 user_gr.group_data).get('extern_type'),
166 "active": h.bool2icon(user_gr.users_group_active),
166 "active": h.bool2icon(user_gr.users_group_active),
167 "owner": user_profile(user_gr.User.username),
167 "owner": user_profile(user_gr.User.username),
168 "action": user_group_actions(
168 "action": user_group_actions(
169 user_gr.users_group_id, user_gr.users_group_name)
169 user_gr.users_group_id, user_gr.users_group_name)
170 })
170 })
171
171
172 data = ({
172 data = ({
173 'draw': draw,
173 'draw': draw,
174 'data': user_groups_data,
174 'data': user_groups_data,
175 'recordsTotal': user_groups_data_total_count,
175 'recordsTotal': user_groups_data_total_count,
176 'recordsFiltered': user_groups_data_total_filtered_count,
176 'recordsFiltered': user_groups_data_total_filtered_count,
177 })
177 })
178
178
179 return data
179 return data
180
180
181 @LoginRequired()
181 @LoginRequired()
182 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
182 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
183 @view_config(
183 @view_config(
184 route_name='user_groups_new', request_method='GET',
184 route_name='user_groups_new', request_method='GET',
185 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
185 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
186 def user_groups_new(self):
186 def user_groups_new(self):
187 c = self.load_default_context()
187 c = self.load_default_context()
188 return self._get_template_context(c)
188 return self._get_template_context(c)
189
189
190 @LoginRequired()
190 @LoginRequired()
191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 @CSRFRequired()
192 @CSRFRequired()
193 @view_config(
193 @view_config(
194 route_name='user_groups_create', request_method='POST',
194 route_name='user_groups_create', request_method='POST',
195 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
195 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
196 def user_groups_create(self):
196 def user_groups_create(self):
197 _ = self.request.translate
197 _ = self.request.translate
198 c = self.load_default_context()
198 c = self.load_default_context()
199 users_group_form = UserGroupForm()()
199 users_group_form = UserGroupForm()()
200
200
201 user_group_name = self.request.POST.get('users_group_name')
201 user_group_name = self.request.POST.get('users_group_name')
202 try:
202 try:
203 form_result = users_group_form.to_python(dict(self.request.POST))
203 form_result = users_group_form.to_python(dict(self.request.POST))
204 user_group = UserGroupModel().create(
204 user_group = UserGroupModel().create(
205 name=form_result['users_group_name'],
205 name=form_result['users_group_name'],
206 description=form_result['user_group_description'],
206 description=form_result['user_group_description'],
207 owner=self._rhodecode_user.user_id,
207 owner=self._rhodecode_user.user_id,
208 active=form_result['users_group_active'])
208 active=form_result['users_group_active'])
209 Session().flush()
209 Session().flush()
210 creation_data = user_group.get_api_data()
210 creation_data = user_group.get_api_data()
211 user_group_name = form_result['users_group_name']
211 user_group_name = form_result['users_group_name']
212
212
213 audit_logger.store_web(
213 audit_logger.store_web(
214 'user_group.create', action_data={'data': creation_data},
214 'user_group.create', action_data={'data': creation_data},
215 user=self._rhodecode_user)
215 user=self._rhodecode_user)
216
216
217 user_group_link = h.link_to(
217 user_group_link = h.link_to(
218 h.escape(user_group_name),
218 h.escape(user_group_name),
219 h.route_path(
219 h.route_path(
220 'edit_user_group', user_group_id=user_group.users_group_id))
220 'edit_user_group', user_group_id=user_group.users_group_id))
221 h.flash(h.literal(_('Created user group %(user_group_link)s')
221 h.flash(h.literal(_('Created user group %(user_group_link)s')
222 % {'user_group_link': user_group_link}),
222 % {'user_group_link': user_group_link}),
223 category='success')
223 category='success')
224 Session().commit()
224 Session().commit()
225 user_group_id = user_group.users_group_id
225 user_group_id = user_group.users_group_id
226 except formencode.Invalid as errors:
226 except formencode.Invalid as errors:
227
227
228 data = render(
228 data = render(
229 'rhodecode:templates/admin/user_groups/user_group_add.mako',
229 'rhodecode:templates/admin/user_groups/user_group_add.mako',
230 self._get_template_context(c), self.request)
230 self._get_template_context(c), self.request)
231 html = formencode.htmlfill.render(
231 html = formencode.htmlfill.render(
232 data,
232 data,
233 defaults=errors.value,
233 defaults=errors.value,
234 errors=errors.error_dict or {},
234 errors=errors.error_dict or {},
235 prefix_error=False,
235 prefix_error=False,
236 encoding="UTF-8",
236 encoding="UTF-8",
237 force_defaults=False
237 force_defaults=False
238 )
238 )
239 return Response(html)
239 return Response(html)
240
240
241 except Exception:
241 except Exception:
242 log.exception("Exception creating user group")
242 log.exception("Exception creating user group")
243 h.flash(_('Error occurred during creation of user group %s') \
243 h.flash(_('Error occurred during creation of user group %s') \
244 % user_group_name, category='error')
244 % user_group_name, category='error')
245 raise HTTPFound(h.route_path('user_groups_new'))
245 raise HTTPFound(h.route_path('user_groups_new'))
246
246
247 raise HTTPFound(
247 raise HTTPFound(
248 h.route_path('edit_user_group', user_group_id=user_group_id))
248 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1179 +1,1179 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.authentication.plugins import auth_rhodecode
33 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.events import trigger
34 from rhodecode.events import trigger
35
35
36 from rhodecode.lib import audit_logger
36 from rhodecode.lib import audit_logger
37 from rhodecode.lib.exceptions import (
37 from rhodecode.lib.exceptions import (
38 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
38 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
39 UserOwnsUserGroupsException, DefaultUserException)
39 UserOwnsUserGroupsException, DefaultUserException)
40 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.auth import (
41 from rhodecode.lib.auth import (
42 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
42 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
43 from rhodecode.lib import helpers as h
43 from rhodecode.lib import helpers as h
44 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
44 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
45 from rhodecode.model.auth_token import AuthTokenModel
45 from rhodecode.model.auth_token import AuthTokenModel
46 from rhodecode.model.forms import (
46 from rhodecode.model.forms import (
47 UserForm, UserIndividualPermissionsForm, UserPermissionsForm)
47 UserForm, UserIndividualPermissionsForm, UserPermissionsForm)
48 from rhodecode.model.permission import PermissionModel
48 from rhodecode.model.permission import PermissionModel
49 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.ssh_key import SshKeyModel
50 from rhodecode.model.ssh_key import SshKeyModel
51 from rhodecode.model.user import UserModel
51 from rhodecode.model.user import UserModel
52 from rhodecode.model.user_group import UserGroupModel
52 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
54 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
55 UserApiKeys, UserSshKeys, RepoGroup)
55 UserApiKeys, UserSshKeys, RepoGroup)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 class AdminUsersView(BaseAppView, DataGridAppView):
61 class AdminUsersView(BaseAppView, DataGridAppView):
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 self._register_global_c(c)
65 self._register_global_c(c)
66 return c
66 return c
67
67
68 @LoginRequired()
68 @LoginRequired()
69 @HasPermissionAllDecorator('hg.admin')
69 @HasPermissionAllDecorator('hg.admin')
70 @view_config(
70 @view_config(
71 route_name='users', request_method='GET',
71 route_name='users', request_method='GET',
72 renderer='rhodecode:templates/admin/users/users.mako')
72 renderer='rhodecode:templates/admin/users/users.mako')
73 def users_list(self):
73 def users_list(self):
74 c = self.load_default_context()
74 c = self.load_default_context()
75 return self._get_template_context(c)
75 return self._get_template_context(c)
76
76
77 @LoginRequired()
77 @LoginRequired()
78 @HasPermissionAllDecorator('hg.admin')
78 @HasPermissionAllDecorator('hg.admin')
79 @view_config(
79 @view_config(
80 # renderer defined below
80 # renderer defined below
81 route_name='users_data', request_method='GET',
81 route_name='users_data', request_method='GET',
82 renderer='json_ext', xhr=True)
82 renderer='json_ext', xhr=True)
83 def users_list_data(self):
83 def users_list_data(self):
84 self.load_default_context()
84 self.load_default_context()
85 column_map = {
85 column_map = {
86 'first_name': 'name',
86 'first_name': 'name',
87 'last_name': 'lastname',
87 'last_name': 'lastname',
88 }
88 }
89 draw, start, limit = self._extract_chunk(self.request)
89 draw, start, limit = self._extract_chunk(self.request)
90 search_q, order_by, order_dir = self._extract_ordering(
90 search_q, order_by, order_dir = self._extract_ordering(
91 self.request, column_map=column_map)
91 self.request, column_map=column_map)
92
92
93 _render = self.request.get_partial_renderer(
93 _render = self.request.get_partial_renderer(
94 'data_table/_dt_elements.mako')
94 'rhodecode:templates/data_table/_dt_elements.mako')
95
95
96 def user_actions(user_id, username):
96 def user_actions(user_id, username):
97 return _render("user_actions", user_id, username)
97 return _render("user_actions", user_id, username)
98
98
99 users_data_total_count = User.query()\
99 users_data_total_count = User.query()\
100 .filter(User.username != User.DEFAULT_USER) \
100 .filter(User.username != User.DEFAULT_USER) \
101 .count()
101 .count()
102
102
103 # json generate
103 # json generate
104 base_q = User.query().filter(User.username != User.DEFAULT_USER)
104 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105
105
106 if search_q:
106 if search_q:
107 like_expression = u'%{}%'.format(safe_unicode(search_q))
107 like_expression = u'%{}%'.format(safe_unicode(search_q))
108 base_q = base_q.filter(or_(
108 base_q = base_q.filter(or_(
109 User.username.ilike(like_expression),
109 User.username.ilike(like_expression),
110 User._email.ilike(like_expression),
110 User._email.ilike(like_expression),
111 User.name.ilike(like_expression),
111 User.name.ilike(like_expression),
112 User.lastname.ilike(like_expression),
112 User.lastname.ilike(like_expression),
113 ))
113 ))
114
114
115 users_data_total_filtered_count = base_q.count()
115 users_data_total_filtered_count = base_q.count()
116
116
117 sort_col = getattr(User, order_by, None)
117 sort_col = getattr(User, order_by, None)
118 if sort_col:
118 if sort_col:
119 if order_dir == 'asc':
119 if order_dir == 'asc':
120 # handle null values properly to order by NULL last
120 # handle null values properly to order by NULL last
121 if order_by in ['last_activity']:
121 if order_by in ['last_activity']:
122 sort_col = coalesce(sort_col, datetime.date.max)
122 sort_col = coalesce(sort_col, datetime.date.max)
123 sort_col = sort_col.asc()
123 sort_col = sort_col.asc()
124 else:
124 else:
125 # handle null values properly to order by NULL last
125 # handle null values properly to order by NULL last
126 if order_by in ['last_activity']:
126 if order_by in ['last_activity']:
127 sort_col = coalesce(sort_col, datetime.date.min)
127 sort_col = coalesce(sort_col, datetime.date.min)
128 sort_col = sort_col.desc()
128 sort_col = sort_col.desc()
129
129
130 base_q = base_q.order_by(sort_col)
130 base_q = base_q.order_by(sort_col)
131 base_q = base_q.offset(start).limit(limit)
131 base_q = base_q.offset(start).limit(limit)
132
132
133 users_list = base_q.all()
133 users_list = base_q.all()
134
134
135 users_data = []
135 users_data = []
136 for user in users_list:
136 for user in users_list:
137 users_data.append({
137 users_data.append({
138 "username": h.gravatar_with_user(self.request, user.username),
138 "username": h.gravatar_with_user(self.request, user.username),
139 "email": user.email,
139 "email": user.email,
140 "first_name": user.first_name,
140 "first_name": user.first_name,
141 "last_name": user.last_name,
141 "last_name": user.last_name,
142 "last_login": h.format_date(user.last_login),
142 "last_login": h.format_date(user.last_login),
143 "last_activity": h.format_date(user.last_activity),
143 "last_activity": h.format_date(user.last_activity),
144 "active": h.bool2icon(user.active),
144 "active": h.bool2icon(user.active),
145 "active_raw": user.active,
145 "active_raw": user.active,
146 "admin": h.bool2icon(user.admin),
146 "admin": h.bool2icon(user.admin),
147 "extern_type": user.extern_type,
147 "extern_type": user.extern_type,
148 "extern_name": user.extern_name,
148 "extern_name": user.extern_name,
149 "action": user_actions(user.user_id, user.username),
149 "action": user_actions(user.user_id, user.username),
150 })
150 })
151
151
152 data = ({
152 data = ({
153 'draw': draw,
153 'draw': draw,
154 'data': users_data,
154 'data': users_data,
155 'recordsTotal': users_data_total_count,
155 'recordsTotal': users_data_total_count,
156 'recordsFiltered': users_data_total_filtered_count,
156 'recordsFiltered': users_data_total_filtered_count,
157 })
157 })
158
158
159 return data
159 return data
160
160
161 def _set_personal_repo_group_template_vars(self, c_obj):
161 def _set_personal_repo_group_template_vars(self, c_obj):
162 DummyUser = AttributeDict({
162 DummyUser = AttributeDict({
163 'username': '${username}',
163 'username': '${username}',
164 'user_id': '${user_id}',
164 'user_id': '${user_id}',
165 })
165 })
166 c_obj.default_create_repo_group = RepoGroupModel() \
166 c_obj.default_create_repo_group = RepoGroupModel() \
167 .get_default_create_personal_repo_group()
167 .get_default_create_personal_repo_group()
168 c_obj.personal_repo_group_name = RepoGroupModel() \
168 c_obj.personal_repo_group_name = RepoGroupModel() \
169 .get_personal_group_name(DummyUser)
169 .get_personal_group_name(DummyUser)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @HasPermissionAllDecorator('hg.admin')
172 @HasPermissionAllDecorator('hg.admin')
173 @view_config(
173 @view_config(
174 route_name='users_new', request_method='GET',
174 route_name='users_new', request_method='GET',
175 renderer='rhodecode:templates/admin/users/user_add.mako')
175 renderer='rhodecode:templates/admin/users/user_add.mako')
176 def users_new(self):
176 def users_new(self):
177 _ = self.request.translate
177 _ = self.request.translate
178 c = self.load_default_context()
178 c = self.load_default_context()
179 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
179 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
180 self._set_personal_repo_group_template_vars(c)
180 self._set_personal_repo_group_template_vars(c)
181 return self._get_template_context(c)
181 return self._get_template_context(c)
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasPermissionAllDecorator('hg.admin')
184 @HasPermissionAllDecorator('hg.admin')
185 @CSRFRequired()
185 @CSRFRequired()
186 @view_config(
186 @view_config(
187 route_name='users_create', request_method='POST',
187 route_name='users_create', request_method='POST',
188 renderer='rhodecode:templates/admin/users/user_add.mako')
188 renderer='rhodecode:templates/admin/users/user_add.mako')
189 def users_create(self):
189 def users_create(self):
190 _ = self.request.translate
190 _ = self.request.translate
191 c = self.load_default_context()
191 c = self.load_default_context()
192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
193 user_model = UserModel()
193 user_model = UserModel()
194 user_form = UserForm()()
194 user_form = UserForm()()
195 try:
195 try:
196 form_result = user_form.to_python(dict(self.request.POST))
196 form_result = user_form.to_python(dict(self.request.POST))
197 user = user_model.create(form_result)
197 user = user_model.create(form_result)
198 Session().flush()
198 Session().flush()
199 creation_data = user.get_api_data()
199 creation_data = user.get_api_data()
200 username = form_result['username']
200 username = form_result['username']
201
201
202 audit_logger.store_web(
202 audit_logger.store_web(
203 'user.create', action_data={'data': creation_data},
203 'user.create', action_data={'data': creation_data},
204 user=c.rhodecode_user)
204 user=c.rhodecode_user)
205
205
206 user_link = h.link_to(
206 user_link = h.link_to(
207 h.escape(username),
207 h.escape(username),
208 h.route_path('user_edit', user_id=user.user_id))
208 h.route_path('user_edit', user_id=user.user_id))
209 h.flash(h.literal(_('Created user %(user_link)s')
209 h.flash(h.literal(_('Created user %(user_link)s')
210 % {'user_link': user_link}), category='success')
210 % {'user_link': user_link}), category='success')
211 Session().commit()
211 Session().commit()
212 except formencode.Invalid as errors:
212 except formencode.Invalid as errors:
213 self._set_personal_repo_group_template_vars(c)
213 self._set_personal_repo_group_template_vars(c)
214 data = render(
214 data = render(
215 'rhodecode:templates/admin/users/user_add.mako',
215 'rhodecode:templates/admin/users/user_add.mako',
216 self._get_template_context(c), self.request)
216 self._get_template_context(c), self.request)
217 html = formencode.htmlfill.render(
217 html = formencode.htmlfill.render(
218 data,
218 data,
219 defaults=errors.value,
219 defaults=errors.value,
220 errors=errors.error_dict or {},
220 errors=errors.error_dict or {},
221 prefix_error=False,
221 prefix_error=False,
222 encoding="UTF-8",
222 encoding="UTF-8",
223 force_defaults=False
223 force_defaults=False
224 )
224 )
225 return Response(html)
225 return Response(html)
226 except UserCreationError as e:
226 except UserCreationError as e:
227 h.flash(e, 'error')
227 h.flash(e, 'error')
228 except Exception:
228 except Exception:
229 log.exception("Exception creation of user")
229 log.exception("Exception creation of user")
230 h.flash(_('Error occurred during creation of user %s')
230 h.flash(_('Error occurred during creation of user %s')
231 % self.request.POST.get('username'), category='error')
231 % self.request.POST.get('username'), category='error')
232 raise HTTPFound(h.route_path('users'))
232 raise HTTPFound(h.route_path('users'))
233
233
234
234
235 class UsersView(UserAppView):
235 class UsersView(UserAppView):
236 ALLOW_SCOPED_TOKENS = False
236 ALLOW_SCOPED_TOKENS = False
237 """
237 """
238 This view has alternative version inside EE, if modified please take a look
238 This view has alternative version inside EE, if modified please take a look
239 in there as well.
239 in there as well.
240 """
240 """
241
241
242 def load_default_context(self):
242 def load_default_context(self):
243 c = self._get_local_tmpl_context()
243 c = self._get_local_tmpl_context()
244 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
244 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
245 c.allowed_languages = [
245 c.allowed_languages = [
246 ('en', 'English (en)'),
246 ('en', 'English (en)'),
247 ('de', 'German (de)'),
247 ('de', 'German (de)'),
248 ('fr', 'French (fr)'),
248 ('fr', 'French (fr)'),
249 ('it', 'Italian (it)'),
249 ('it', 'Italian (it)'),
250 ('ja', 'Japanese (ja)'),
250 ('ja', 'Japanese (ja)'),
251 ('pl', 'Polish (pl)'),
251 ('pl', 'Polish (pl)'),
252 ('pt', 'Portuguese (pt)'),
252 ('pt', 'Portuguese (pt)'),
253 ('ru', 'Russian (ru)'),
253 ('ru', 'Russian (ru)'),
254 ('zh', 'Chinese (zh)'),
254 ('zh', 'Chinese (zh)'),
255 ]
255 ]
256 req = self.request
256 req = self.request
257
257
258 c.available_permissions = req.registry.settings['available_permissions']
258 c.available_permissions = req.registry.settings['available_permissions']
259 PermissionModel().set_global_permission_choices(
259 PermissionModel().set_global_permission_choices(
260 c, gettext_translator=req.translate)
260 c, gettext_translator=req.translate)
261
261
262 self._register_global_c(c)
262 self._register_global_c(c)
263 return c
263 return c
264
264
265 @LoginRequired()
265 @LoginRequired()
266 @HasPermissionAllDecorator('hg.admin')
266 @HasPermissionAllDecorator('hg.admin')
267 @CSRFRequired()
267 @CSRFRequired()
268 @view_config(
268 @view_config(
269 route_name='user_update', request_method='POST',
269 route_name='user_update', request_method='POST',
270 renderer='rhodecode:templates/admin/users/user_edit.mako')
270 renderer='rhodecode:templates/admin/users/user_edit.mako')
271 def user_update(self):
271 def user_update(self):
272 _ = self.request.translate
272 _ = self.request.translate
273 c = self.load_default_context()
273 c = self.load_default_context()
274
274
275 user_id = self.db_user_id
275 user_id = self.db_user_id
276 c.user = self.db_user
276 c.user = self.db_user
277
277
278 c.active = 'profile'
278 c.active = 'profile'
279 c.extern_type = c.user.extern_type
279 c.extern_type = c.user.extern_type
280 c.extern_name = c.user.extern_name
280 c.extern_name = c.user.extern_name
281 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
281 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
282 available_languages = [x[0] for x in c.allowed_languages]
282 available_languages = [x[0] for x in c.allowed_languages]
283 _form = UserForm(edit=True, available_languages=available_languages,
283 _form = UserForm(edit=True, available_languages=available_languages,
284 old_data={'user_id': user_id,
284 old_data={'user_id': user_id,
285 'email': c.user.email})()
285 'email': c.user.email})()
286 form_result = {}
286 form_result = {}
287 old_values = c.user.get_api_data()
287 old_values = c.user.get_api_data()
288 try:
288 try:
289 form_result = _form.to_python(dict(self.request.POST))
289 form_result = _form.to_python(dict(self.request.POST))
290 skip_attrs = ['extern_type', 'extern_name']
290 skip_attrs = ['extern_type', 'extern_name']
291 # TODO: plugin should define if username can be updated
291 # TODO: plugin should define if username can be updated
292 if c.extern_type != "rhodecode":
292 if c.extern_type != "rhodecode":
293 # forbid updating username for external accounts
293 # forbid updating username for external accounts
294 skip_attrs.append('username')
294 skip_attrs.append('username')
295
295
296 UserModel().update_user(
296 UserModel().update_user(
297 user_id, skip_attrs=skip_attrs, **form_result)
297 user_id, skip_attrs=skip_attrs, **form_result)
298
298
299 audit_logger.store_web(
299 audit_logger.store_web(
300 'user.edit', action_data={'old_data': old_values},
300 'user.edit', action_data={'old_data': old_values},
301 user=c.rhodecode_user)
301 user=c.rhodecode_user)
302
302
303 Session().commit()
303 Session().commit()
304 h.flash(_('User updated successfully'), category='success')
304 h.flash(_('User updated successfully'), category='success')
305 except formencode.Invalid as errors:
305 except formencode.Invalid as errors:
306 data = render(
306 data = render(
307 'rhodecode:templates/admin/users/user_edit.mako',
307 'rhodecode:templates/admin/users/user_edit.mako',
308 self._get_template_context(c), self.request)
308 self._get_template_context(c), self.request)
309 html = formencode.htmlfill.render(
309 html = formencode.htmlfill.render(
310 data,
310 data,
311 defaults=errors.value,
311 defaults=errors.value,
312 errors=errors.error_dict or {},
312 errors=errors.error_dict or {},
313 prefix_error=False,
313 prefix_error=False,
314 encoding="UTF-8",
314 encoding="UTF-8",
315 force_defaults=False
315 force_defaults=False
316 )
316 )
317 return Response(html)
317 return Response(html)
318 except UserCreationError as e:
318 except UserCreationError as e:
319 h.flash(e, 'error')
319 h.flash(e, 'error')
320 except Exception:
320 except Exception:
321 log.exception("Exception updating user")
321 log.exception("Exception updating user")
322 h.flash(_('Error occurred during update of user %s')
322 h.flash(_('Error occurred during update of user %s')
323 % form_result.get('username'), category='error')
323 % form_result.get('username'), category='error')
324 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
324 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
325
325
326 @LoginRequired()
326 @LoginRequired()
327 @HasPermissionAllDecorator('hg.admin')
327 @HasPermissionAllDecorator('hg.admin')
328 @CSRFRequired()
328 @CSRFRequired()
329 @view_config(
329 @view_config(
330 route_name='user_delete', request_method='POST',
330 route_name='user_delete', request_method='POST',
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 def user_delete(self):
332 def user_delete(self):
333 _ = self.request.translate
333 _ = self.request.translate
334 c = self.load_default_context()
334 c = self.load_default_context()
335 c.user = self.db_user
335 c.user = self.db_user
336
336
337 _repos = c.user.repositories
337 _repos = c.user.repositories
338 _repo_groups = c.user.repository_groups
338 _repo_groups = c.user.repository_groups
339 _user_groups = c.user.user_groups
339 _user_groups = c.user.user_groups
340
340
341 handle_repos = None
341 handle_repos = None
342 handle_repo_groups = None
342 handle_repo_groups = None
343 handle_user_groups = None
343 handle_user_groups = None
344 # dummy call for flash of handle
344 # dummy call for flash of handle
345 set_handle_flash_repos = lambda: None
345 set_handle_flash_repos = lambda: None
346 set_handle_flash_repo_groups = lambda: None
346 set_handle_flash_repo_groups = lambda: None
347 set_handle_flash_user_groups = lambda: None
347 set_handle_flash_user_groups = lambda: None
348
348
349 if _repos and self.request.POST.get('user_repos'):
349 if _repos and self.request.POST.get('user_repos'):
350 do = self.request.POST['user_repos']
350 do = self.request.POST['user_repos']
351 if do == 'detach':
351 if do == 'detach':
352 handle_repos = 'detach'
352 handle_repos = 'detach'
353 set_handle_flash_repos = lambda: h.flash(
353 set_handle_flash_repos = lambda: h.flash(
354 _('Detached %s repositories') % len(_repos),
354 _('Detached %s repositories') % len(_repos),
355 category='success')
355 category='success')
356 elif do == 'delete':
356 elif do == 'delete':
357 handle_repos = 'delete'
357 handle_repos = 'delete'
358 set_handle_flash_repos = lambda: h.flash(
358 set_handle_flash_repos = lambda: h.flash(
359 _('Deleted %s repositories') % len(_repos),
359 _('Deleted %s repositories') % len(_repos),
360 category='success')
360 category='success')
361
361
362 if _repo_groups and self.request.POST.get('user_repo_groups'):
362 if _repo_groups and self.request.POST.get('user_repo_groups'):
363 do = self.request.POST['user_repo_groups']
363 do = self.request.POST['user_repo_groups']
364 if do == 'detach':
364 if do == 'detach':
365 handle_repo_groups = 'detach'
365 handle_repo_groups = 'detach'
366 set_handle_flash_repo_groups = lambda: h.flash(
366 set_handle_flash_repo_groups = lambda: h.flash(
367 _('Detached %s repository groups') % len(_repo_groups),
367 _('Detached %s repository groups') % len(_repo_groups),
368 category='success')
368 category='success')
369 elif do == 'delete':
369 elif do == 'delete':
370 handle_repo_groups = 'delete'
370 handle_repo_groups = 'delete'
371 set_handle_flash_repo_groups = lambda: h.flash(
371 set_handle_flash_repo_groups = lambda: h.flash(
372 _('Deleted %s repository groups') % len(_repo_groups),
372 _('Deleted %s repository groups') % len(_repo_groups),
373 category='success')
373 category='success')
374
374
375 if _user_groups and self.request.POST.get('user_user_groups'):
375 if _user_groups and self.request.POST.get('user_user_groups'):
376 do = self.request.POST['user_user_groups']
376 do = self.request.POST['user_user_groups']
377 if do == 'detach':
377 if do == 'detach':
378 handle_user_groups = 'detach'
378 handle_user_groups = 'detach'
379 set_handle_flash_user_groups = lambda: h.flash(
379 set_handle_flash_user_groups = lambda: h.flash(
380 _('Detached %s user groups') % len(_user_groups),
380 _('Detached %s user groups') % len(_user_groups),
381 category='success')
381 category='success')
382 elif do == 'delete':
382 elif do == 'delete':
383 handle_user_groups = 'delete'
383 handle_user_groups = 'delete'
384 set_handle_flash_user_groups = lambda: h.flash(
384 set_handle_flash_user_groups = lambda: h.flash(
385 _('Deleted %s user groups') % len(_user_groups),
385 _('Deleted %s user groups') % len(_user_groups),
386 category='success')
386 category='success')
387
387
388 old_values = c.user.get_api_data()
388 old_values = c.user.get_api_data()
389 try:
389 try:
390 UserModel().delete(c.user, handle_repos=handle_repos,
390 UserModel().delete(c.user, handle_repos=handle_repos,
391 handle_repo_groups=handle_repo_groups,
391 handle_repo_groups=handle_repo_groups,
392 handle_user_groups=handle_user_groups)
392 handle_user_groups=handle_user_groups)
393
393
394 audit_logger.store_web(
394 audit_logger.store_web(
395 'user.delete', action_data={'old_data': old_values},
395 'user.delete', action_data={'old_data': old_values},
396 user=c.rhodecode_user)
396 user=c.rhodecode_user)
397
397
398 Session().commit()
398 Session().commit()
399 set_handle_flash_repos()
399 set_handle_flash_repos()
400 set_handle_flash_repo_groups()
400 set_handle_flash_repo_groups()
401 set_handle_flash_user_groups()
401 set_handle_flash_user_groups()
402 h.flash(_('Successfully deleted user'), category='success')
402 h.flash(_('Successfully deleted user'), category='success')
403 except (UserOwnsReposException, UserOwnsRepoGroupsException,
403 except (UserOwnsReposException, UserOwnsRepoGroupsException,
404 UserOwnsUserGroupsException, DefaultUserException) as e:
404 UserOwnsUserGroupsException, DefaultUserException) as e:
405 h.flash(e, category='warning')
405 h.flash(e, category='warning')
406 except Exception:
406 except Exception:
407 log.exception("Exception during deletion of user")
407 log.exception("Exception during deletion of user")
408 h.flash(_('An error occurred during deletion of user'),
408 h.flash(_('An error occurred during deletion of user'),
409 category='error')
409 category='error')
410 raise HTTPFound(h.route_path('users'))
410 raise HTTPFound(h.route_path('users'))
411
411
412 @LoginRequired()
412 @LoginRequired()
413 @HasPermissionAllDecorator('hg.admin')
413 @HasPermissionAllDecorator('hg.admin')
414 @view_config(
414 @view_config(
415 route_name='user_edit', request_method='GET',
415 route_name='user_edit', request_method='GET',
416 renderer='rhodecode:templates/admin/users/user_edit.mako')
416 renderer='rhodecode:templates/admin/users/user_edit.mako')
417 def user_edit(self):
417 def user_edit(self):
418 _ = self.request.translate
418 _ = self.request.translate
419 c = self.load_default_context()
419 c = self.load_default_context()
420 c.user = self.db_user
420 c.user = self.db_user
421
421
422 c.active = 'profile'
422 c.active = 'profile'
423 c.extern_type = c.user.extern_type
423 c.extern_type = c.user.extern_type
424 c.extern_name = c.user.extern_name
424 c.extern_name = c.user.extern_name
425 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
425 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
426
426
427 defaults = c.user.get_dict()
427 defaults = c.user.get_dict()
428 defaults.update({'language': c.user.user_data.get('language')})
428 defaults.update({'language': c.user.user_data.get('language')})
429
429
430 data = render(
430 data = render(
431 'rhodecode:templates/admin/users/user_edit.mako',
431 'rhodecode:templates/admin/users/user_edit.mako',
432 self._get_template_context(c), self.request)
432 self._get_template_context(c), self.request)
433 html = formencode.htmlfill.render(
433 html = formencode.htmlfill.render(
434 data,
434 data,
435 defaults=defaults,
435 defaults=defaults,
436 encoding="UTF-8",
436 encoding="UTF-8",
437 force_defaults=False
437 force_defaults=False
438 )
438 )
439 return Response(html)
439 return Response(html)
440
440
441 @LoginRequired()
441 @LoginRequired()
442 @HasPermissionAllDecorator('hg.admin')
442 @HasPermissionAllDecorator('hg.admin')
443 @view_config(
443 @view_config(
444 route_name='user_edit_advanced', request_method='GET',
444 route_name='user_edit_advanced', request_method='GET',
445 renderer='rhodecode:templates/admin/users/user_edit.mako')
445 renderer='rhodecode:templates/admin/users/user_edit.mako')
446 def user_edit_advanced(self):
446 def user_edit_advanced(self):
447 _ = self.request.translate
447 _ = self.request.translate
448 c = self.load_default_context()
448 c = self.load_default_context()
449
449
450 user_id = self.db_user_id
450 user_id = self.db_user_id
451 c.user = self.db_user
451 c.user = self.db_user
452
452
453 c.active = 'advanced'
453 c.active = 'advanced'
454 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
454 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
455 c.personal_repo_group_name = RepoGroupModel()\
455 c.personal_repo_group_name = RepoGroupModel()\
456 .get_personal_group_name(c.user)
456 .get_personal_group_name(c.user)
457
457
458 c.user_to_review_rules = sorted(
458 c.user_to_review_rules = sorted(
459 (x.user for x in c.user.user_review_rules),
459 (x.user for x in c.user.user_review_rules),
460 key=lambda u: u.username.lower())
460 key=lambda u: u.username.lower())
461
461
462 c.first_admin = User.get_first_super_admin()
462 c.first_admin = User.get_first_super_admin()
463 defaults = c.user.get_dict()
463 defaults = c.user.get_dict()
464
464
465 # Interim workaround if the user participated on any pull requests as a
465 # Interim workaround if the user participated on any pull requests as a
466 # reviewer.
466 # reviewer.
467 has_review = len(c.user.reviewer_pull_requests)
467 has_review = len(c.user.reviewer_pull_requests)
468 c.can_delete_user = not has_review
468 c.can_delete_user = not has_review
469 c.can_delete_user_message = ''
469 c.can_delete_user_message = ''
470 inactive_link = h.link_to(
470 inactive_link = h.link_to(
471 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
471 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
472 if has_review == 1:
472 if has_review == 1:
473 c.can_delete_user_message = h.literal(_(
473 c.can_delete_user_message = h.literal(_(
474 'The user participates as reviewer in {} pull request and '
474 'The user participates as reviewer in {} pull request and '
475 'cannot be deleted. \nYou can set the user to '
475 'cannot be deleted. \nYou can set the user to '
476 '"{}" instead of deleting it.').format(
476 '"{}" instead of deleting it.').format(
477 has_review, inactive_link))
477 has_review, inactive_link))
478 elif has_review:
478 elif has_review:
479 c.can_delete_user_message = h.literal(_(
479 c.can_delete_user_message = h.literal(_(
480 'The user participates as reviewer in {} pull requests and '
480 'The user participates as reviewer in {} pull requests and '
481 'cannot be deleted. \nYou can set the user to '
481 'cannot be deleted. \nYou can set the user to '
482 '"{}" instead of deleting it.').format(
482 '"{}" instead of deleting it.').format(
483 has_review, inactive_link))
483 has_review, inactive_link))
484
484
485 data = render(
485 data = render(
486 'rhodecode:templates/admin/users/user_edit.mako',
486 'rhodecode:templates/admin/users/user_edit.mako',
487 self._get_template_context(c), self.request)
487 self._get_template_context(c), self.request)
488 html = formencode.htmlfill.render(
488 html = formencode.htmlfill.render(
489 data,
489 data,
490 defaults=defaults,
490 defaults=defaults,
491 encoding="UTF-8",
491 encoding="UTF-8",
492 force_defaults=False
492 force_defaults=False
493 )
493 )
494 return Response(html)
494 return Response(html)
495
495
496 @LoginRequired()
496 @LoginRequired()
497 @HasPermissionAllDecorator('hg.admin')
497 @HasPermissionAllDecorator('hg.admin')
498 @view_config(
498 @view_config(
499 route_name='user_edit_global_perms', request_method='GET',
499 route_name='user_edit_global_perms', request_method='GET',
500 renderer='rhodecode:templates/admin/users/user_edit.mako')
500 renderer='rhodecode:templates/admin/users/user_edit.mako')
501 def user_edit_global_perms(self):
501 def user_edit_global_perms(self):
502 _ = self.request.translate
502 _ = self.request.translate
503 c = self.load_default_context()
503 c = self.load_default_context()
504 c.user = self.db_user
504 c.user = self.db_user
505
505
506 c.active = 'global_perms'
506 c.active = 'global_perms'
507
507
508 c.default_user = User.get_default_user()
508 c.default_user = User.get_default_user()
509 defaults = c.user.get_dict()
509 defaults = c.user.get_dict()
510 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
510 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
511 defaults.update(c.default_user.get_default_perms())
511 defaults.update(c.default_user.get_default_perms())
512 defaults.update(c.user.get_default_perms())
512 defaults.update(c.user.get_default_perms())
513
513
514 data = render(
514 data = render(
515 'rhodecode:templates/admin/users/user_edit.mako',
515 'rhodecode:templates/admin/users/user_edit.mako',
516 self._get_template_context(c), self.request)
516 self._get_template_context(c), self.request)
517 html = formencode.htmlfill.render(
517 html = formencode.htmlfill.render(
518 data,
518 data,
519 defaults=defaults,
519 defaults=defaults,
520 encoding="UTF-8",
520 encoding="UTF-8",
521 force_defaults=False
521 force_defaults=False
522 )
522 )
523 return Response(html)
523 return Response(html)
524
524
525 @LoginRequired()
525 @LoginRequired()
526 @HasPermissionAllDecorator('hg.admin')
526 @HasPermissionAllDecorator('hg.admin')
527 @CSRFRequired()
527 @CSRFRequired()
528 @view_config(
528 @view_config(
529 route_name='user_edit_global_perms_update', request_method='POST',
529 route_name='user_edit_global_perms_update', request_method='POST',
530 renderer='rhodecode:templates/admin/users/user_edit.mako')
530 renderer='rhodecode:templates/admin/users/user_edit.mako')
531 def user_edit_global_perms_update(self):
531 def user_edit_global_perms_update(self):
532 _ = self.request.translate
532 _ = self.request.translate
533 c = self.load_default_context()
533 c = self.load_default_context()
534
534
535 user_id = self.db_user_id
535 user_id = self.db_user_id
536 c.user = self.db_user
536 c.user = self.db_user
537
537
538 c.active = 'global_perms'
538 c.active = 'global_perms'
539 try:
539 try:
540 # first stage that verifies the checkbox
540 # first stage that verifies the checkbox
541 _form = UserIndividualPermissionsForm()
541 _form = UserIndividualPermissionsForm()
542 form_result = _form.to_python(dict(self.request.POST))
542 form_result = _form.to_python(dict(self.request.POST))
543 inherit_perms = form_result['inherit_default_permissions']
543 inherit_perms = form_result['inherit_default_permissions']
544 c.user.inherit_default_permissions = inherit_perms
544 c.user.inherit_default_permissions = inherit_perms
545 Session().add(c.user)
545 Session().add(c.user)
546
546
547 if not inherit_perms:
547 if not inherit_perms:
548 # only update the individual ones if we un check the flag
548 # only update the individual ones if we un check the flag
549 _form = UserPermissionsForm(
549 _form = UserPermissionsForm(
550 [x[0] for x in c.repo_create_choices],
550 [x[0] for x in c.repo_create_choices],
551 [x[0] for x in c.repo_create_on_write_choices],
551 [x[0] for x in c.repo_create_on_write_choices],
552 [x[0] for x in c.repo_group_create_choices],
552 [x[0] for x in c.repo_group_create_choices],
553 [x[0] for x in c.user_group_create_choices],
553 [x[0] for x in c.user_group_create_choices],
554 [x[0] for x in c.fork_choices],
554 [x[0] for x in c.fork_choices],
555 [x[0] for x in c.inherit_default_permission_choices])()
555 [x[0] for x in c.inherit_default_permission_choices])()
556
556
557 form_result = _form.to_python(dict(self.request.POST))
557 form_result = _form.to_python(dict(self.request.POST))
558 form_result.update({'perm_user_id': c.user.user_id})
558 form_result.update({'perm_user_id': c.user.user_id})
559
559
560 PermissionModel().update_user_permissions(form_result)
560 PermissionModel().update_user_permissions(form_result)
561
561
562 # TODO(marcink): implement global permissions
562 # TODO(marcink): implement global permissions
563 # audit_log.store_web('user.edit.permissions')
563 # audit_log.store_web('user.edit.permissions')
564
564
565 Session().commit()
565 Session().commit()
566 h.flash(_('User global permissions updated successfully'),
566 h.flash(_('User global permissions updated successfully'),
567 category='success')
567 category='success')
568
568
569 except formencode.Invalid as errors:
569 except formencode.Invalid as errors:
570 data = render(
570 data = render(
571 'rhodecode:templates/admin/users/user_edit.mako',
571 'rhodecode:templates/admin/users/user_edit.mako',
572 self._get_template_context(c), self.request)
572 self._get_template_context(c), self.request)
573 html = formencode.htmlfill.render(
573 html = formencode.htmlfill.render(
574 data,
574 data,
575 defaults=errors.value,
575 defaults=errors.value,
576 errors=errors.error_dict or {},
576 errors=errors.error_dict or {},
577 prefix_error=False,
577 prefix_error=False,
578 encoding="UTF-8",
578 encoding="UTF-8",
579 force_defaults=False
579 force_defaults=False
580 )
580 )
581 return Response(html)
581 return Response(html)
582 except Exception:
582 except Exception:
583 log.exception("Exception during permissions saving")
583 log.exception("Exception during permissions saving")
584 h.flash(_('An error occurred during permissions saving'),
584 h.flash(_('An error occurred during permissions saving'),
585 category='error')
585 category='error')
586 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
586 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
587
587
588 @LoginRequired()
588 @LoginRequired()
589 @HasPermissionAllDecorator('hg.admin')
589 @HasPermissionAllDecorator('hg.admin')
590 @CSRFRequired()
590 @CSRFRequired()
591 @view_config(
591 @view_config(
592 route_name='user_force_password_reset', request_method='POST',
592 route_name='user_force_password_reset', request_method='POST',
593 renderer='rhodecode:templates/admin/users/user_edit.mako')
593 renderer='rhodecode:templates/admin/users/user_edit.mako')
594 def user_force_password_reset(self):
594 def user_force_password_reset(self):
595 """
595 """
596 toggle reset password flag for this user
596 toggle reset password flag for this user
597 """
597 """
598 _ = self.request.translate
598 _ = self.request.translate
599 c = self.load_default_context()
599 c = self.load_default_context()
600
600
601 user_id = self.db_user_id
601 user_id = self.db_user_id
602 c.user = self.db_user
602 c.user = self.db_user
603
603
604 try:
604 try:
605 old_value = c.user.user_data.get('force_password_change')
605 old_value = c.user.user_data.get('force_password_change')
606 c.user.update_userdata(force_password_change=not old_value)
606 c.user.update_userdata(force_password_change=not old_value)
607
607
608 if old_value:
608 if old_value:
609 msg = _('Force password change disabled for user')
609 msg = _('Force password change disabled for user')
610 audit_logger.store_web(
610 audit_logger.store_web(
611 'user.edit.password_reset.disabled',
611 'user.edit.password_reset.disabled',
612 user=c.rhodecode_user)
612 user=c.rhodecode_user)
613 else:
613 else:
614 msg = _('Force password change enabled for user')
614 msg = _('Force password change enabled for user')
615 audit_logger.store_web(
615 audit_logger.store_web(
616 'user.edit.password_reset.enabled',
616 'user.edit.password_reset.enabled',
617 user=c.rhodecode_user)
617 user=c.rhodecode_user)
618
618
619 Session().commit()
619 Session().commit()
620 h.flash(msg, category='success')
620 h.flash(msg, category='success')
621 except Exception:
621 except Exception:
622 log.exception("Exception during password reset for user")
622 log.exception("Exception during password reset for user")
623 h.flash(_('An error occurred during password reset for user'),
623 h.flash(_('An error occurred during password reset for user'),
624 category='error')
624 category='error')
625
625
626 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
626 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
627
627
628 @LoginRequired()
628 @LoginRequired()
629 @HasPermissionAllDecorator('hg.admin')
629 @HasPermissionAllDecorator('hg.admin')
630 @CSRFRequired()
630 @CSRFRequired()
631 @view_config(
631 @view_config(
632 route_name='user_create_personal_repo_group', request_method='POST',
632 route_name='user_create_personal_repo_group', request_method='POST',
633 renderer='rhodecode:templates/admin/users/user_edit.mako')
633 renderer='rhodecode:templates/admin/users/user_edit.mako')
634 def user_create_personal_repo_group(self):
634 def user_create_personal_repo_group(self):
635 """
635 """
636 Create personal repository group for this user
636 Create personal repository group for this user
637 """
637 """
638 from rhodecode.model.repo_group import RepoGroupModel
638 from rhodecode.model.repo_group import RepoGroupModel
639
639
640 _ = self.request.translate
640 _ = self.request.translate
641 c = self.load_default_context()
641 c = self.load_default_context()
642
642
643 user_id = self.db_user_id
643 user_id = self.db_user_id
644 c.user = self.db_user
644 c.user = self.db_user
645
645
646 personal_repo_group = RepoGroup.get_user_personal_repo_group(
646 personal_repo_group = RepoGroup.get_user_personal_repo_group(
647 c.user.user_id)
647 c.user.user_id)
648 if personal_repo_group:
648 if personal_repo_group:
649 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
649 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
650
650
651 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
651 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
652 c.user)
652 c.user)
653 named_personal_group = RepoGroup.get_by_group_name(
653 named_personal_group = RepoGroup.get_by_group_name(
654 personal_repo_group_name)
654 personal_repo_group_name)
655 try:
655 try:
656
656
657 if named_personal_group and named_personal_group.user_id == c.user.user_id:
657 if named_personal_group and named_personal_group.user_id == c.user.user_id:
658 # migrate the same named group, and mark it as personal
658 # migrate the same named group, and mark it as personal
659 named_personal_group.personal = True
659 named_personal_group.personal = True
660 Session().add(named_personal_group)
660 Session().add(named_personal_group)
661 Session().commit()
661 Session().commit()
662 msg = _('Linked repository group `%s` as personal' % (
662 msg = _('Linked repository group `%s` as personal' % (
663 personal_repo_group_name,))
663 personal_repo_group_name,))
664 h.flash(msg, category='success')
664 h.flash(msg, category='success')
665 elif not named_personal_group:
665 elif not named_personal_group:
666 RepoGroupModel().create_personal_repo_group(c.user)
666 RepoGroupModel().create_personal_repo_group(c.user)
667
667
668 msg = _('Created repository group `%s`' % (
668 msg = _('Created repository group `%s`' % (
669 personal_repo_group_name,))
669 personal_repo_group_name,))
670 h.flash(msg, category='success')
670 h.flash(msg, category='success')
671 else:
671 else:
672 msg = _('Repository group `%s` is already taken' % (
672 msg = _('Repository group `%s` is already taken' % (
673 personal_repo_group_name,))
673 personal_repo_group_name,))
674 h.flash(msg, category='warning')
674 h.flash(msg, category='warning')
675 except Exception:
675 except Exception:
676 log.exception("Exception during repository group creation")
676 log.exception("Exception during repository group creation")
677 msg = _(
677 msg = _(
678 'An error occurred during repository group creation for user')
678 'An error occurred during repository group creation for user')
679 h.flash(msg, category='error')
679 h.flash(msg, category='error')
680 Session().rollback()
680 Session().rollback()
681
681
682 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
682 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
683
683
684 @LoginRequired()
684 @LoginRequired()
685 @HasPermissionAllDecorator('hg.admin')
685 @HasPermissionAllDecorator('hg.admin')
686 @view_config(
686 @view_config(
687 route_name='edit_user_auth_tokens', request_method='GET',
687 route_name='edit_user_auth_tokens', request_method='GET',
688 renderer='rhodecode:templates/admin/users/user_edit.mako')
688 renderer='rhodecode:templates/admin/users/user_edit.mako')
689 def auth_tokens(self):
689 def auth_tokens(self):
690 _ = self.request.translate
690 _ = self.request.translate
691 c = self.load_default_context()
691 c = self.load_default_context()
692 c.user = self.db_user
692 c.user = self.db_user
693
693
694 c.active = 'auth_tokens'
694 c.active = 'auth_tokens'
695
695
696 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
696 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
697 c.role_values = [
697 c.role_values = [
698 (x, AuthTokenModel.cls._get_role_name(x))
698 (x, AuthTokenModel.cls._get_role_name(x))
699 for x in AuthTokenModel.cls.ROLES]
699 for x in AuthTokenModel.cls.ROLES]
700 c.role_options = [(c.role_values, _("Role"))]
700 c.role_options = [(c.role_values, _("Role"))]
701 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
701 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
702 c.user.user_id, show_expired=True)
702 c.user.user_id, show_expired=True)
703 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
703 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
704 return self._get_template_context(c)
704 return self._get_template_context(c)
705
705
706 def maybe_attach_token_scope(self, token):
706 def maybe_attach_token_scope(self, token):
707 # implemented in EE edition
707 # implemented in EE edition
708 pass
708 pass
709
709
710 @LoginRequired()
710 @LoginRequired()
711 @HasPermissionAllDecorator('hg.admin')
711 @HasPermissionAllDecorator('hg.admin')
712 @CSRFRequired()
712 @CSRFRequired()
713 @view_config(
713 @view_config(
714 route_name='edit_user_auth_tokens_add', request_method='POST')
714 route_name='edit_user_auth_tokens_add', request_method='POST')
715 def auth_tokens_add(self):
715 def auth_tokens_add(self):
716 _ = self.request.translate
716 _ = self.request.translate
717 c = self.load_default_context()
717 c = self.load_default_context()
718
718
719 user_id = self.db_user_id
719 user_id = self.db_user_id
720 c.user = self.db_user
720 c.user = self.db_user
721
721
722 user_data = c.user.get_api_data()
722 user_data = c.user.get_api_data()
723 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
723 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
724 description = self.request.POST.get('description')
724 description = self.request.POST.get('description')
725 role = self.request.POST.get('role')
725 role = self.request.POST.get('role')
726
726
727 token = AuthTokenModel().create(
727 token = AuthTokenModel().create(
728 c.user.user_id, description, lifetime, role)
728 c.user.user_id, description, lifetime, role)
729 token_data = token.get_api_data()
729 token_data = token.get_api_data()
730
730
731 self.maybe_attach_token_scope(token)
731 self.maybe_attach_token_scope(token)
732 audit_logger.store_web(
732 audit_logger.store_web(
733 'user.edit.token.add', action_data={
733 'user.edit.token.add', action_data={
734 'data': {'token': token_data, 'user': user_data}},
734 'data': {'token': token_data, 'user': user_data}},
735 user=self._rhodecode_user, )
735 user=self._rhodecode_user, )
736 Session().commit()
736 Session().commit()
737
737
738 h.flash(_("Auth token successfully created"), category='success')
738 h.flash(_("Auth token successfully created"), category='success')
739 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
739 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
740
740
741 @LoginRequired()
741 @LoginRequired()
742 @HasPermissionAllDecorator('hg.admin')
742 @HasPermissionAllDecorator('hg.admin')
743 @CSRFRequired()
743 @CSRFRequired()
744 @view_config(
744 @view_config(
745 route_name='edit_user_auth_tokens_delete', request_method='POST')
745 route_name='edit_user_auth_tokens_delete', request_method='POST')
746 def auth_tokens_delete(self):
746 def auth_tokens_delete(self):
747 _ = self.request.translate
747 _ = self.request.translate
748 c = self.load_default_context()
748 c = self.load_default_context()
749
749
750 user_id = self.db_user_id
750 user_id = self.db_user_id
751 c.user = self.db_user
751 c.user = self.db_user
752
752
753 user_data = c.user.get_api_data()
753 user_data = c.user.get_api_data()
754
754
755 del_auth_token = self.request.POST.get('del_auth_token')
755 del_auth_token = self.request.POST.get('del_auth_token')
756
756
757 if del_auth_token:
757 if del_auth_token:
758 token = UserApiKeys.get_or_404(del_auth_token)
758 token = UserApiKeys.get_or_404(del_auth_token)
759 token_data = token.get_api_data()
759 token_data = token.get_api_data()
760
760
761 AuthTokenModel().delete(del_auth_token, c.user.user_id)
761 AuthTokenModel().delete(del_auth_token, c.user.user_id)
762 audit_logger.store_web(
762 audit_logger.store_web(
763 'user.edit.token.delete', action_data={
763 'user.edit.token.delete', action_data={
764 'data': {'token': token_data, 'user': user_data}},
764 'data': {'token': token_data, 'user': user_data}},
765 user=self._rhodecode_user,)
765 user=self._rhodecode_user,)
766 Session().commit()
766 Session().commit()
767 h.flash(_("Auth token successfully deleted"), category='success')
767 h.flash(_("Auth token successfully deleted"), category='success')
768
768
769 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
769 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
770
770
771 @LoginRequired()
771 @LoginRequired()
772 @HasPermissionAllDecorator('hg.admin')
772 @HasPermissionAllDecorator('hg.admin')
773 @view_config(
773 @view_config(
774 route_name='edit_user_ssh_keys', request_method='GET',
774 route_name='edit_user_ssh_keys', request_method='GET',
775 renderer='rhodecode:templates/admin/users/user_edit.mako')
775 renderer='rhodecode:templates/admin/users/user_edit.mako')
776 def ssh_keys(self):
776 def ssh_keys(self):
777 _ = self.request.translate
777 _ = self.request.translate
778 c = self.load_default_context()
778 c = self.load_default_context()
779 c.user = self.db_user
779 c.user = self.db_user
780
780
781 c.active = 'ssh_keys'
781 c.active = 'ssh_keys'
782 c.default_key = self.request.GET.get('default_key')
782 c.default_key = self.request.GET.get('default_key')
783 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
783 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
784 return self._get_template_context(c)
784 return self._get_template_context(c)
785
785
786 @LoginRequired()
786 @LoginRequired()
787 @HasPermissionAllDecorator('hg.admin')
787 @HasPermissionAllDecorator('hg.admin')
788 @view_config(
788 @view_config(
789 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
789 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
790 renderer='rhodecode:templates/admin/users/user_edit.mako')
790 renderer='rhodecode:templates/admin/users/user_edit.mako')
791 def ssh_keys_generate_keypair(self):
791 def ssh_keys_generate_keypair(self):
792 _ = self.request.translate
792 _ = self.request.translate
793 c = self.load_default_context()
793 c = self.load_default_context()
794
794
795 c.user = self.db_user
795 c.user = self.db_user
796
796
797 c.active = 'ssh_keys_generate'
797 c.active = 'ssh_keys_generate'
798 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
798 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
799 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
799 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
800
800
801 return self._get_template_context(c)
801 return self._get_template_context(c)
802
802
803 @LoginRequired()
803 @LoginRequired()
804 @HasPermissionAllDecorator('hg.admin')
804 @HasPermissionAllDecorator('hg.admin')
805 @CSRFRequired()
805 @CSRFRequired()
806 @view_config(
806 @view_config(
807 route_name='edit_user_ssh_keys_add', request_method='POST')
807 route_name='edit_user_ssh_keys_add', request_method='POST')
808 def ssh_keys_add(self):
808 def ssh_keys_add(self):
809 _ = self.request.translate
809 _ = self.request.translate
810 c = self.load_default_context()
810 c = self.load_default_context()
811
811
812 user_id = self.db_user_id
812 user_id = self.db_user_id
813 c.user = self.db_user
813 c.user = self.db_user
814
814
815 user_data = c.user.get_api_data()
815 user_data = c.user.get_api_data()
816 key_data = self.request.POST.get('key_data')
816 key_data = self.request.POST.get('key_data')
817 description = self.request.POST.get('description')
817 description = self.request.POST.get('description')
818
818
819 try:
819 try:
820 if not key_data:
820 if not key_data:
821 raise ValueError('Please add a valid public key')
821 raise ValueError('Please add a valid public key')
822
822
823 key = SshKeyModel().parse_key(key_data.strip())
823 key = SshKeyModel().parse_key(key_data.strip())
824 fingerprint = key.hash_md5()
824 fingerprint = key.hash_md5()
825
825
826 ssh_key = SshKeyModel().create(
826 ssh_key = SshKeyModel().create(
827 c.user.user_id, fingerprint, key_data, description)
827 c.user.user_id, fingerprint, key_data, description)
828 ssh_key_data = ssh_key.get_api_data()
828 ssh_key_data = ssh_key.get_api_data()
829
829
830 audit_logger.store_web(
830 audit_logger.store_web(
831 'user.edit.ssh_key.add', action_data={
831 'user.edit.ssh_key.add', action_data={
832 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
832 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
833 user=self._rhodecode_user, )
833 user=self._rhodecode_user, )
834 Session().commit()
834 Session().commit()
835
835
836 # Trigger an event on change of keys.
836 # Trigger an event on change of keys.
837 trigger(SshKeyFileChangeEvent(), self.request.registry)
837 trigger(SshKeyFileChangeEvent(), self.request.registry)
838
838
839 h.flash(_("Ssh Key successfully created"), category='success')
839 h.flash(_("Ssh Key successfully created"), category='success')
840
840
841 except IntegrityError:
841 except IntegrityError:
842 log.exception("Exception during ssh key saving")
842 log.exception("Exception during ssh key saving")
843 h.flash(_('An error occurred during ssh key saving: {}').format(
843 h.flash(_('An error occurred during ssh key saving: {}').format(
844 'Such key already exists, please use a different one'),
844 'Such key already exists, please use a different one'),
845 category='error')
845 category='error')
846 except Exception as e:
846 except Exception as e:
847 log.exception("Exception during ssh key saving")
847 log.exception("Exception during ssh key saving")
848 h.flash(_('An error occurred during ssh key saving: {}').format(e),
848 h.flash(_('An error occurred during ssh key saving: {}').format(e),
849 category='error')
849 category='error')
850
850
851 return HTTPFound(
851 return HTTPFound(
852 h.route_path('edit_user_ssh_keys', user_id=user_id))
852 h.route_path('edit_user_ssh_keys', user_id=user_id))
853
853
854 @LoginRequired()
854 @LoginRequired()
855 @HasPermissionAllDecorator('hg.admin')
855 @HasPermissionAllDecorator('hg.admin')
856 @CSRFRequired()
856 @CSRFRequired()
857 @view_config(
857 @view_config(
858 route_name='edit_user_ssh_keys_delete', request_method='POST')
858 route_name='edit_user_ssh_keys_delete', request_method='POST')
859 def ssh_keys_delete(self):
859 def ssh_keys_delete(self):
860 _ = self.request.translate
860 _ = self.request.translate
861 c = self.load_default_context()
861 c = self.load_default_context()
862
862
863 user_id = self.db_user_id
863 user_id = self.db_user_id
864 c.user = self.db_user
864 c.user = self.db_user
865
865
866 user_data = c.user.get_api_data()
866 user_data = c.user.get_api_data()
867
867
868 del_ssh_key = self.request.POST.get('del_ssh_key')
868 del_ssh_key = self.request.POST.get('del_ssh_key')
869
869
870 if del_ssh_key:
870 if del_ssh_key:
871 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
871 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
872 ssh_key_data = ssh_key.get_api_data()
872 ssh_key_data = ssh_key.get_api_data()
873
873
874 SshKeyModel().delete(del_ssh_key, c.user.user_id)
874 SshKeyModel().delete(del_ssh_key, c.user.user_id)
875 audit_logger.store_web(
875 audit_logger.store_web(
876 'user.edit.ssh_key.delete', action_data={
876 'user.edit.ssh_key.delete', action_data={
877 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
877 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
878 user=self._rhodecode_user,)
878 user=self._rhodecode_user,)
879 Session().commit()
879 Session().commit()
880 # Trigger an event on change of keys.
880 # Trigger an event on change of keys.
881 trigger(SshKeyFileChangeEvent(), self.request.registry)
881 trigger(SshKeyFileChangeEvent(), self.request.registry)
882 h.flash(_("Ssh key successfully deleted"), category='success')
882 h.flash(_("Ssh key successfully deleted"), category='success')
883
883
884 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
884 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
885
885
886 @LoginRequired()
886 @LoginRequired()
887 @HasPermissionAllDecorator('hg.admin')
887 @HasPermissionAllDecorator('hg.admin')
888 @view_config(
888 @view_config(
889 route_name='edit_user_emails', request_method='GET',
889 route_name='edit_user_emails', request_method='GET',
890 renderer='rhodecode:templates/admin/users/user_edit.mako')
890 renderer='rhodecode:templates/admin/users/user_edit.mako')
891 def emails(self):
891 def emails(self):
892 _ = self.request.translate
892 _ = self.request.translate
893 c = self.load_default_context()
893 c = self.load_default_context()
894 c.user = self.db_user
894 c.user = self.db_user
895
895
896 c.active = 'emails'
896 c.active = 'emails'
897 c.user_email_map = UserEmailMap.query() \
897 c.user_email_map = UserEmailMap.query() \
898 .filter(UserEmailMap.user == c.user).all()
898 .filter(UserEmailMap.user == c.user).all()
899
899
900 return self._get_template_context(c)
900 return self._get_template_context(c)
901
901
902 @LoginRequired()
902 @LoginRequired()
903 @HasPermissionAllDecorator('hg.admin')
903 @HasPermissionAllDecorator('hg.admin')
904 @CSRFRequired()
904 @CSRFRequired()
905 @view_config(
905 @view_config(
906 route_name='edit_user_emails_add', request_method='POST')
906 route_name='edit_user_emails_add', request_method='POST')
907 def emails_add(self):
907 def emails_add(self):
908 _ = self.request.translate
908 _ = self.request.translate
909 c = self.load_default_context()
909 c = self.load_default_context()
910
910
911 user_id = self.db_user_id
911 user_id = self.db_user_id
912 c.user = self.db_user
912 c.user = self.db_user
913
913
914 email = self.request.POST.get('new_email')
914 email = self.request.POST.get('new_email')
915 user_data = c.user.get_api_data()
915 user_data = c.user.get_api_data()
916 try:
916 try:
917 UserModel().add_extra_email(c.user.user_id, email)
917 UserModel().add_extra_email(c.user.user_id, email)
918 audit_logger.store_web(
918 audit_logger.store_web(
919 'user.edit.email.add',
919 'user.edit.email.add',
920 action_data={'email': email, 'user': user_data},
920 action_data={'email': email, 'user': user_data},
921 user=self._rhodecode_user)
921 user=self._rhodecode_user)
922 Session().commit()
922 Session().commit()
923 h.flash(_("Added new email address `%s` for user account") % email,
923 h.flash(_("Added new email address `%s` for user account") % email,
924 category='success')
924 category='success')
925 except formencode.Invalid as error:
925 except formencode.Invalid as error:
926 h.flash(h.escape(error.error_dict['email']), category='error')
926 h.flash(h.escape(error.error_dict['email']), category='error')
927 except IntegrityError:
927 except IntegrityError:
928 log.warning("Email %s already exists", email)
928 log.warning("Email %s already exists", email)
929 h.flash(_('Email `{}` is already registered for another user.').format(email),
929 h.flash(_('Email `{}` is already registered for another user.').format(email),
930 category='error')
930 category='error')
931 except Exception:
931 except Exception:
932 log.exception("Exception during email saving")
932 log.exception("Exception during email saving")
933 h.flash(_('An error occurred during email saving'),
933 h.flash(_('An error occurred during email saving'),
934 category='error')
934 category='error')
935 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
935 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
936
936
937 @LoginRequired()
937 @LoginRequired()
938 @HasPermissionAllDecorator('hg.admin')
938 @HasPermissionAllDecorator('hg.admin')
939 @CSRFRequired()
939 @CSRFRequired()
940 @view_config(
940 @view_config(
941 route_name='edit_user_emails_delete', request_method='POST')
941 route_name='edit_user_emails_delete', request_method='POST')
942 def emails_delete(self):
942 def emails_delete(self):
943 _ = self.request.translate
943 _ = self.request.translate
944 c = self.load_default_context()
944 c = self.load_default_context()
945
945
946 user_id = self.db_user_id
946 user_id = self.db_user_id
947 c.user = self.db_user
947 c.user = self.db_user
948
948
949 email_id = self.request.POST.get('del_email_id')
949 email_id = self.request.POST.get('del_email_id')
950 user_model = UserModel()
950 user_model = UserModel()
951
951
952 email = UserEmailMap.query().get(email_id).email
952 email = UserEmailMap.query().get(email_id).email
953 user_data = c.user.get_api_data()
953 user_data = c.user.get_api_data()
954 user_model.delete_extra_email(c.user.user_id, email_id)
954 user_model.delete_extra_email(c.user.user_id, email_id)
955 audit_logger.store_web(
955 audit_logger.store_web(
956 'user.edit.email.delete',
956 'user.edit.email.delete',
957 action_data={'email': email, 'user': user_data},
957 action_data={'email': email, 'user': user_data},
958 user=self._rhodecode_user)
958 user=self._rhodecode_user)
959 Session().commit()
959 Session().commit()
960 h.flash(_("Removed email address from user account"),
960 h.flash(_("Removed email address from user account"),
961 category='success')
961 category='success')
962 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
962 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
963
963
964 @LoginRequired()
964 @LoginRequired()
965 @HasPermissionAllDecorator('hg.admin')
965 @HasPermissionAllDecorator('hg.admin')
966 @view_config(
966 @view_config(
967 route_name='edit_user_ips', request_method='GET',
967 route_name='edit_user_ips', request_method='GET',
968 renderer='rhodecode:templates/admin/users/user_edit.mako')
968 renderer='rhodecode:templates/admin/users/user_edit.mako')
969 def ips(self):
969 def ips(self):
970 _ = self.request.translate
970 _ = self.request.translate
971 c = self.load_default_context()
971 c = self.load_default_context()
972 c.user = self.db_user
972 c.user = self.db_user
973
973
974 c.active = 'ips'
974 c.active = 'ips'
975 c.user_ip_map = UserIpMap.query() \
975 c.user_ip_map = UserIpMap.query() \
976 .filter(UserIpMap.user == c.user).all()
976 .filter(UserIpMap.user == c.user).all()
977
977
978 c.inherit_default_ips = c.user.inherit_default_permissions
978 c.inherit_default_ips = c.user.inherit_default_permissions
979 c.default_user_ip_map = UserIpMap.query() \
979 c.default_user_ip_map = UserIpMap.query() \
980 .filter(UserIpMap.user == User.get_default_user()).all()
980 .filter(UserIpMap.user == User.get_default_user()).all()
981
981
982 return self._get_template_context(c)
982 return self._get_template_context(c)
983
983
984 @LoginRequired()
984 @LoginRequired()
985 @HasPermissionAllDecorator('hg.admin')
985 @HasPermissionAllDecorator('hg.admin')
986 @CSRFRequired()
986 @CSRFRequired()
987 @view_config(
987 @view_config(
988 route_name='edit_user_ips_add', request_method='POST')
988 route_name='edit_user_ips_add', request_method='POST')
989 # NOTE(marcink): this view is allowed for default users, as we can
989 # NOTE(marcink): this view is allowed for default users, as we can
990 # edit their IP white list
990 # edit their IP white list
991 def ips_add(self):
991 def ips_add(self):
992 _ = self.request.translate
992 _ = self.request.translate
993 c = self.load_default_context()
993 c = self.load_default_context()
994
994
995 user_id = self.db_user_id
995 user_id = self.db_user_id
996 c.user = self.db_user
996 c.user = self.db_user
997
997
998 user_model = UserModel()
998 user_model = UserModel()
999 desc = self.request.POST.get('description')
999 desc = self.request.POST.get('description')
1000 try:
1000 try:
1001 ip_list = user_model.parse_ip_range(
1001 ip_list = user_model.parse_ip_range(
1002 self.request.POST.get('new_ip'))
1002 self.request.POST.get('new_ip'))
1003 except Exception as e:
1003 except Exception as e:
1004 ip_list = []
1004 ip_list = []
1005 log.exception("Exception during ip saving")
1005 log.exception("Exception during ip saving")
1006 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1006 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1007 category='error')
1007 category='error')
1008 added = []
1008 added = []
1009 user_data = c.user.get_api_data()
1009 user_data = c.user.get_api_data()
1010 for ip in ip_list:
1010 for ip in ip_list:
1011 try:
1011 try:
1012 user_model.add_extra_ip(c.user.user_id, ip, desc)
1012 user_model.add_extra_ip(c.user.user_id, ip, desc)
1013 audit_logger.store_web(
1013 audit_logger.store_web(
1014 'user.edit.ip.add',
1014 'user.edit.ip.add',
1015 action_data={'ip': ip, 'user': user_data},
1015 action_data={'ip': ip, 'user': user_data},
1016 user=self._rhodecode_user)
1016 user=self._rhodecode_user)
1017 Session().commit()
1017 Session().commit()
1018 added.append(ip)
1018 added.append(ip)
1019 except formencode.Invalid as error:
1019 except formencode.Invalid as error:
1020 msg = error.error_dict['ip']
1020 msg = error.error_dict['ip']
1021 h.flash(msg, category='error')
1021 h.flash(msg, category='error')
1022 except Exception:
1022 except Exception:
1023 log.exception("Exception during ip saving")
1023 log.exception("Exception during ip saving")
1024 h.flash(_('An error occurred during ip saving'),
1024 h.flash(_('An error occurred during ip saving'),
1025 category='error')
1025 category='error')
1026 if added:
1026 if added:
1027 h.flash(
1027 h.flash(
1028 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1028 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1029 category='success')
1029 category='success')
1030 if 'default_user' in self.request.POST:
1030 if 'default_user' in self.request.POST:
1031 # case for editing global IP list we do it for 'DEFAULT' user
1031 # case for editing global IP list we do it for 'DEFAULT' user
1032 raise HTTPFound(h.route_path('admin_permissions_ips'))
1032 raise HTTPFound(h.route_path('admin_permissions_ips'))
1033 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1033 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1034
1034
1035 @LoginRequired()
1035 @LoginRequired()
1036 @HasPermissionAllDecorator('hg.admin')
1036 @HasPermissionAllDecorator('hg.admin')
1037 @CSRFRequired()
1037 @CSRFRequired()
1038 @view_config(
1038 @view_config(
1039 route_name='edit_user_ips_delete', request_method='POST')
1039 route_name='edit_user_ips_delete', request_method='POST')
1040 # NOTE(marcink): this view is allowed for default users, as we can
1040 # NOTE(marcink): this view is allowed for default users, as we can
1041 # edit their IP white list
1041 # edit their IP white list
1042 def ips_delete(self):
1042 def ips_delete(self):
1043 _ = self.request.translate
1043 _ = self.request.translate
1044 c = self.load_default_context()
1044 c = self.load_default_context()
1045
1045
1046 user_id = self.db_user_id
1046 user_id = self.db_user_id
1047 c.user = self.db_user
1047 c.user = self.db_user
1048
1048
1049 ip_id = self.request.POST.get('del_ip_id')
1049 ip_id = self.request.POST.get('del_ip_id')
1050 user_model = UserModel()
1050 user_model = UserModel()
1051 user_data = c.user.get_api_data()
1051 user_data = c.user.get_api_data()
1052 ip = UserIpMap.query().get(ip_id).ip_addr
1052 ip = UserIpMap.query().get(ip_id).ip_addr
1053 user_model.delete_extra_ip(c.user.user_id, ip_id)
1053 user_model.delete_extra_ip(c.user.user_id, ip_id)
1054 audit_logger.store_web(
1054 audit_logger.store_web(
1055 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1055 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1056 user=self._rhodecode_user)
1056 user=self._rhodecode_user)
1057 Session().commit()
1057 Session().commit()
1058 h.flash(_("Removed ip address from user whitelist"), category='success')
1058 h.flash(_("Removed ip address from user whitelist"), category='success')
1059
1059
1060 if 'default_user' in self.request.POST:
1060 if 'default_user' in self.request.POST:
1061 # case for editing global IP list we do it for 'DEFAULT' user
1061 # case for editing global IP list we do it for 'DEFAULT' user
1062 raise HTTPFound(h.route_path('admin_permissions_ips'))
1062 raise HTTPFound(h.route_path('admin_permissions_ips'))
1063 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1063 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1064
1064
1065 @LoginRequired()
1065 @LoginRequired()
1066 @HasPermissionAllDecorator('hg.admin')
1066 @HasPermissionAllDecorator('hg.admin')
1067 @view_config(
1067 @view_config(
1068 route_name='edit_user_groups_management', request_method='GET',
1068 route_name='edit_user_groups_management', request_method='GET',
1069 renderer='rhodecode:templates/admin/users/user_edit.mako')
1069 renderer='rhodecode:templates/admin/users/user_edit.mako')
1070 def groups_management(self):
1070 def groups_management(self):
1071 c = self.load_default_context()
1071 c = self.load_default_context()
1072 c.user = self.db_user
1072 c.user = self.db_user
1073 c.data = c.user.group_member
1073 c.data = c.user.group_member
1074
1074
1075 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1075 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1076 for group in c.user.group_member]
1076 for group in c.user.group_member]
1077 c.groups = json.dumps(groups)
1077 c.groups = json.dumps(groups)
1078 c.active = 'groups'
1078 c.active = 'groups'
1079
1079
1080 return self._get_template_context(c)
1080 return self._get_template_context(c)
1081
1081
1082 @LoginRequired()
1082 @LoginRequired()
1083 @HasPermissionAllDecorator('hg.admin')
1083 @HasPermissionAllDecorator('hg.admin')
1084 @CSRFRequired()
1084 @CSRFRequired()
1085 @view_config(
1085 @view_config(
1086 route_name='edit_user_groups_management_updates', request_method='POST')
1086 route_name='edit_user_groups_management_updates', request_method='POST')
1087 def groups_management_updates(self):
1087 def groups_management_updates(self):
1088 _ = self.request.translate
1088 _ = self.request.translate
1089 c = self.load_default_context()
1089 c = self.load_default_context()
1090
1090
1091 user_id = self.db_user_id
1091 user_id = self.db_user_id
1092 c.user = self.db_user
1092 c.user = self.db_user
1093
1093
1094 user_groups = set(self.request.POST.getall('users_group_id'))
1094 user_groups = set(self.request.POST.getall('users_group_id'))
1095 user_groups_objects = []
1095 user_groups_objects = []
1096
1096
1097 for ugid in user_groups:
1097 for ugid in user_groups:
1098 user_groups_objects.append(
1098 user_groups_objects.append(
1099 UserGroupModel().get_group(safe_int(ugid)))
1099 UserGroupModel().get_group(safe_int(ugid)))
1100 user_group_model = UserGroupModel()
1100 user_group_model = UserGroupModel()
1101 added_to_groups, removed_from_groups = \
1101 added_to_groups, removed_from_groups = \
1102 user_group_model.change_groups(c.user, user_groups_objects)
1102 user_group_model.change_groups(c.user, user_groups_objects)
1103
1103
1104 user_data = c.user.get_api_data()
1104 user_data = c.user.get_api_data()
1105 for user_group_id in added_to_groups:
1105 for user_group_id in added_to_groups:
1106 user_group = UserGroup.get(user_group_id)
1106 user_group = UserGroup.get(user_group_id)
1107 old_values = user_group.get_api_data()
1107 old_values = user_group.get_api_data()
1108 audit_logger.store_web(
1108 audit_logger.store_web(
1109 'user_group.edit.member.add',
1109 'user_group.edit.member.add',
1110 action_data={'user': user_data, 'old_data': old_values},
1110 action_data={'user': user_data, 'old_data': old_values},
1111 user=self._rhodecode_user)
1111 user=self._rhodecode_user)
1112
1112
1113 for user_group_id in removed_from_groups:
1113 for user_group_id in removed_from_groups:
1114 user_group = UserGroup.get(user_group_id)
1114 user_group = UserGroup.get(user_group_id)
1115 old_values = user_group.get_api_data()
1115 old_values = user_group.get_api_data()
1116 audit_logger.store_web(
1116 audit_logger.store_web(
1117 'user_group.edit.member.delete',
1117 'user_group.edit.member.delete',
1118 action_data={'user': user_data, 'old_data': old_values},
1118 action_data={'user': user_data, 'old_data': old_values},
1119 user=self._rhodecode_user)
1119 user=self._rhodecode_user)
1120
1120
1121 Session().commit()
1121 Session().commit()
1122 c.active = 'user_groups_management'
1122 c.active = 'user_groups_management'
1123 h.flash(_("Groups successfully changed"), category='success')
1123 h.flash(_("Groups successfully changed"), category='success')
1124
1124
1125 return HTTPFound(h.route_path(
1125 return HTTPFound(h.route_path(
1126 'edit_user_groups_management', user_id=user_id))
1126 'edit_user_groups_management', user_id=user_id))
1127
1127
1128 @LoginRequired()
1128 @LoginRequired()
1129 @HasPermissionAllDecorator('hg.admin')
1129 @HasPermissionAllDecorator('hg.admin')
1130 @view_config(
1130 @view_config(
1131 route_name='edit_user_audit_logs', request_method='GET',
1131 route_name='edit_user_audit_logs', request_method='GET',
1132 renderer='rhodecode:templates/admin/users/user_edit.mako')
1132 renderer='rhodecode:templates/admin/users/user_edit.mako')
1133 def user_audit_logs(self):
1133 def user_audit_logs(self):
1134 _ = self.request.translate
1134 _ = self.request.translate
1135 c = self.load_default_context()
1135 c = self.load_default_context()
1136 c.user = self.db_user
1136 c.user = self.db_user
1137
1137
1138 c.active = 'audit'
1138 c.active = 'audit'
1139
1139
1140 p = safe_int(self.request.GET.get('page', 1), 1)
1140 p = safe_int(self.request.GET.get('page', 1), 1)
1141
1141
1142 filter_term = self.request.GET.get('filter')
1142 filter_term = self.request.GET.get('filter')
1143 user_log = UserModel().get_user_log(c.user, filter_term)
1143 user_log = UserModel().get_user_log(c.user, filter_term)
1144
1144
1145 def url_generator(**kw):
1145 def url_generator(**kw):
1146 if filter_term:
1146 if filter_term:
1147 kw['filter'] = filter_term
1147 kw['filter'] = filter_term
1148 return self.request.current_route_path(_query=kw)
1148 return self.request.current_route_path(_query=kw)
1149
1149
1150 c.audit_logs = h.Page(
1150 c.audit_logs = h.Page(
1151 user_log, page=p, items_per_page=10, url=url_generator)
1151 user_log, page=p, items_per_page=10, url=url_generator)
1152 c.filter_term = filter_term
1152 c.filter_term = filter_term
1153 return self._get_template_context(c)
1153 return self._get_template_context(c)
1154
1154
1155 @LoginRequired()
1155 @LoginRequired()
1156 @HasPermissionAllDecorator('hg.admin')
1156 @HasPermissionAllDecorator('hg.admin')
1157 @view_config(
1157 @view_config(
1158 route_name='edit_user_perms_summary', request_method='GET',
1158 route_name='edit_user_perms_summary', request_method='GET',
1159 renderer='rhodecode:templates/admin/users/user_edit.mako')
1159 renderer='rhodecode:templates/admin/users/user_edit.mako')
1160 def user_perms_summary(self):
1160 def user_perms_summary(self):
1161 _ = self.request.translate
1161 _ = self.request.translate
1162 c = self.load_default_context()
1162 c = self.load_default_context()
1163 c.user = self.db_user
1163 c.user = self.db_user
1164
1164
1165 c.active = 'perms_summary'
1165 c.active = 'perms_summary'
1166 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1166 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1167
1167
1168 return self._get_template_context(c)
1168 return self._get_template_context(c)
1169
1169
1170 @LoginRequired()
1170 @LoginRequired()
1171 @HasPermissionAllDecorator('hg.admin')
1171 @HasPermissionAllDecorator('hg.admin')
1172 @view_config(
1172 @view_config(
1173 route_name='edit_user_perms_summary_json', request_method='GET',
1173 route_name='edit_user_perms_summary_json', request_method='GET',
1174 renderer='json_ext')
1174 renderer='json_ext')
1175 def user_perms_summary_json(self):
1175 def user_perms_summary_json(self):
1176 self.load_default_context()
1176 self.load_default_context()
1177 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1177 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1178
1178
1179 return perm_user.permissions
1179 return perm_user.permissions
@@ -1,413 +1,413 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2017 RhodeCode GmbH
3 # Copyright (C) 2013-2017 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 time
21 import time
22 import logging
22 import logging
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 from rhodecode.apps._base import BaseAppView
33 from rhodecode.apps._base import BaseAppView
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.utils2 import time_to_datetime
36 from rhodecode.lib.utils2 import time_to_datetime
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
39 from rhodecode.model.gist import GistModel
39 from rhodecode.model.gist import GistModel
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.db import Gist, User, or_
41 from rhodecode.model.db import Gist, User, or_
42 from rhodecode.model import validation_schema
42 from rhodecode.model import validation_schema
43 from rhodecode.model.validation_schema.schemas import gist_schema
43 from rhodecode.model.validation_schema.schemas import gist_schema
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class GistView(BaseAppView):
49 class GistView(BaseAppView):
50
50
51 def load_default_context(self):
51 def load_default_context(self):
52 _ = self.request.translate
52 _ = self.request.translate
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54 c.user = c.auth_user.get_instance()
54 c.user = c.auth_user.get_instance()
55
55
56 c.lifetime_values = [
56 c.lifetime_values = [
57 (-1, _('forever')),
57 (-1, _('forever')),
58 (5, _('5 minutes')),
58 (5, _('5 minutes')),
59 (60, _('1 hour')),
59 (60, _('1 hour')),
60 (60 * 24, _('1 day')),
60 (60 * 24, _('1 day')),
61 (60 * 24 * 30, _('1 month')),
61 (60 * 24 * 30, _('1 month')),
62 ]
62 ]
63
63
64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
65 c.acl_options = [
65 c.acl_options = [
66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
68 ]
68 ]
69
69
70 self._register_global_c(c)
70 self._register_global_c(c)
71 return c
71 return c
72
72
73 @LoginRequired()
73 @LoginRequired()
74 @view_config(
74 @view_config(
75 route_name='gists_show', request_method='GET',
75 route_name='gists_show', request_method='GET',
76 renderer='rhodecode:templates/admin/gists/index.mako')
76 renderer='rhodecode:templates/admin/gists/index.mako')
77 def gist_show_all(self):
77 def gist_show_all(self):
78 c = self.load_default_context()
78 c = self.load_default_context()
79
79
80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
81 c.show_private = self.request.GET.get('private') and not_default_user
81 c.show_private = self.request.GET.get('private') and not_default_user
82 c.show_public = self.request.GET.get('public') and not_default_user
82 c.show_public = self.request.GET.get('public') and not_default_user
83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
84
84
85 gists = _gists = Gist().query()\
85 gists = _gists = Gist().query()\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 .order_by(Gist.created_on.desc())
87 .order_by(Gist.created_on.desc())
88
88
89 c.active = 'public'
89 c.active = 'public'
90 # MY private
90 # MY private
91 if c.show_private and not c.show_public:
91 if c.show_private and not c.show_public:
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 c.active = 'my_private'
94 c.active = 'my_private'
95 # MY public
95 # MY public
96 elif c.show_public and not c.show_private:
96 elif c.show_public and not c.show_private:
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 c.active = 'my_public'
99 c.active = 'my_public'
100 # MY public+private
100 # MY public+private
101 elif c.show_private and c.show_public:
101 elif c.show_private and c.show_public:
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 Gist.gist_type == Gist.GIST_PRIVATE))\
103 Gist.gist_type == Gist.GIST_PRIVATE))\
104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
105 c.active = 'my_all'
105 c.active = 'my_all'
106 # Show all by super-admin
106 # Show all by super-admin
107 elif c.show_all:
107 elif c.show_all:
108 c.active = 'all'
108 c.active = 'all'
109 gists = _gists
109 gists = _gists
110
110
111 # default show ALL public gists
111 # default show ALL public gists
112 if not c.show_public and not c.show_private and not c.show_all:
112 if not c.show_public and not c.show_private and not c.show_all:
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 c.active = 'public'
114 c.active = 'public'
115
115
116 _render = self.request.get_partial_renderer(
116 _render = self.request.get_partial_renderer(
117 'data_table/_dt_elements.mako')
117 'rhodecode:templates/data_table/_dt_elements.mako')
118
118
119 data = []
119 data = []
120
120
121 for gist in gists:
121 for gist in gists:
122 data.append({
122 data.append({
123 'created_on': _render('gist_created', gist.created_on),
123 'created_on': _render('gist_created', gist.created_on),
124 'created_on_raw': gist.created_on,
124 'created_on_raw': gist.created_on,
125 'type': _render('gist_type', gist.gist_type),
125 'type': _render('gist_type', gist.gist_type),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 'author_raw': h.escape(gist.owner.full_contact),
128 'author_raw': h.escape(gist.owner.full_contact),
129 'expires': _render('gist_expires', gist.gist_expires),
129 'expires': _render('gist_expires', gist.gist_expires),
130 'description': _render('gist_description', gist.gist_description)
130 'description': _render('gist_description', gist.gist_description)
131 })
131 })
132 c.data = json.dumps(data)
132 c.data = json.dumps(data)
133
133
134 return self._get_template_context(c)
134 return self._get_template_context(c)
135
135
136 @LoginRequired()
136 @LoginRequired()
137 @NotAnonymous()
137 @NotAnonymous()
138 @view_config(
138 @view_config(
139 route_name='gists_new', request_method='GET',
139 route_name='gists_new', request_method='GET',
140 renderer='rhodecode:templates/admin/gists/new.mako')
140 renderer='rhodecode:templates/admin/gists/new.mako')
141 def gist_new(self):
141 def gist_new(self):
142 c = self.load_default_context()
142 c = self.load_default_context()
143 return self._get_template_context(c)
143 return self._get_template_context(c)
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @CSRFRequired()
147 @CSRFRequired()
148 @view_config(
148 @view_config(
149 route_name='gists_create', request_method='POST',
149 route_name='gists_create', request_method='POST',
150 renderer='rhodecode:templates/admin/gists/new.mako')
150 renderer='rhodecode:templates/admin/gists/new.mako')
151 def gist_create(self):
151 def gist_create(self):
152 _ = self.request.translate
152 _ = self.request.translate
153 c = self.load_default_context()
153 c = self.load_default_context()
154
154
155 data = dict(self.request.POST)
155 data = dict(self.request.POST)
156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
157 data['nodes'] = [{
157 data['nodes'] = [{
158 'filename': data['filename'],
158 'filename': data['filename'],
159 'content': data.get('content'),
159 'content': data.get('content'),
160 'mimetype': data.get('mimetype') # None is autodetect
160 'mimetype': data.get('mimetype') # None is autodetect
161 }]
161 }]
162
162
163 data['gist_type'] = (
163 data['gist_type'] = (
164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
165 data['gist_acl_level'] = (
165 data['gist_acl_level'] = (
166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
167
167
168 schema = gist_schema.GistSchema().bind(
168 schema = gist_schema.GistSchema().bind(
169 lifetime_options=[x[0] for x in c.lifetime_values])
169 lifetime_options=[x[0] for x in c.lifetime_values])
170
170
171 try:
171 try:
172
172
173 schema_data = schema.deserialize(data)
173 schema_data = schema.deserialize(data)
174 # convert to safer format with just KEYs so we sure no duplicates
174 # convert to safer format with just KEYs so we sure no duplicates
175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
176 schema_data['nodes'])
176 schema_data['nodes'])
177
177
178 gist = GistModel().create(
178 gist = GistModel().create(
179 gist_id=schema_data['gistid'], # custom access id not real ID
179 gist_id=schema_data['gistid'], # custom access id not real ID
180 description=schema_data['description'],
180 description=schema_data['description'],
181 owner=self._rhodecode_user.user_id,
181 owner=self._rhodecode_user.user_id,
182 gist_mapping=schema_data['nodes'],
182 gist_mapping=schema_data['nodes'],
183 gist_type=schema_data['gist_type'],
183 gist_type=schema_data['gist_type'],
184 lifetime=schema_data['lifetime'],
184 lifetime=schema_data['lifetime'],
185 gist_acl_level=schema_data['gist_acl_level']
185 gist_acl_level=schema_data['gist_acl_level']
186 )
186 )
187 Session().commit()
187 Session().commit()
188 new_gist_id = gist.gist_access_id
188 new_gist_id = gist.gist_access_id
189 except validation_schema.Invalid as errors:
189 except validation_schema.Invalid as errors:
190 defaults = data
190 defaults = data
191 errors = errors.asdict()
191 errors = errors.asdict()
192
192
193 if 'nodes.0.content' in errors:
193 if 'nodes.0.content' in errors:
194 errors['content'] = errors['nodes.0.content']
194 errors['content'] = errors['nodes.0.content']
195 del errors['nodes.0.content']
195 del errors['nodes.0.content']
196 if 'nodes.0.filename' in errors:
196 if 'nodes.0.filename' in errors:
197 errors['filename'] = errors['nodes.0.filename']
197 errors['filename'] = errors['nodes.0.filename']
198 del errors['nodes.0.filename']
198 del errors['nodes.0.filename']
199
199
200 data = render('rhodecode:templates/admin/gists/new.mako',
200 data = render('rhodecode:templates/admin/gists/new.mako',
201 self._get_template_context(c), self.request)
201 self._get_template_context(c), self.request)
202 html = formencode.htmlfill.render(
202 html = formencode.htmlfill.render(
203 data,
203 data,
204 defaults=defaults,
204 defaults=defaults,
205 errors=errors,
205 errors=errors,
206 prefix_error=False,
206 prefix_error=False,
207 encoding="UTF-8",
207 encoding="UTF-8",
208 force_defaults=False
208 force_defaults=False
209 )
209 )
210 return Response(html)
210 return Response(html)
211
211
212 except Exception:
212 except Exception:
213 log.exception("Exception while trying to create a gist")
213 log.exception("Exception while trying to create a gist")
214 h.flash(_('Error occurred during gist creation'), category='error')
214 h.flash(_('Error occurred during gist creation'), category='error')
215 raise HTTPFound(h.route_url('gists_new'))
215 raise HTTPFound(h.route_url('gists_new'))
216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
217
217
218 @LoginRequired()
218 @LoginRequired()
219 @NotAnonymous()
219 @NotAnonymous()
220 @CSRFRequired()
220 @CSRFRequired()
221 @view_config(
221 @view_config(
222 route_name='gist_delete', request_method='POST')
222 route_name='gist_delete', request_method='POST')
223 def gist_delete(self):
223 def gist_delete(self):
224 _ = self.request.translate
224 _ = self.request.translate
225 gist_id = self.request.matchdict['gist_id']
225 gist_id = self.request.matchdict['gist_id']
226
226
227 c = self.load_default_context()
227 c = self.load_default_context()
228 c.gist = Gist.get_or_404(gist_id)
228 c.gist = Gist.get_or_404(gist_id)
229
229
230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
231 if not (h.HasPermissionAny('hg.admin')() or owner):
231 if not (h.HasPermissionAny('hg.admin')() or owner):
232 log.warning('Deletion of Gist was forbidden '
232 log.warning('Deletion of Gist was forbidden '
233 'by unauthorized user: `%s`', self._rhodecode_user)
233 'by unauthorized user: `%s`', self._rhodecode_user)
234 raise HTTPNotFound()
234 raise HTTPNotFound()
235
235
236 GistModel().delete(c.gist)
236 GistModel().delete(c.gist)
237 Session().commit()
237 Session().commit()
238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
239
239
240 raise HTTPFound(h.route_url('gists_show'))
240 raise HTTPFound(h.route_url('gists_show'))
241
241
242 def _get_gist(self, gist_id):
242 def _get_gist(self, gist_id):
243
243
244 gist = Gist.get_or_404(gist_id)
244 gist = Gist.get_or_404(gist_id)
245
245
246 # Check if this gist is expired
246 # Check if this gist is expired
247 if gist.gist_expires != -1:
247 if gist.gist_expires != -1:
248 if time.time() > gist.gist_expires:
248 if time.time() > gist.gist_expires:
249 log.error(
249 log.error(
250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
251 raise HTTPNotFound()
251 raise HTTPNotFound()
252
252
253 # check if this gist requires a login
253 # check if this gist requires a login
254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
256 log.error("Anonymous user %s tried to access protected gist `%s`",
256 log.error("Anonymous user %s tried to access protected gist `%s`",
257 self._rhodecode_user, gist_id)
257 self._rhodecode_user, gist_id)
258 raise HTTPNotFound()
258 raise HTTPNotFound()
259 return gist
259 return gist
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @view_config(
262 @view_config(
263 route_name='gist_show', request_method='GET',
263 route_name='gist_show', request_method='GET',
264 renderer='rhodecode:templates/admin/gists/show.mako')
264 renderer='rhodecode:templates/admin/gists/show.mako')
265 @view_config(
265 @view_config(
266 route_name='gist_show_rev', request_method='GET',
266 route_name='gist_show_rev', request_method='GET',
267 renderer='rhodecode:templates/admin/gists/show.mako')
267 renderer='rhodecode:templates/admin/gists/show.mako')
268 @view_config(
268 @view_config(
269 route_name='gist_show_formatted', request_method='GET',
269 route_name='gist_show_formatted', request_method='GET',
270 renderer=None)
270 renderer=None)
271 @view_config(
271 @view_config(
272 route_name='gist_show_formatted_path', request_method='GET',
272 route_name='gist_show_formatted_path', request_method='GET',
273 renderer=None)
273 renderer=None)
274 def gist_show(self):
274 def gist_show(self):
275 gist_id = self.request.matchdict['gist_id']
275 gist_id = self.request.matchdict['gist_id']
276
276
277 # TODO(marcink): expose those via matching dict
277 # TODO(marcink): expose those via matching dict
278 revision = self.request.matchdict.get('revision', 'tip')
278 revision = self.request.matchdict.get('revision', 'tip')
279 f_path = self.request.matchdict.get('f_path', None)
279 f_path = self.request.matchdict.get('f_path', None)
280 return_format = self.request.matchdict.get('format')
280 return_format = self.request.matchdict.get('format')
281
281
282 c = self.load_default_context()
282 c = self.load_default_context()
283 c.gist = self._get_gist(gist_id)
283 c.gist = self._get_gist(gist_id)
284 c.render = not self.request.GET.get('no-render', False)
284 c.render = not self.request.GET.get('no-render', False)
285
285
286 try:
286 try:
287 c.file_last_commit, c.files = GistModel().get_gist_files(
287 c.file_last_commit, c.files = GistModel().get_gist_files(
288 gist_id, revision=revision)
288 gist_id, revision=revision)
289 except VCSError:
289 except VCSError:
290 log.exception("Exception in gist show")
290 log.exception("Exception in gist show")
291 raise HTTPNotFound()
291 raise HTTPNotFound()
292
292
293 if return_format == 'raw':
293 if return_format == 'raw':
294 content = '\n\n'.join([f.content for f in c.files
294 content = '\n\n'.join([f.content for f in c.files
295 if (f_path is None or f.path == f_path)])
295 if (f_path is None or f.path == f_path)])
296 response = Response(content)
296 response = Response(content)
297 response.content_type = 'text/plain'
297 response.content_type = 'text/plain'
298 return response
298 return response
299
299
300 return self._get_template_context(c)
300 return self._get_template_context(c)
301
301
302 @LoginRequired()
302 @LoginRequired()
303 @NotAnonymous()
303 @NotAnonymous()
304 @view_config(
304 @view_config(
305 route_name='gist_edit', request_method='GET',
305 route_name='gist_edit', request_method='GET',
306 renderer='rhodecode:templates/admin/gists/edit.mako')
306 renderer='rhodecode:templates/admin/gists/edit.mako')
307 def gist_edit(self):
307 def gist_edit(self):
308 _ = self.request.translate
308 _ = self.request.translate
309 gist_id = self.request.matchdict['gist_id']
309 gist_id = self.request.matchdict['gist_id']
310 c = self.load_default_context()
310 c = self.load_default_context()
311 c.gist = self._get_gist(gist_id)
311 c.gist = self._get_gist(gist_id)
312
312
313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
314 if not (h.HasPermissionAny('hg.admin')() or owner):
314 if not (h.HasPermissionAny('hg.admin')() or owner):
315 raise HTTPNotFound()
315 raise HTTPNotFound()
316
316
317 try:
317 try:
318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
319 except VCSError:
319 except VCSError:
320 log.exception("Exception in gist edit")
320 log.exception("Exception in gist edit")
321 raise HTTPNotFound()
321 raise HTTPNotFound()
322
322
323 if c.gist.gist_expires == -1:
323 if c.gist.gist_expires == -1:
324 expiry = _('never')
324 expiry = _('never')
325 else:
325 else:
326 # this cannot use timeago, since it's used in select2 as a value
326 # this cannot use timeago, since it's used in select2 as a value
327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
328
328
329 c.lifetime_values.append(
329 c.lifetime_values.append(
330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
331 )
331 )
332
332
333 return self._get_template_context(c)
333 return self._get_template_context(c)
334
334
335 @LoginRequired()
335 @LoginRequired()
336 @NotAnonymous()
336 @NotAnonymous()
337 @CSRFRequired()
337 @CSRFRequired()
338 @view_config(
338 @view_config(
339 route_name='gist_update', request_method='POST',
339 route_name='gist_update', request_method='POST',
340 renderer='rhodecode:templates/admin/gists/edit.mako')
340 renderer='rhodecode:templates/admin/gists/edit.mako')
341 def gist_update(self):
341 def gist_update(self):
342 _ = self.request.translate
342 _ = self.request.translate
343 gist_id = self.request.matchdict['gist_id']
343 gist_id = self.request.matchdict['gist_id']
344 c = self.load_default_context()
344 c = self.load_default_context()
345 c.gist = self._get_gist(gist_id)
345 c.gist = self._get_gist(gist_id)
346
346
347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
348 if not (h.HasPermissionAny('hg.admin')() or owner):
348 if not (h.HasPermissionAny('hg.admin')() or owner):
349 raise HTTPNotFound()
349 raise HTTPNotFound()
350
350
351 data = peppercorn.parse(self.request.POST.items())
351 data = peppercorn.parse(self.request.POST.items())
352
352
353 schema = gist_schema.GistSchema()
353 schema = gist_schema.GistSchema()
354 schema = schema.bind(
354 schema = schema.bind(
355 # '0' is special value to leave lifetime untouched
355 # '0' is special value to leave lifetime untouched
356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
357 )
357 )
358
358
359 try:
359 try:
360 schema_data = schema.deserialize(data)
360 schema_data = schema.deserialize(data)
361 # convert to safer format with just KEYs so we sure no duplicates
361 # convert to safer format with just KEYs so we sure no duplicates
362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
363 schema_data['nodes'])
363 schema_data['nodes'])
364
364
365 GistModel().update(
365 GistModel().update(
366 gist=c.gist,
366 gist=c.gist,
367 description=schema_data['description'],
367 description=schema_data['description'],
368 owner=c.gist.owner,
368 owner=c.gist.owner,
369 gist_mapping=schema_data['nodes'],
369 gist_mapping=schema_data['nodes'],
370 lifetime=schema_data['lifetime'],
370 lifetime=schema_data['lifetime'],
371 gist_acl_level=schema_data['gist_acl_level']
371 gist_acl_level=schema_data['gist_acl_level']
372 )
372 )
373
373
374 Session().commit()
374 Session().commit()
375 h.flash(_('Successfully updated gist content'), category='success')
375 h.flash(_('Successfully updated gist content'), category='success')
376 except NodeNotChangedError:
376 except NodeNotChangedError:
377 # raised if nothing was changed in repo itself. We anyway then
377 # raised if nothing was changed in repo itself. We anyway then
378 # store only DB stuff for gist
378 # store only DB stuff for gist
379 Session().commit()
379 Session().commit()
380 h.flash(_('Successfully updated gist data'), category='success')
380 h.flash(_('Successfully updated gist data'), category='success')
381 except validation_schema.Invalid as errors:
381 except validation_schema.Invalid as errors:
382 errors = h.escape(errors.asdict())
382 errors = h.escape(errors.asdict())
383 h.flash(_('Error occurred during update of gist {}: {}').format(
383 h.flash(_('Error occurred during update of gist {}: {}').format(
384 gist_id, errors), category='error')
384 gist_id, errors), category='error')
385 except Exception:
385 except Exception:
386 log.exception("Exception in gist edit")
386 log.exception("Exception in gist edit")
387 h.flash(_('Error occurred during update of gist %s') % gist_id,
387 h.flash(_('Error occurred during update of gist %s') % gist_id,
388 category='error')
388 category='error')
389
389
390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
391
391
392 @LoginRequired()
392 @LoginRequired()
393 @NotAnonymous()
393 @NotAnonymous()
394 @view_config(
394 @view_config(
395 route_name='gist_edit_check_revision', request_method='GET',
395 route_name='gist_edit_check_revision', request_method='GET',
396 renderer='json_ext')
396 renderer='json_ext')
397 def gist_edit_check_revision(self):
397 def gist_edit_check_revision(self):
398 _ = self.request.translate
398 _ = self.request.translate
399 gist_id = self.request.matchdict['gist_id']
399 gist_id = self.request.matchdict['gist_id']
400 c = self.load_default_context()
400 c = self.load_default_context()
401 c.gist = self._get_gist(gist_id)
401 c.gist = self._get_gist(gist_id)
402
402
403 last_rev = c.gist.scm_instance().get_commit()
403 last_rev = c.gist.scm_instance().get_commit()
404 success = True
404 success = True
405 revision = self.request.GET.get('revision')
405 revision = self.request.GET.get('revision')
406
406
407 if revision != last_rev.raw_id:
407 if revision != last_rev.raw_id:
408 log.error('Last revision %s is different then submitted %s'
408 log.error('Last revision %s is different then submitted %s'
409 % (revision, last_rev))
409 % (revision, last_rev))
410 # our gist has newer version than we
410 # our gist has newer version than we
411 success = False
411 success = False
412
412
413 return {'success': success}
413 return {'success': success}
@@ -1,580 +1,580 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
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 LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
37 from rhodecode.lib.channelstream import (
37 from rhodecode.lib.channelstream import (
38 channelstream_request, ChannelstreamException)
38 channelstream_request, ChannelstreamException)
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.db import (
42 from rhodecode.model.db import (
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 PullRequest)
44 PullRequest)
45 from rhodecode.model.forms import UserForm
45 from rhodecode.model.forms import UserForm
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.scm import RepoList
48 from rhodecode.model.scm import RepoList
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.validation_schema.schemas import user_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class MyAccountView(BaseAppView, DataGridAppView):
56 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
57 ALLOW_SCOPED_TOKENS = False
58 """
58 """
59 This view has alternative version inside EE, if modified please take a look
59 This view has alternative version inside EE, if modified please take a look
60 in there as well.
60 in there as well.
61 """
61 """
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
65 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 self._register_global_c(c)
67 self._register_global_c(c)
68 return c
68 return c
69
69
70 @LoginRequired()
70 @LoginRequired()
71 @NotAnonymous()
71 @NotAnonymous()
72 @view_config(
72 @view_config(
73 route_name='my_account_profile', request_method='GET',
73 route_name='my_account_profile', request_method='GET',
74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 def my_account_profile(self):
75 def my_account_profile(self):
76 c = self.load_default_context()
76 c = self.load_default_context()
77 c.active = 'profile'
77 c.active = 'profile'
78 return self._get_template_context(c)
78 return self._get_template_context(c)
79
79
80 @LoginRequired()
80 @LoginRequired()
81 @NotAnonymous()
81 @NotAnonymous()
82 @view_config(
82 @view_config(
83 route_name='my_account_password', request_method='GET',
83 route_name='my_account_password', request_method='GET',
84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 def my_account_password(self):
85 def my_account_password(self):
86 c = self.load_default_context()
86 c = self.load_default_context()
87 c.active = 'password'
87 c.active = 'password'
88 c.extern_type = c.user.extern_type
88 c.extern_type = c.user.extern_type
89
89
90 schema = user_schema.ChangePasswordSchema().bind(
90 schema = user_schema.ChangePasswordSchema().bind(
91 username=c.user.username)
91 username=c.user.username)
92
92
93 form = forms.Form(
93 form = forms.Form(
94 schema,
94 schema,
95 action=h.route_path('my_account_password_update'),
95 action=h.route_path('my_account_password_update'),
96 buttons=(forms.buttons.save, forms.buttons.reset))
96 buttons=(forms.buttons.save, forms.buttons.reset))
97
97
98 c.form = form
98 c.form = form
99 return self._get_template_context(c)
99 return self._get_template_context(c)
100
100
101 @LoginRequired()
101 @LoginRequired()
102 @NotAnonymous()
102 @NotAnonymous()
103 @CSRFRequired()
103 @CSRFRequired()
104 @view_config(
104 @view_config(
105 route_name='my_account_password_update', request_method='POST',
105 route_name='my_account_password_update', request_method='POST',
106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
107 def my_account_password_update(self):
107 def my_account_password_update(self):
108 _ = self.request.translate
108 _ = self.request.translate
109 c = self.load_default_context()
109 c = self.load_default_context()
110 c.active = 'password'
110 c.active = 'password'
111 c.extern_type = c.user.extern_type
111 c.extern_type = c.user.extern_type
112
112
113 schema = user_schema.ChangePasswordSchema().bind(
113 schema = user_schema.ChangePasswordSchema().bind(
114 username=c.user.username)
114 username=c.user.username)
115
115
116 form = forms.Form(
116 form = forms.Form(
117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
118
118
119 if c.extern_type != 'rhodecode':
119 if c.extern_type != 'rhodecode':
120 raise HTTPFound(self.request.route_path('my_account_password'))
120 raise HTTPFound(self.request.route_path('my_account_password'))
121
121
122 controls = self.request.POST.items()
122 controls = self.request.POST.items()
123 try:
123 try:
124 valid_data = form.validate(controls)
124 valid_data = form.validate(controls)
125 UserModel().update_user(c.user.user_id, **valid_data)
125 UserModel().update_user(c.user.user_id, **valid_data)
126 c.user.update_userdata(force_password_change=False)
126 c.user.update_userdata(force_password_change=False)
127 Session().commit()
127 Session().commit()
128 except forms.ValidationFailure as e:
128 except forms.ValidationFailure as e:
129 c.form = e
129 c.form = e
130 return self._get_template_context(c)
130 return self._get_template_context(c)
131
131
132 except Exception:
132 except Exception:
133 log.exception("Exception updating password")
133 log.exception("Exception updating password")
134 h.flash(_('Error occurred during update of user password'),
134 h.flash(_('Error occurred during update of user password'),
135 category='error')
135 category='error')
136 else:
136 else:
137 instance = c.auth_user.get_instance()
137 instance = c.auth_user.get_instance()
138 self.session.setdefault('rhodecode_user', {}).update(
138 self.session.setdefault('rhodecode_user', {}).update(
139 {'password': md5(instance.password)})
139 {'password': md5(instance.password)})
140 self.session.save()
140 self.session.save()
141 h.flash(_("Successfully updated password"), category='success')
141 h.flash(_("Successfully updated password"), category='success')
142
142
143 raise HTTPFound(self.request.route_path('my_account_password'))
143 raise HTTPFound(self.request.route_path('my_account_password'))
144
144
145 @LoginRequired()
145 @LoginRequired()
146 @NotAnonymous()
146 @NotAnonymous()
147 @view_config(
147 @view_config(
148 route_name='my_account_auth_tokens', request_method='GET',
148 route_name='my_account_auth_tokens', request_method='GET',
149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
150 def my_account_auth_tokens(self):
150 def my_account_auth_tokens(self):
151 _ = self.request.translate
151 _ = self.request.translate
152
152
153 c = self.load_default_context()
153 c = self.load_default_context()
154 c.active = 'auth_tokens'
154 c.active = 'auth_tokens'
155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
156 c.role_values = [
156 c.role_values = [
157 (x, AuthTokenModel.cls._get_role_name(x))
157 (x, AuthTokenModel.cls._get_role_name(x))
158 for x in AuthTokenModel.cls.ROLES]
158 for x in AuthTokenModel.cls.ROLES]
159 c.role_options = [(c.role_values, _("Role"))]
159 c.role_options = [(c.role_values, _("Role"))]
160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
161 c.user.user_id, show_expired=True)
161 c.user.user_id, show_expired=True)
162 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
162 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
163 return self._get_template_context(c)
163 return self._get_template_context(c)
164
164
165 def maybe_attach_token_scope(self, token):
165 def maybe_attach_token_scope(self, token):
166 # implemented in EE edition
166 # implemented in EE edition
167 pass
167 pass
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @NotAnonymous()
170 @NotAnonymous()
171 @CSRFRequired()
171 @CSRFRequired()
172 @view_config(
172 @view_config(
173 route_name='my_account_auth_tokens_add', request_method='POST',)
173 route_name='my_account_auth_tokens_add', request_method='POST',)
174 def my_account_auth_tokens_add(self):
174 def my_account_auth_tokens_add(self):
175 _ = self.request.translate
175 _ = self.request.translate
176 c = self.load_default_context()
176 c = self.load_default_context()
177
177
178 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
178 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
179 description = self.request.POST.get('description')
179 description = self.request.POST.get('description')
180 role = self.request.POST.get('role')
180 role = self.request.POST.get('role')
181
181
182 token = AuthTokenModel().create(
182 token = AuthTokenModel().create(
183 c.user.user_id, description, lifetime, role)
183 c.user.user_id, description, lifetime, role)
184 token_data = token.get_api_data()
184 token_data = token.get_api_data()
185
185
186 self.maybe_attach_token_scope(token)
186 self.maybe_attach_token_scope(token)
187 audit_logger.store_web(
187 audit_logger.store_web(
188 'user.edit.token.add', action_data={
188 'user.edit.token.add', action_data={
189 'data': {'token': token_data, 'user': 'self'}},
189 'data': {'token': token_data, 'user': 'self'}},
190 user=self._rhodecode_user, )
190 user=self._rhodecode_user, )
191 Session().commit()
191 Session().commit()
192
192
193 h.flash(_("Auth token successfully created"), category='success')
193 h.flash(_("Auth token successfully created"), category='success')
194 return HTTPFound(h.route_path('my_account_auth_tokens'))
194 return HTTPFound(h.route_path('my_account_auth_tokens'))
195
195
196 @LoginRequired()
196 @LoginRequired()
197 @NotAnonymous()
197 @NotAnonymous()
198 @CSRFRequired()
198 @CSRFRequired()
199 @view_config(
199 @view_config(
200 route_name='my_account_auth_tokens_delete', request_method='POST')
200 route_name='my_account_auth_tokens_delete', request_method='POST')
201 def my_account_auth_tokens_delete(self):
201 def my_account_auth_tokens_delete(self):
202 _ = self.request.translate
202 _ = self.request.translate
203 c = self.load_default_context()
203 c = self.load_default_context()
204
204
205 del_auth_token = self.request.POST.get('del_auth_token')
205 del_auth_token = self.request.POST.get('del_auth_token')
206
206
207 if del_auth_token:
207 if del_auth_token:
208 token = UserApiKeys.get_or_404(del_auth_token)
208 token = UserApiKeys.get_or_404(del_auth_token)
209 token_data = token.get_api_data()
209 token_data = token.get_api_data()
210
210
211 AuthTokenModel().delete(del_auth_token, c.user.user_id)
211 AuthTokenModel().delete(del_auth_token, c.user.user_id)
212 audit_logger.store_web(
212 audit_logger.store_web(
213 'user.edit.token.delete', action_data={
213 'user.edit.token.delete', action_data={
214 'data': {'token': token_data, 'user': 'self'}},
214 'data': {'token': token_data, 'user': 'self'}},
215 user=self._rhodecode_user,)
215 user=self._rhodecode_user,)
216 Session().commit()
216 Session().commit()
217 h.flash(_("Auth token successfully deleted"), category='success')
217 h.flash(_("Auth token successfully deleted"), category='success')
218
218
219 return HTTPFound(h.route_path('my_account_auth_tokens'))
219 return HTTPFound(h.route_path('my_account_auth_tokens'))
220
220
221 @LoginRequired()
221 @LoginRequired()
222 @NotAnonymous()
222 @NotAnonymous()
223 @view_config(
223 @view_config(
224 route_name='my_account_emails', request_method='GET',
224 route_name='my_account_emails', request_method='GET',
225 renderer='rhodecode:templates/admin/my_account/my_account.mako')
225 renderer='rhodecode:templates/admin/my_account/my_account.mako')
226 def my_account_emails(self):
226 def my_account_emails(self):
227 _ = self.request.translate
227 _ = self.request.translate
228
228
229 c = self.load_default_context()
229 c = self.load_default_context()
230 c.active = 'emails'
230 c.active = 'emails'
231
231
232 c.user_email_map = UserEmailMap.query()\
232 c.user_email_map = UserEmailMap.query()\
233 .filter(UserEmailMap.user == c.user).all()
233 .filter(UserEmailMap.user == c.user).all()
234 return self._get_template_context(c)
234 return self._get_template_context(c)
235
235
236 @LoginRequired()
236 @LoginRequired()
237 @NotAnonymous()
237 @NotAnonymous()
238 @CSRFRequired()
238 @CSRFRequired()
239 @view_config(
239 @view_config(
240 route_name='my_account_emails_add', request_method='POST')
240 route_name='my_account_emails_add', request_method='POST')
241 def my_account_emails_add(self):
241 def my_account_emails_add(self):
242 _ = self.request.translate
242 _ = self.request.translate
243 c = self.load_default_context()
243 c = self.load_default_context()
244
244
245 email = self.request.POST.get('new_email')
245 email = self.request.POST.get('new_email')
246
246
247 try:
247 try:
248 UserModel().add_extra_email(c.user.user_id, email)
248 UserModel().add_extra_email(c.user.user_id, email)
249 audit_logger.store_web(
249 audit_logger.store_web(
250 'user.edit.email.add', action_data={
250 'user.edit.email.add', action_data={
251 'data': {'email': email, 'user': 'self'}},
251 'data': {'email': email, 'user': 'self'}},
252 user=self._rhodecode_user,)
252 user=self._rhodecode_user,)
253
253
254 Session().commit()
254 Session().commit()
255 h.flash(_("Added new email address `%s` for user account") % email,
255 h.flash(_("Added new email address `%s` for user account") % email,
256 category='success')
256 category='success')
257 except formencode.Invalid as error:
257 except formencode.Invalid as error:
258 h.flash(h.escape(error.error_dict['email']), category='error')
258 h.flash(h.escape(error.error_dict['email']), category='error')
259 except Exception:
259 except Exception:
260 log.exception("Exception in my_account_emails")
260 log.exception("Exception in my_account_emails")
261 h.flash(_('An error occurred during email saving'),
261 h.flash(_('An error occurred during email saving'),
262 category='error')
262 category='error')
263 return HTTPFound(h.route_path('my_account_emails'))
263 return HTTPFound(h.route_path('my_account_emails'))
264
264
265 @LoginRequired()
265 @LoginRequired()
266 @NotAnonymous()
266 @NotAnonymous()
267 @CSRFRequired()
267 @CSRFRequired()
268 @view_config(
268 @view_config(
269 route_name='my_account_emails_delete', request_method='POST')
269 route_name='my_account_emails_delete', request_method='POST')
270 def my_account_emails_delete(self):
270 def my_account_emails_delete(self):
271 _ = self.request.translate
271 _ = self.request.translate
272 c = self.load_default_context()
272 c = self.load_default_context()
273
273
274 del_email_id = self.request.POST.get('del_email_id')
274 del_email_id = self.request.POST.get('del_email_id')
275 if del_email_id:
275 if del_email_id:
276 email = UserEmailMap.get_or_404(del_email_id).email
276 email = UserEmailMap.get_or_404(del_email_id).email
277 UserModel().delete_extra_email(c.user.user_id, del_email_id)
277 UserModel().delete_extra_email(c.user.user_id, del_email_id)
278 audit_logger.store_web(
278 audit_logger.store_web(
279 'user.edit.email.delete', action_data={
279 'user.edit.email.delete', action_data={
280 'data': {'email': email, 'user': 'self'}},
280 'data': {'email': email, 'user': 'self'}},
281 user=self._rhodecode_user,)
281 user=self._rhodecode_user,)
282 Session().commit()
282 Session().commit()
283 h.flash(_("Email successfully deleted"),
283 h.flash(_("Email successfully deleted"),
284 category='success')
284 category='success')
285 return HTTPFound(h.route_path('my_account_emails'))
285 return HTTPFound(h.route_path('my_account_emails'))
286
286
287 @LoginRequired()
287 @LoginRequired()
288 @NotAnonymous()
288 @NotAnonymous()
289 @CSRFRequired()
289 @CSRFRequired()
290 @view_config(
290 @view_config(
291 route_name='my_account_notifications_test_channelstream',
291 route_name='my_account_notifications_test_channelstream',
292 request_method='POST', renderer='json_ext')
292 request_method='POST', renderer='json_ext')
293 def my_account_notifications_test_channelstream(self):
293 def my_account_notifications_test_channelstream(self):
294 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
294 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
295 self._rhodecode_user.username, datetime.datetime.now())
295 self._rhodecode_user.username, datetime.datetime.now())
296 payload = {
296 payload = {
297 # 'channel': 'broadcast',
297 # 'channel': 'broadcast',
298 'type': 'message',
298 'type': 'message',
299 'timestamp': datetime.datetime.utcnow(),
299 'timestamp': datetime.datetime.utcnow(),
300 'user': 'system',
300 'user': 'system',
301 'pm_users': [self._rhodecode_user.username],
301 'pm_users': [self._rhodecode_user.username],
302 'message': {
302 'message': {
303 'message': message,
303 'message': message,
304 'level': 'info',
304 'level': 'info',
305 'topic': '/notifications'
305 'topic': '/notifications'
306 }
306 }
307 }
307 }
308
308
309 registry = self.request.registry
309 registry = self.request.registry
310 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
310 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
311 channelstream_config = rhodecode_plugins.get('channelstream', {})
311 channelstream_config = rhodecode_plugins.get('channelstream', {})
312
312
313 try:
313 try:
314 channelstream_request(channelstream_config, [payload], '/message')
314 channelstream_request(channelstream_config, [payload], '/message')
315 except ChannelstreamException as e:
315 except ChannelstreamException as e:
316 log.exception('Failed to send channelstream data')
316 log.exception('Failed to send channelstream data')
317 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
317 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
318 return {"response": 'Channelstream data sent. '
318 return {"response": 'Channelstream data sent. '
319 'You should see a new live message now.'}
319 'You should see a new live message now.'}
320
320
321 def _load_my_repos_data(self, watched=False):
321 def _load_my_repos_data(self, watched=False):
322 if watched:
322 if watched:
323 admin = False
323 admin = False
324 follows_repos = Session().query(UserFollowing)\
324 follows_repos = Session().query(UserFollowing)\
325 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
325 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
326 .options(joinedload(UserFollowing.follows_repository))\
326 .options(joinedload(UserFollowing.follows_repository))\
327 .all()
327 .all()
328 repo_list = [x.follows_repository for x in follows_repos]
328 repo_list = [x.follows_repository for x in follows_repos]
329 else:
329 else:
330 admin = True
330 admin = True
331 repo_list = Repository.get_all_repos(
331 repo_list = Repository.get_all_repos(
332 user_id=self._rhodecode_user.user_id)
332 user_id=self._rhodecode_user.user_id)
333 repo_list = RepoList(repo_list, perm_set=[
333 repo_list = RepoList(repo_list, perm_set=[
334 'repository.read', 'repository.write', 'repository.admin'])
334 'repository.read', 'repository.write', 'repository.admin'])
335
335
336 repos_data = RepoModel().get_repos_as_dict(
336 repos_data = RepoModel().get_repos_as_dict(
337 repo_list=repo_list, admin=admin)
337 repo_list=repo_list, admin=admin)
338 # json used to render the grid
338 # json used to render the grid
339 return json.dumps(repos_data)
339 return json.dumps(repos_data)
340
340
341 @LoginRequired()
341 @LoginRequired()
342 @NotAnonymous()
342 @NotAnonymous()
343 @view_config(
343 @view_config(
344 route_name='my_account_repos', request_method='GET',
344 route_name='my_account_repos', request_method='GET',
345 renderer='rhodecode:templates/admin/my_account/my_account.mako')
345 renderer='rhodecode:templates/admin/my_account/my_account.mako')
346 def my_account_repos(self):
346 def my_account_repos(self):
347 c = self.load_default_context()
347 c = self.load_default_context()
348 c.active = 'repos'
348 c.active = 'repos'
349
349
350 # json used to render the grid
350 # json used to render the grid
351 c.data = self._load_my_repos_data()
351 c.data = self._load_my_repos_data()
352 return self._get_template_context(c)
352 return self._get_template_context(c)
353
353
354 @LoginRequired()
354 @LoginRequired()
355 @NotAnonymous()
355 @NotAnonymous()
356 @view_config(
356 @view_config(
357 route_name='my_account_watched', request_method='GET',
357 route_name='my_account_watched', request_method='GET',
358 renderer='rhodecode:templates/admin/my_account/my_account.mako')
358 renderer='rhodecode:templates/admin/my_account/my_account.mako')
359 def my_account_watched(self):
359 def my_account_watched(self):
360 c = self.load_default_context()
360 c = self.load_default_context()
361 c.active = 'watched'
361 c.active = 'watched'
362
362
363 # json used to render the grid
363 # json used to render the grid
364 c.data = self._load_my_repos_data(watched=True)
364 c.data = self._load_my_repos_data(watched=True)
365 return self._get_template_context(c)
365 return self._get_template_context(c)
366
366
367 @LoginRequired()
367 @LoginRequired()
368 @NotAnonymous()
368 @NotAnonymous()
369 @view_config(
369 @view_config(
370 route_name='my_account_perms', request_method='GET',
370 route_name='my_account_perms', request_method='GET',
371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 def my_account_perms(self):
372 def my_account_perms(self):
373 c = self.load_default_context()
373 c = self.load_default_context()
374 c.active = 'perms'
374 c.active = 'perms'
375
375
376 c.perm_user = c.auth_user
376 c.perm_user = c.auth_user
377 return self._get_template_context(c)
377 return self._get_template_context(c)
378
378
379 @LoginRequired()
379 @LoginRequired()
380 @NotAnonymous()
380 @NotAnonymous()
381 @view_config(
381 @view_config(
382 route_name='my_account_notifications', request_method='GET',
382 route_name='my_account_notifications', request_method='GET',
383 renderer='rhodecode:templates/admin/my_account/my_account.mako')
383 renderer='rhodecode:templates/admin/my_account/my_account.mako')
384 def my_notifications(self):
384 def my_notifications(self):
385 c = self.load_default_context()
385 c = self.load_default_context()
386 c.active = 'notifications'
386 c.active = 'notifications'
387
387
388 return self._get_template_context(c)
388 return self._get_template_context(c)
389
389
390 @LoginRequired()
390 @LoginRequired()
391 @NotAnonymous()
391 @NotAnonymous()
392 @CSRFRequired()
392 @CSRFRequired()
393 @view_config(
393 @view_config(
394 route_name='my_account_notifications_toggle_visibility',
394 route_name='my_account_notifications_toggle_visibility',
395 request_method='POST', renderer='json_ext')
395 request_method='POST', renderer='json_ext')
396 def my_notifications_toggle_visibility(self):
396 def my_notifications_toggle_visibility(self):
397 user = self._rhodecode_db_user
397 user = self._rhodecode_db_user
398 new_status = not user.user_data.get('notification_status', True)
398 new_status = not user.user_data.get('notification_status', True)
399 user.update_userdata(notification_status=new_status)
399 user.update_userdata(notification_status=new_status)
400 Session().commit()
400 Session().commit()
401 return user.user_data['notification_status']
401 return user.user_data['notification_status']
402
402
403 @LoginRequired()
403 @LoginRequired()
404 @NotAnonymous()
404 @NotAnonymous()
405 @view_config(
405 @view_config(
406 route_name='my_account_edit',
406 route_name='my_account_edit',
407 request_method='GET',
407 request_method='GET',
408 renderer='rhodecode:templates/admin/my_account/my_account.mako')
408 renderer='rhodecode:templates/admin/my_account/my_account.mako')
409 def my_account_edit(self):
409 def my_account_edit(self):
410 c = self.load_default_context()
410 c = self.load_default_context()
411 c.active = 'profile_edit'
411 c.active = 'profile_edit'
412
412
413 c.perm_user = c.auth_user
413 c.perm_user = c.auth_user
414 c.extern_type = c.user.extern_type
414 c.extern_type = c.user.extern_type
415 c.extern_name = c.user.extern_name
415 c.extern_name = c.user.extern_name
416
416
417 defaults = c.user.get_dict()
417 defaults = c.user.get_dict()
418
418
419 data = render('rhodecode:templates/admin/my_account/my_account.mako',
419 data = render('rhodecode:templates/admin/my_account/my_account.mako',
420 self._get_template_context(c), self.request)
420 self._get_template_context(c), self.request)
421 html = formencode.htmlfill.render(
421 html = formencode.htmlfill.render(
422 data,
422 data,
423 defaults=defaults,
423 defaults=defaults,
424 encoding="UTF-8",
424 encoding="UTF-8",
425 force_defaults=False
425 force_defaults=False
426 )
426 )
427 return Response(html)
427 return Response(html)
428
428
429 @LoginRequired()
429 @LoginRequired()
430 @NotAnonymous()
430 @NotAnonymous()
431 @CSRFRequired()
431 @CSRFRequired()
432 @view_config(
432 @view_config(
433 route_name='my_account_update',
433 route_name='my_account_update',
434 request_method='POST',
434 request_method='POST',
435 renderer='rhodecode:templates/admin/my_account/my_account.mako')
435 renderer='rhodecode:templates/admin/my_account/my_account.mako')
436 def my_account_update(self):
436 def my_account_update(self):
437 _ = self.request.translate
437 _ = self.request.translate
438 c = self.load_default_context()
438 c = self.load_default_context()
439 c.active = 'profile_edit'
439 c.active = 'profile_edit'
440
440
441 c.perm_user = c.auth_user
441 c.perm_user = c.auth_user
442 c.extern_type = c.user.extern_type
442 c.extern_type = c.user.extern_type
443 c.extern_name = c.user.extern_name
443 c.extern_name = c.user.extern_name
444
444
445 _form = UserForm(edit=True,
445 _form = UserForm(edit=True,
446 old_data={'user_id': self._rhodecode_user.user_id,
446 old_data={'user_id': self._rhodecode_user.user_id,
447 'email': self._rhodecode_user.email})()
447 'email': self._rhodecode_user.email})()
448 form_result = {}
448 form_result = {}
449 try:
449 try:
450 post_data = dict(self.request.POST)
450 post_data = dict(self.request.POST)
451 post_data['new_password'] = ''
451 post_data['new_password'] = ''
452 post_data['password_confirmation'] = ''
452 post_data['password_confirmation'] = ''
453 form_result = _form.to_python(post_data)
453 form_result = _form.to_python(post_data)
454 # skip updating those attrs for my account
454 # skip updating those attrs for my account
455 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
455 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
456 'new_password', 'password_confirmation']
456 'new_password', 'password_confirmation']
457 # TODO: plugin should define if username can be updated
457 # TODO: plugin should define if username can be updated
458 if c.extern_type != "rhodecode":
458 if c.extern_type != "rhodecode":
459 # forbid updating username for external accounts
459 # forbid updating username for external accounts
460 skip_attrs.append('username')
460 skip_attrs.append('username')
461
461
462 UserModel().update_user(
462 UserModel().update_user(
463 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
463 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
464 **form_result)
464 **form_result)
465 h.flash(_('Your account was updated successfully'),
465 h.flash(_('Your account was updated successfully'),
466 category='success')
466 category='success')
467 Session().commit()
467 Session().commit()
468
468
469 except formencode.Invalid as errors:
469 except formencode.Invalid as errors:
470 data = render(
470 data = render(
471 'rhodecode:templates/admin/my_account/my_account.mako',
471 'rhodecode:templates/admin/my_account/my_account.mako',
472 self._get_template_context(c), self.request)
472 self._get_template_context(c), self.request)
473
473
474 html = formencode.htmlfill.render(
474 html = formencode.htmlfill.render(
475 data,
475 data,
476 defaults=errors.value,
476 defaults=errors.value,
477 errors=errors.error_dict or {},
477 errors=errors.error_dict or {},
478 prefix_error=False,
478 prefix_error=False,
479 encoding="UTF-8",
479 encoding="UTF-8",
480 force_defaults=False)
480 force_defaults=False)
481 return Response(html)
481 return Response(html)
482
482
483 except Exception:
483 except Exception:
484 log.exception("Exception updating user")
484 log.exception("Exception updating user")
485 h.flash(_('Error occurred during update of user %s')
485 h.flash(_('Error occurred during update of user %s')
486 % form_result.get('username'), category='error')
486 % form_result.get('username'), category='error')
487 raise HTTPFound(h.route_path('my_account_profile'))
487 raise HTTPFound(h.route_path('my_account_profile'))
488
488
489 raise HTTPFound(h.route_path('my_account_profile'))
489 raise HTTPFound(h.route_path('my_account_profile'))
490
490
491 def _get_pull_requests_list(self, statuses):
491 def _get_pull_requests_list(self, statuses):
492 draw, start, limit = self._extract_chunk(self.request)
492 draw, start, limit = self._extract_chunk(self.request)
493 search_q, order_by, order_dir = self._extract_ordering(self.request)
493 search_q, order_by, order_dir = self._extract_ordering(self.request)
494 _render = self.request.get_partial_renderer(
494 _render = self.request.get_partial_renderer(
495 'data_table/_dt_elements.mako')
495 'rhodecode:templates/data_table/_dt_elements.mako')
496
496
497 pull_requests = PullRequestModel().get_im_participating_in(
497 pull_requests = PullRequestModel().get_im_participating_in(
498 user_id=self._rhodecode_user.user_id,
498 user_id=self._rhodecode_user.user_id,
499 statuses=statuses,
499 statuses=statuses,
500 offset=start, length=limit, order_by=order_by,
500 offset=start, length=limit, order_by=order_by,
501 order_dir=order_dir)
501 order_dir=order_dir)
502
502
503 pull_requests_total_count = PullRequestModel().count_im_participating_in(
503 pull_requests_total_count = PullRequestModel().count_im_participating_in(
504 user_id=self._rhodecode_user.user_id, statuses=statuses)
504 user_id=self._rhodecode_user.user_id, statuses=statuses)
505
505
506 data = []
506 data = []
507 comments_model = CommentsModel()
507 comments_model = CommentsModel()
508 for pr in pull_requests:
508 for pr in pull_requests:
509 repo_id = pr.target_repo_id
509 repo_id = pr.target_repo_id
510 comments = comments_model.get_all_comments(
510 comments = comments_model.get_all_comments(
511 repo_id, pull_request=pr)
511 repo_id, pull_request=pr)
512 owned = pr.user_id == self._rhodecode_user.user_id
512 owned = pr.user_id == self._rhodecode_user.user_id
513
513
514 data.append({
514 data.append({
515 'target_repo': _render('pullrequest_target_repo',
515 'target_repo': _render('pullrequest_target_repo',
516 pr.target_repo.repo_name),
516 pr.target_repo.repo_name),
517 'name': _render('pullrequest_name',
517 'name': _render('pullrequest_name',
518 pr.pull_request_id, pr.target_repo.repo_name,
518 pr.pull_request_id, pr.target_repo.repo_name,
519 short=True),
519 short=True),
520 'name_raw': pr.pull_request_id,
520 'name_raw': pr.pull_request_id,
521 'status': _render('pullrequest_status',
521 'status': _render('pullrequest_status',
522 pr.calculated_review_status()),
522 pr.calculated_review_status()),
523 'title': _render(
523 'title': _render(
524 'pullrequest_title', pr.title, pr.description),
524 'pullrequest_title', pr.title, pr.description),
525 'description': h.escape(pr.description),
525 'description': h.escape(pr.description),
526 'updated_on': _render('pullrequest_updated_on',
526 'updated_on': _render('pullrequest_updated_on',
527 h.datetime_to_time(pr.updated_on)),
527 h.datetime_to_time(pr.updated_on)),
528 'updated_on_raw': h.datetime_to_time(pr.updated_on),
528 'updated_on_raw': h.datetime_to_time(pr.updated_on),
529 'created_on': _render('pullrequest_updated_on',
529 'created_on': _render('pullrequest_updated_on',
530 h.datetime_to_time(pr.created_on)),
530 h.datetime_to_time(pr.created_on)),
531 'created_on_raw': h.datetime_to_time(pr.created_on),
531 'created_on_raw': h.datetime_to_time(pr.created_on),
532 'author': _render('pullrequest_author',
532 'author': _render('pullrequest_author',
533 pr.author.full_contact, ),
533 pr.author.full_contact, ),
534 'author_raw': pr.author.full_name,
534 'author_raw': pr.author.full_name,
535 'comments': _render('pullrequest_comments', len(comments)),
535 'comments': _render('pullrequest_comments', len(comments)),
536 'comments_raw': len(comments),
536 'comments_raw': len(comments),
537 'closed': pr.is_closed(),
537 'closed': pr.is_closed(),
538 'owned': owned
538 'owned': owned
539 })
539 })
540
540
541 # json used to render the grid
541 # json used to render the grid
542 data = ({
542 data = ({
543 'draw': draw,
543 'draw': draw,
544 'data': data,
544 'data': data,
545 'recordsTotal': pull_requests_total_count,
545 'recordsTotal': pull_requests_total_count,
546 'recordsFiltered': pull_requests_total_count,
546 'recordsFiltered': pull_requests_total_count,
547 })
547 })
548 return data
548 return data
549
549
550 @LoginRequired()
550 @LoginRequired()
551 @NotAnonymous()
551 @NotAnonymous()
552 @view_config(
552 @view_config(
553 route_name='my_account_pullrequests',
553 route_name='my_account_pullrequests',
554 request_method='GET',
554 request_method='GET',
555 renderer='rhodecode:templates/admin/my_account/my_account.mako')
555 renderer='rhodecode:templates/admin/my_account/my_account.mako')
556 def my_account_pullrequests(self):
556 def my_account_pullrequests(self):
557 c = self.load_default_context()
557 c = self.load_default_context()
558 c.active = 'pullrequests'
558 c.active = 'pullrequests'
559 req_get = self.request.GET
559 req_get = self.request.GET
560
560
561 c.closed = str2bool(req_get.get('pr_show_closed'))
561 c.closed = str2bool(req_get.get('pr_show_closed'))
562
562
563 return self._get_template_context(c)
563 return self._get_template_context(c)
564
564
565 @LoginRequired()
565 @LoginRequired()
566 @NotAnonymous()
566 @NotAnonymous()
567 @view_config(
567 @view_config(
568 route_name='my_account_pullrequests_data',
568 route_name='my_account_pullrequests_data',
569 request_method='GET', renderer='json_ext')
569 request_method='GET', renderer='json_ext')
570 def my_account_pullrequests_data(self):
570 def my_account_pullrequests_data(self):
571 req_get = self.request.GET
571 req_get = self.request.GET
572 closed = str2bool(req_get.get('closed'))
572 closed = str2bool(req_get.get('closed'))
573
573
574 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
574 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
575 if closed:
575 if closed:
576 statuses += [PullRequest.STATUS_CLOSED]
576 statuses += [PullRequest.STATUS_CLOSED]
577
577
578 data = self._get_pull_requests_list(statuses=statuses)
578 data = self._get_pull_requests_list(statuses=statuses)
579 return data
579 return data
580
580
@@ -1,204 +1,204 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
3 # Copyright (C) 2017-2017 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 pytz
21 import pytz
22 import logging
22 import logging
23
23
24 from beaker.cache import cache_region
24 from beaker.cache import cache_region
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.response import Response
26 from pyramid.response import Response
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, HasRepoPermissionAnyDecorator)
33 LoginRequired, HasRepoPermissionAnyDecorator)
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 from rhodecode.model.db import UserApiKeys, CacheKey
36 from rhodecode.model.db import UserApiKeys, CacheKey
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class RepoFeedView(RepoAppView):
41 class RepoFeedView(RepoAppView):
42 def load_default_context(self):
42 def load_default_context(self):
43 c = self._get_local_tmpl_context()
43 c = self._get_local_tmpl_context()
44
44
45 self._register_global_c(c)
45 self._register_global_c(c)
46 self._load_defaults()
46 self._load_defaults()
47 return c
47 return c
48
48
49 def _get_config(self):
49 def _get_config(self):
50 import rhodecode
50 import rhodecode
51 config = rhodecode.CONFIG
51 config = rhodecode.CONFIG
52
52
53 return {
53 return {
54 'language': 'en-us',
54 'language': 'en-us',
55 'feed_ttl': '5', # TTL of feed,
55 'feed_ttl': '5', # TTL of feed,
56 'feed_include_diff':
56 'feed_include_diff':
57 str2bool(config.get('rss_include_diff', False)),
57 str2bool(config.get('rss_include_diff', False)),
58 'feed_items_per_page':
58 'feed_items_per_page':
59 safe_int(config.get('rss_items_per_page', 20)),
59 safe_int(config.get('rss_items_per_page', 20)),
60 'feed_diff_limit':
60 'feed_diff_limit':
61 # we need to protect from parsing huge diffs here other way
61 # we need to protect from parsing huge diffs here other way
62 # we can kill the server
62 # we can kill the server
63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
64 }
64 }
65
65
66 def _load_defaults(self):
66 def _load_defaults(self):
67 _ = self.request.translate
67 _ = self.request.translate
68 config = self._get_config()
68 config = self._get_config()
69 # common values for feeds
69 # common values for feeds
70 self.description = _('Changes on %s repository')
70 self.description = _('Changes on %s repository')
71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
72 self.language = config["language"]
72 self.language = config["language"]
73 self.ttl = config["feed_ttl"]
73 self.ttl = config["feed_ttl"]
74 self.feed_include_diff = config['feed_include_diff']
74 self.feed_include_diff = config['feed_include_diff']
75 self.feed_diff_limit = config['feed_diff_limit']
75 self.feed_diff_limit = config['feed_diff_limit']
76 self.feed_items_per_page = config['feed_items_per_page']
76 self.feed_items_per_page = config['feed_items_per_page']
77
77
78 def _changes(self, commit):
78 def _changes(self, commit):
79 diff_processor = DiffProcessor(
79 diff_processor = DiffProcessor(
80 commit.diff(), diff_limit=self.feed_diff_limit)
80 commit.diff(), diff_limit=self.feed_diff_limit)
81 _parsed = diff_processor.prepare(inline_diff=False)
81 _parsed = diff_processor.prepare(inline_diff=False)
82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83
83
84 return _parsed, limited_diff
84 return _parsed, limited_diff
85
85
86 def _get_title(self, commit):
86 def _get_title(self, commit):
87 return h.shorter(commit.message, 160)
87 return h.shorter(commit.message, 160)
88
88
89 def _get_description(self, commit):
89 def _get_description(self, commit):
90 _renderer = self.request.get_partial_renderer(
90 _renderer = self.request.get_partial_renderer(
91 'feed/atom_feed_entry.mako')
91 'rhodecode:templates/feed/atom_feed_entry.mako')
92 parsed_diff, limited_diff = self._changes(commit)
92 parsed_diff, limited_diff = self._changes(commit)
93 return _renderer(
93 return _renderer(
94 'body',
94 'body',
95 commit=commit,
95 commit=commit,
96 parsed_diff=parsed_diff,
96 parsed_diff=parsed_diff,
97 limited_diff=limited_diff,
97 limited_diff=limited_diff,
98 feed_include_diff=self.feed_include_diff,
98 feed_include_diff=self.feed_include_diff,
99 )
99 )
100
100
101 def _set_timezone(self, date, tzinfo=pytz.utc):
101 def _set_timezone(self, date, tzinfo=pytz.utc):
102 if not getattr(date, "tzinfo", None):
102 if not getattr(date, "tzinfo", None):
103 date.replace(tzinfo=tzinfo)
103 date.replace(tzinfo=tzinfo)
104 return date
104 return date
105
105
106 def _get_commits(self):
106 def _get_commits(self):
107 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
107 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
108
108
109 def uid(self, repo_id, commit_id):
109 def uid(self, repo_id, commit_id):
110 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
110 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
111
111
112 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
112 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
113 @HasRepoPermissionAnyDecorator(
113 @HasRepoPermissionAnyDecorator(
114 'repository.read', 'repository.write', 'repository.admin')
114 'repository.read', 'repository.write', 'repository.admin')
115 @view_config(
115 @view_config(
116 route_name='atom_feed_home', request_method='GET',
116 route_name='atom_feed_home', request_method='GET',
117 renderer=None)
117 renderer=None)
118 def atom(self):
118 def atom(self):
119 """
119 """
120 Produce an atom-1.0 feed via feedgenerator module
120 Produce an atom-1.0 feed via feedgenerator module
121 """
121 """
122 self.load_default_context()
122 self.load_default_context()
123
123
124 @cache_region('long_term')
124 @cache_region('long_term')
125 def _generate_feed(cache_key):
125 def _generate_feed(cache_key):
126 feed = Atom1Feed(
126 feed = Atom1Feed(
127 title=self.title % self.db_repo_name,
127 title=self.title % self.db_repo_name,
128 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
128 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
129 description=self.description % self.db_repo_name,
129 description=self.description % self.db_repo_name,
130 language=self.language,
130 language=self.language,
131 ttl=self.ttl
131 ttl=self.ttl
132 )
132 )
133
133
134 for commit in reversed(self._get_commits()):
134 for commit in reversed(self._get_commits()):
135 date = self._set_timezone(commit.date)
135 date = self._set_timezone(commit.date)
136 feed.add_item(
136 feed.add_item(
137 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
137 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
138 title=self._get_title(commit),
138 title=self._get_title(commit),
139 author_name=commit.author,
139 author_name=commit.author,
140 description=self._get_description(commit),
140 description=self._get_description(commit),
141 link=h.route_url(
141 link=h.route_url(
142 'repo_commit', repo_name=self.db_repo_name,
142 'repo_commit', repo_name=self.db_repo_name,
143 commit_id=commit.raw_id),
143 commit_id=commit.raw_id),
144 pubdate=date,)
144 pubdate=date,)
145
145
146 return feed.mime_type, feed.writeString('utf-8')
146 return feed.mime_type, feed.writeString('utf-8')
147
147
148 invalidator_context = CacheKey.repo_context_cache(
148 invalidator_context = CacheKey.repo_context_cache(
149 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
149 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
150
150
151 with invalidator_context as context:
151 with invalidator_context as context:
152 context.invalidate()
152 context.invalidate()
153 mime_type, feed = context.compute()
153 mime_type, feed = context.compute()
154
154
155 response = Response(feed)
155 response = Response(feed)
156 response.content_type = mime_type
156 response.content_type = mime_type
157 return response
157 return response
158
158
159 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
159 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
160 @HasRepoPermissionAnyDecorator(
160 @HasRepoPermissionAnyDecorator(
161 'repository.read', 'repository.write', 'repository.admin')
161 'repository.read', 'repository.write', 'repository.admin')
162 @view_config(
162 @view_config(
163 route_name='rss_feed_home', request_method='GET',
163 route_name='rss_feed_home', request_method='GET',
164 renderer=None)
164 renderer=None)
165 def rss(self):
165 def rss(self):
166 """
166 """
167 Produce an rss2 feed via feedgenerator module
167 Produce an rss2 feed via feedgenerator module
168 """
168 """
169 self.load_default_context()
169 self.load_default_context()
170
170
171 @cache_region('long_term')
171 @cache_region('long_term')
172 def _generate_feed(cache_key):
172 def _generate_feed(cache_key):
173 feed = Rss201rev2Feed(
173 feed = Rss201rev2Feed(
174 title=self.title % self.db_repo_name,
174 title=self.title % self.db_repo_name,
175 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
175 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
176 description=self.description % self.db_repo_name,
176 description=self.description % self.db_repo_name,
177 language=self.language,
177 language=self.language,
178 ttl=self.ttl
178 ttl=self.ttl
179 )
179 )
180
180
181 for commit in reversed(self._get_commits()):
181 for commit in reversed(self._get_commits()):
182 date = self._set_timezone(commit.date)
182 date = self._set_timezone(commit.date)
183 feed.add_item(
183 feed.add_item(
184 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
184 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
185 title=self._get_title(commit),
185 title=self._get_title(commit),
186 author_name=commit.author,
186 author_name=commit.author,
187 description=self._get_description(commit),
187 description=self._get_description(commit),
188 link=h.route_url(
188 link=h.route_url(
189 'repo_commit', repo_name=self.db_repo_name,
189 'repo_commit', repo_name=self.db_repo_name,
190 commit_id=commit.raw_id),
190 commit_id=commit.raw_id),
191 pubdate=date,)
191 pubdate=date,)
192
192
193 return feed.mime_type, feed.writeString('utf-8')
193 return feed.mime_type, feed.writeString('utf-8')
194
194
195 invalidator_context = CacheKey.repo_context_cache(
195 invalidator_context = CacheKey.repo_context_cache(
196 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
196 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
197
197
198 with invalidator_context as context:
198 with invalidator_context as context:
199 context.invalidate()
199 context.invalidate()
200 mime_type, feed = context.compute()
200 mime_type, feed = context.compute()
201
201
202 response = Response(feed)
202 response = Response(feed)
203 response.content_type = mime_type
203 response.content_type = mime_type
204 return response
204 return response
@@ -1,1235 +1,1235 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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 collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34
34
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.base import vcs_operation_context
37 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 NotAnonymous, CSRFRequired)
40 NotAnonymous, CSRFRequired)
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 ChangesetComment, ChangesetStatus, Repository)
48 ChangesetComment, ChangesetStatus, Repository)
49 from rhodecode.model.forms import PullRequestForm
49 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58
58
59 def load_default_context(self):
59 def load_default_context(self):
60 c = self._get_local_tmpl_context(include_app_defaults=True)
60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 self._register_global_c(c)
63 self._register_global_c(c)
64 return c
64 return c
65
65
66 def _get_pull_requests_list(
66 def _get_pull_requests_list(
67 self, repo_name, source, filter_type, opened_by, statuses):
67 self, repo_name, source, filter_type, opened_by, statuses):
68
68
69 draw, start, limit = self._extract_chunk(self.request)
69 draw, start, limit = self._extract_chunk(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 _render = self.request.get_partial_renderer(
71 _render = self.request.get_partial_renderer(
72 'data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73
73
74 # pagination
74 # pagination
75
75
76 if filter_type == 'awaiting_review':
76 if filter_type == 'awaiting_review':
77 pull_requests = PullRequestModel().get_awaiting_review(
77 pull_requests = PullRequestModel().get_awaiting_review(
78 repo_name, source=source, opened_by=opened_by,
78 repo_name, source=source, opened_by=opened_by,
79 statuses=statuses, offset=start, length=limit,
79 statuses=statuses, offset=start, length=limit,
80 order_by=order_by, order_dir=order_dir)
80 order_by=order_by, order_dir=order_dir)
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 repo_name, source=source, statuses=statuses,
82 repo_name, source=source, statuses=statuses,
83 opened_by=opened_by)
83 opened_by=opened_by)
84 elif filter_type == 'awaiting_my_review':
84 elif filter_type == 'awaiting_my_review':
85 pull_requests = PullRequestModel().get_awaiting_my_review(
85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 repo_name, source=source, opened_by=opened_by,
86 repo_name, source=source, opened_by=opened_by,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 offset=start, length=limit, order_by=order_by,
88 offset=start, length=limit, order_by=order_by,
89 order_dir=order_dir)
89 order_dir=order_dir)
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 statuses=statuses, opened_by=opened_by)
92 statuses=statuses, opened_by=opened_by)
93 else:
93 else:
94 pull_requests = PullRequestModel().get_all(
94 pull_requests = PullRequestModel().get_all(
95 repo_name, source=source, opened_by=opened_by,
95 repo_name, source=source, opened_by=opened_by,
96 statuses=statuses, offset=start, length=limit,
96 statuses=statuses, offset=start, length=limit,
97 order_by=order_by, order_dir=order_dir)
97 order_by=order_by, order_dir=order_dir)
98 pull_requests_total_count = PullRequestModel().count_all(
98 pull_requests_total_count = PullRequestModel().count_all(
99 repo_name, source=source, statuses=statuses,
99 repo_name, source=source, statuses=statuses,
100 opened_by=opened_by)
100 opened_by=opened_by)
101
101
102 data = []
102 data = []
103 comments_model = CommentsModel()
103 comments_model = CommentsModel()
104 for pr in pull_requests:
104 for pr in pull_requests:
105 comments = comments_model.get_all_comments(
105 comments = comments_model.get_all_comments(
106 self.db_repo.repo_id, pull_request=pr)
106 self.db_repo.repo_id, pull_request=pr)
107
107
108 data.append({
108 data.append({
109 'name': _render('pullrequest_name',
109 'name': _render('pullrequest_name',
110 pr.pull_request_id, pr.target_repo.repo_name),
110 pr.pull_request_id, pr.target_repo.repo_name),
111 'name_raw': pr.pull_request_id,
111 'name_raw': pr.pull_request_id,
112 'status': _render('pullrequest_status',
112 'status': _render('pullrequest_status',
113 pr.calculated_review_status()),
113 pr.calculated_review_status()),
114 'title': _render(
114 'title': _render(
115 'pullrequest_title', pr.title, pr.description),
115 'pullrequest_title', pr.title, pr.description),
116 'description': h.escape(pr.description),
116 'description': h.escape(pr.description),
117 'updated_on': _render('pullrequest_updated_on',
117 'updated_on': _render('pullrequest_updated_on',
118 h.datetime_to_time(pr.updated_on)),
118 h.datetime_to_time(pr.updated_on)),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 'created_on': _render('pullrequest_updated_on',
120 'created_on': _render('pullrequest_updated_on',
121 h.datetime_to_time(pr.created_on)),
121 h.datetime_to_time(pr.created_on)),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 'author': _render('pullrequest_author',
123 'author': _render('pullrequest_author',
124 pr.author.full_contact, ),
124 pr.author.full_contact, ),
125 'author_raw': pr.author.full_name,
125 'author_raw': pr.author.full_name,
126 'comments': _render('pullrequest_comments', len(comments)),
126 'comments': _render('pullrequest_comments', len(comments)),
127 'comments_raw': len(comments),
127 'comments_raw': len(comments),
128 'closed': pr.is_closed(),
128 'closed': pr.is_closed(),
129 })
129 })
130
130
131 data = ({
131 data = ({
132 'draw': draw,
132 'draw': draw,
133 'data': data,
133 'data': data,
134 'recordsTotal': pull_requests_total_count,
134 'recordsTotal': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
135 'recordsFiltered': pull_requests_total_count,
136 })
136 })
137 return data
137 return data
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasRepoPermissionAnyDecorator(
140 @HasRepoPermissionAnyDecorator(
141 'repository.read', 'repository.write', 'repository.admin')
141 'repository.read', 'repository.write', 'repository.admin')
142 @view_config(
142 @view_config(
143 route_name='pullrequest_show_all', request_method='GET',
143 route_name='pullrequest_show_all', request_method='GET',
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 def pull_request_list(self):
145 def pull_request_list(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147
147
148 req_get = self.request.GET
148 req_get = self.request.GET
149 c.source = str2bool(req_get.get('source'))
149 c.source = str2bool(req_get.get('source'))
150 c.closed = str2bool(req_get.get('closed'))
150 c.closed = str2bool(req_get.get('closed'))
151 c.my = str2bool(req_get.get('my'))
151 c.my = str2bool(req_get.get('my'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154
154
155 c.active = 'open'
155 c.active = 'open'
156 if c.my:
156 if c.my:
157 c.active = 'my'
157 c.active = 'my'
158 if c.closed:
158 if c.closed:
159 c.active = 'closed'
159 c.active = 'closed'
160 if c.awaiting_review and not c.source:
160 if c.awaiting_review and not c.source:
161 c.active = 'awaiting'
161 c.active = 'awaiting'
162 if c.source and not c.awaiting_review:
162 if c.source and not c.awaiting_review:
163 c.active = 'source'
163 c.active = 'source'
164 if c.awaiting_my_review:
164 if c.awaiting_my_review:
165 c.active = 'awaiting_my'
165 c.active = 'awaiting_my'
166
166
167 return self._get_template_context(c)
167 return self._get_template_context(c)
168
168
169 @LoginRequired()
169 @LoginRequired()
170 @HasRepoPermissionAnyDecorator(
170 @HasRepoPermissionAnyDecorator(
171 'repository.read', 'repository.write', 'repository.admin')
171 'repository.read', 'repository.write', 'repository.admin')
172 @view_config(
172 @view_config(
173 route_name='pullrequest_show_all_data', request_method='GET',
173 route_name='pullrequest_show_all_data', request_method='GET',
174 renderer='json_ext', xhr=True)
174 renderer='json_ext', xhr=True)
175 def pull_request_list_data(self):
175 def pull_request_list_data(self):
176
176
177 # additional filters
177 # additional filters
178 req_get = self.request.GET
178 req_get = self.request.GET
179 source = str2bool(req_get.get('source'))
179 source = str2bool(req_get.get('source'))
180 closed = str2bool(req_get.get('closed'))
180 closed = str2bool(req_get.get('closed'))
181 my = str2bool(req_get.get('my'))
181 my = str2bool(req_get.get('my'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184
184
185 filter_type = 'awaiting_review' if awaiting_review \
185 filter_type = 'awaiting_review' if awaiting_review \
186 else 'awaiting_my_review' if awaiting_my_review \
186 else 'awaiting_my_review' if awaiting_my_review \
187 else None
187 else None
188
188
189 opened_by = None
189 opened_by = None
190 if my:
190 if my:
191 opened_by = [self._rhodecode_user.user_id]
191 opened_by = [self._rhodecode_user.user_id]
192
192
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 if closed:
194 if closed:
195 statuses = [PullRequest.STATUS_CLOSED]
195 statuses = [PullRequest.STATUS_CLOSED]
196
196
197 data = self._get_pull_requests_list(
197 data = self._get_pull_requests_list(
198 repo_name=self.db_repo_name, source=source,
198 repo_name=self.db_repo_name, source=source,
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200
200
201 return data
201 return data
202
202
203 def _get_pr_version(self, pull_request_id, version=None):
203 def _get_pr_version(self, pull_request_id, version=None):
204 at_version = None
204 at_version = None
205
205
206 if version and version == 'latest':
206 if version and version == 'latest':
207 pull_request_ver = PullRequest.get(pull_request_id)
207 pull_request_ver = PullRequest.get(pull_request_id)
208 pull_request_obj = pull_request_ver
208 pull_request_obj = pull_request_ver
209 _org_pull_request_obj = pull_request_obj
209 _org_pull_request_obj = pull_request_obj
210 at_version = 'latest'
210 at_version = 'latest'
211 elif version:
211 elif version:
212 pull_request_ver = PullRequestVersion.get_or_404(version)
212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 pull_request_obj = pull_request_ver
213 pull_request_obj = pull_request_ver
214 _org_pull_request_obj = pull_request_ver.pull_request
214 _org_pull_request_obj = pull_request_ver.pull_request
215 at_version = pull_request_ver.pull_request_version_id
215 at_version = pull_request_ver.pull_request_version_id
216 else:
216 else:
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 pull_request_id)
218 pull_request_id)
219
219
220 pull_request_display_obj = PullRequest.get_pr_display_object(
220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 pull_request_obj, _org_pull_request_obj)
221 pull_request_obj, _org_pull_request_obj)
222
222
223 return _org_pull_request_obj, pull_request_obj, \
223 return _org_pull_request_obj, pull_request_obj, \
224 pull_request_display_obj, at_version
224 pull_request_display_obj, at_version
225
225
226 def _get_diffset(self, source_repo_name, source_repo,
226 def _get_diffset(self, source_repo_name, source_repo,
227 source_ref_id, target_ref_id,
227 source_ref_id, target_ref_id,
228 target_commit, source_commit, diff_limit, fulldiff,
228 target_commit, source_commit, diff_limit, fulldiff,
229 file_limit, display_inline_comments):
229 file_limit, display_inline_comments):
230
230
231 vcs_diff = PullRequestModel().get_diff(
231 vcs_diff = PullRequestModel().get_diff(
232 source_repo, source_ref_id, target_ref_id)
232 source_repo, source_ref_id, target_ref_id)
233
233
234 diff_processor = diffs.DiffProcessor(
234 diff_processor = diffs.DiffProcessor(
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 file_limit=file_limit, show_full_diff=fulldiff)
236 file_limit=file_limit, show_full_diff=fulldiff)
237
237
238 _parsed = diff_processor.prepare()
238 _parsed = diff_processor.prepare()
239
239
240 def _node_getter(commit):
240 def _node_getter(commit):
241 def get_node(fname):
241 def get_node(fname):
242 try:
242 try:
243 return commit.get_node(fname)
243 return commit.get_node(fname)
244 except NodeDoesNotExistError:
244 except NodeDoesNotExistError:
245 return None
245 return None
246
246
247 return get_node
247 return get_node
248
248
249 diffset = codeblocks.DiffSet(
249 diffset = codeblocks.DiffSet(
250 repo_name=self.db_repo_name,
250 repo_name=self.db_repo_name,
251 source_repo_name=source_repo_name,
251 source_repo_name=source_repo_name,
252 source_node_getter=_node_getter(target_commit),
252 source_node_getter=_node_getter(target_commit),
253 target_node_getter=_node_getter(source_commit),
253 target_node_getter=_node_getter(source_commit),
254 comments=display_inline_comments
254 comments=display_inline_comments
255 )
255 )
256 diffset = diffset.render_patchset(
256 diffset = diffset.render_patchset(
257 _parsed, target_commit.raw_id, source_commit.raw_id)
257 _parsed, target_commit.raw_id, source_commit.raw_id)
258
258
259 return diffset
259 return diffset
260
260
261 @LoginRequired()
261 @LoginRequired()
262 @HasRepoPermissionAnyDecorator(
262 @HasRepoPermissionAnyDecorator(
263 'repository.read', 'repository.write', 'repository.admin')
263 'repository.read', 'repository.write', 'repository.admin')
264 @view_config(
264 @view_config(
265 route_name='pullrequest_show', request_method='GET',
265 route_name='pullrequest_show', request_method='GET',
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 def pull_request_show(self):
267 def pull_request_show(self):
268 pull_request_id = self.request.matchdict['pull_request_id']
268 pull_request_id = self.request.matchdict['pull_request_id']
269
269
270 c = self.load_default_context()
270 c = self.load_default_context()
271
271
272 version = self.request.GET.get('version')
272 version = self.request.GET.get('version')
273 from_version = self.request.GET.get('from_version') or version
273 from_version = self.request.GET.get('from_version') or version
274 merge_checks = self.request.GET.get('merge_checks')
274 merge_checks = self.request.GET.get('merge_checks')
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276
276
277 (pull_request_latest,
277 (pull_request_latest,
278 pull_request_at_ver,
278 pull_request_at_ver,
279 pull_request_display_obj,
279 pull_request_display_obj,
280 at_version) = self._get_pr_version(
280 at_version) = self._get_pr_version(
281 pull_request_id, version=version)
281 pull_request_id, version=version)
282 pr_closed = pull_request_latest.is_closed()
282 pr_closed = pull_request_latest.is_closed()
283
283
284 if pr_closed and (version or from_version):
284 if pr_closed and (version or from_version):
285 # not allow to browse versions
285 # not allow to browse versions
286 raise HTTPFound(h.route_path(
286 raise HTTPFound(h.route_path(
287 'pullrequest_show', repo_name=self.db_repo_name,
287 'pullrequest_show', repo_name=self.db_repo_name,
288 pull_request_id=pull_request_id))
288 pull_request_id=pull_request_id))
289
289
290 versions = pull_request_display_obj.versions()
290 versions = pull_request_display_obj.versions()
291
291
292 c.at_version = at_version
292 c.at_version = at_version
293 c.at_version_num = (at_version
293 c.at_version_num = (at_version
294 if at_version and at_version != 'latest'
294 if at_version and at_version != 'latest'
295 else None)
295 else None)
296 c.at_version_pos = ChangesetComment.get_index_from_version(
296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 c.at_version_num, versions)
297 c.at_version_num, versions)
298
298
299 (prev_pull_request_latest,
299 (prev_pull_request_latest,
300 prev_pull_request_at_ver,
300 prev_pull_request_at_ver,
301 prev_pull_request_display_obj,
301 prev_pull_request_display_obj,
302 prev_at_version) = self._get_pr_version(
302 prev_at_version) = self._get_pr_version(
303 pull_request_id, version=from_version)
303 pull_request_id, version=from_version)
304
304
305 c.from_version = prev_at_version
305 c.from_version = prev_at_version
306 c.from_version_num = (prev_at_version
306 c.from_version_num = (prev_at_version
307 if prev_at_version and prev_at_version != 'latest'
307 if prev_at_version and prev_at_version != 'latest'
308 else None)
308 else None)
309 c.from_version_pos = ChangesetComment.get_index_from_version(
309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 c.from_version_num, versions)
310 c.from_version_num, versions)
311
311
312 # define if we're in COMPARE mode or VIEW at version mode
312 # define if we're in COMPARE mode or VIEW at version mode
313 compare = at_version != prev_at_version
313 compare = at_version != prev_at_version
314
314
315 # pull_requests repo_name we opened it against
315 # pull_requests repo_name we opened it against
316 # ie. target_repo must match
316 # ie. target_repo must match
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 raise HTTPNotFound()
318 raise HTTPNotFound()
319
319
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 pull_request_at_ver)
321 pull_request_at_ver)
322
322
323 c.pull_request = pull_request_display_obj
323 c.pull_request = pull_request_display_obj
324 c.pull_request_latest = pull_request_latest
324 c.pull_request_latest = pull_request_latest
325
325
326 if compare or (at_version and not at_version == 'latest'):
326 if compare or (at_version and not at_version == 'latest'):
327 c.allowed_to_change_status = False
327 c.allowed_to_change_status = False
328 c.allowed_to_update = False
328 c.allowed_to_update = False
329 c.allowed_to_merge = False
329 c.allowed_to_merge = False
330 c.allowed_to_delete = False
330 c.allowed_to_delete = False
331 c.allowed_to_comment = False
331 c.allowed_to_comment = False
332 c.allowed_to_close = False
332 c.allowed_to_close = False
333 else:
333 else:
334 can_change_status = PullRequestModel().check_user_change_status(
334 can_change_status = PullRequestModel().check_user_change_status(
335 pull_request_at_ver, self._rhodecode_user)
335 pull_request_at_ver, self._rhodecode_user)
336 c.allowed_to_change_status = can_change_status and not pr_closed
336 c.allowed_to_change_status = can_change_status and not pr_closed
337
337
338 c.allowed_to_update = PullRequestModel().check_user_update(
338 c.allowed_to_update = PullRequestModel().check_user_update(
339 pull_request_latest, self._rhodecode_user) and not pr_closed
339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 pull_request_latest, self._rhodecode_user) and not pr_closed
341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 pull_request_latest, self._rhodecode_user) and not pr_closed
343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 c.allowed_to_comment = not pr_closed
344 c.allowed_to_comment = not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346
346
347 c.forbid_adding_reviewers = False
347 c.forbid_adding_reviewers = False
348 c.forbid_author_to_review = False
348 c.forbid_author_to_review = False
349 c.forbid_commit_author_to_review = False
349 c.forbid_commit_author_to_review = False
350
350
351 if pull_request_latest.reviewer_data and \
351 if pull_request_latest.reviewer_data and \
352 'rules' in pull_request_latest.reviewer_data:
352 'rules' in pull_request_latest.reviewer_data:
353 rules = pull_request_latest.reviewer_data['rules'] or {}
353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 try:
354 try:
355 c.forbid_adding_reviewers = rules.get(
355 c.forbid_adding_reviewers = rules.get(
356 'forbid_adding_reviewers')
356 'forbid_adding_reviewers')
357 c.forbid_author_to_review = rules.get(
357 c.forbid_author_to_review = rules.get(
358 'forbid_author_to_review')
358 'forbid_author_to_review')
359 c.forbid_commit_author_to_review = rules.get(
359 c.forbid_commit_author_to_review = rules.get(
360 'forbid_commit_author_to_review')
360 'forbid_commit_author_to_review')
361 except Exception:
361 except Exception:
362 pass
362 pass
363
363
364 # check merge capabilities
364 # check merge capabilities
365 _merge_check = MergeCheck.validate(
365 _merge_check = MergeCheck.validate(
366 pull_request_latest, user=self._rhodecode_user,
366 pull_request_latest, user=self._rhodecode_user,
367 translator=self.request.translate)
367 translator=self.request.translate)
368 c.pr_merge_errors = _merge_check.error_details
368 c.pr_merge_errors = _merge_check.error_details
369 c.pr_merge_possible = not _merge_check.failed
369 c.pr_merge_possible = not _merge_check.failed
370 c.pr_merge_message = _merge_check.merge_msg
370 c.pr_merge_message = _merge_check.merge_msg
371
371
372 c.pr_merge_info = MergeCheck.get_merge_conditions(
372 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 pull_request_latest, translator=self.request.translate)
373 pull_request_latest, translator=self.request.translate)
374
374
375 c.pull_request_review_status = _merge_check.review_status
375 c.pull_request_review_status = _merge_check.review_status
376 if merge_checks:
376 if merge_checks:
377 self.request.override_renderer = \
377 self.request.override_renderer = \
378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 return self._get_template_context(c)
379 return self._get_template_context(c)
380
380
381 comments_model = CommentsModel()
381 comments_model = CommentsModel()
382
382
383 # reviewers and statuses
383 # reviewers and statuses
384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386
386
387 # GENERAL COMMENTS with versions #
387 # GENERAL COMMENTS with versions #
388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 q = q.order_by(ChangesetComment.comment_id.asc())
389 q = q.order_by(ChangesetComment.comment_id.asc())
390 general_comments = q
390 general_comments = q
391
391
392 # pick comments we want to render at current version
392 # pick comments we want to render at current version
393 c.comment_versions = comments_model.aggregate_comments(
393 c.comment_versions = comments_model.aggregate_comments(
394 general_comments, versions, c.at_version_num)
394 general_comments, versions, c.at_version_num)
395 c.comments = c.comment_versions[c.at_version_num]['until']
395 c.comments = c.comment_versions[c.at_version_num]['until']
396
396
397 # INLINE COMMENTS with versions #
397 # INLINE COMMENTS with versions #
398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 q = q.order_by(ChangesetComment.comment_id.asc())
399 q = q.order_by(ChangesetComment.comment_id.asc())
400 inline_comments = q
400 inline_comments = q
401
401
402 c.inline_versions = comments_model.aggregate_comments(
402 c.inline_versions = comments_model.aggregate_comments(
403 inline_comments, versions, c.at_version_num, inline=True)
403 inline_comments, versions, c.at_version_num, inline=True)
404
404
405 # inject latest version
405 # inject latest version
406 latest_ver = PullRequest.get_pr_display_object(
406 latest_ver = PullRequest.get_pr_display_object(
407 pull_request_latest, pull_request_latest)
407 pull_request_latest, pull_request_latest)
408
408
409 c.versions = versions + [latest_ver]
409 c.versions = versions + [latest_ver]
410
410
411 # if we use version, then do not show later comments
411 # if we use version, then do not show later comments
412 # than current version
412 # than current version
413 display_inline_comments = collections.defaultdict(
413 display_inline_comments = collections.defaultdict(
414 lambda: collections.defaultdict(list))
414 lambda: collections.defaultdict(list))
415 for co in inline_comments:
415 for co in inline_comments:
416 if c.at_version_num:
416 if c.at_version_num:
417 # pick comments that are at least UPTO given version, so we
417 # pick comments that are at least UPTO given version, so we
418 # don't render comments for higher version
418 # don't render comments for higher version
419 should_render = co.pull_request_version_id and \
419 should_render = co.pull_request_version_id and \
420 co.pull_request_version_id <= c.at_version_num
420 co.pull_request_version_id <= c.at_version_num
421 else:
421 else:
422 # showing all, for 'latest'
422 # showing all, for 'latest'
423 should_render = True
423 should_render = True
424
424
425 if should_render:
425 if should_render:
426 display_inline_comments[co.f_path][co.line_no].append(co)
426 display_inline_comments[co.f_path][co.line_no].append(co)
427
427
428 # load diff data into template context, if we use compare mode then
428 # load diff data into template context, if we use compare mode then
429 # diff is calculated based on changes between versions of PR
429 # diff is calculated based on changes between versions of PR
430
430
431 source_repo = pull_request_at_ver.source_repo
431 source_repo = pull_request_at_ver.source_repo
432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433
433
434 target_repo = pull_request_at_ver.target_repo
434 target_repo = pull_request_at_ver.target_repo
435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436
436
437 if compare:
437 if compare:
438 # in compare switch the diff base to latest commit from prev version
438 # in compare switch the diff base to latest commit from prev version
439 target_ref_id = prev_pull_request_display_obj.revisions[0]
439 target_ref_id = prev_pull_request_display_obj.revisions[0]
440
440
441 # despite opening commits for bookmarks/branches/tags, we always
441 # despite opening commits for bookmarks/branches/tags, we always
442 # convert this to rev to prevent changes after bookmark or branch change
442 # convert this to rev to prevent changes after bookmark or branch change
443 c.source_ref_type = 'rev'
443 c.source_ref_type = 'rev'
444 c.source_ref = source_ref_id
444 c.source_ref = source_ref_id
445
445
446 c.target_ref_type = 'rev'
446 c.target_ref_type = 'rev'
447 c.target_ref = target_ref_id
447 c.target_ref = target_ref_id
448
448
449 c.source_repo = source_repo
449 c.source_repo = source_repo
450 c.target_repo = target_repo
450 c.target_repo = target_repo
451
451
452 c.commit_ranges = []
452 c.commit_ranges = []
453 source_commit = EmptyCommit()
453 source_commit = EmptyCommit()
454 target_commit = EmptyCommit()
454 target_commit = EmptyCommit()
455 c.missing_requirements = False
455 c.missing_requirements = False
456
456
457 source_scm = source_repo.scm_instance()
457 source_scm = source_repo.scm_instance()
458 target_scm = target_repo.scm_instance()
458 target_scm = target_repo.scm_instance()
459
459
460 # try first shadow repo, fallback to regular repo
460 # try first shadow repo, fallback to regular repo
461 try:
461 try:
462 commits_source_repo = pull_request_latest.get_shadow_repo()
462 commits_source_repo = pull_request_latest.get_shadow_repo()
463 except Exception:
463 except Exception:
464 log.debug('Failed to get shadow repo', exc_info=True)
464 log.debug('Failed to get shadow repo', exc_info=True)
465 commits_source_repo = source_scm
465 commits_source_repo = source_scm
466
466
467 c.commits_source_repo = commits_source_repo
467 c.commits_source_repo = commits_source_repo
468 commit_cache = {}
468 commit_cache = {}
469 try:
469 try:
470 pre_load = ["author", "branch", "date", "message"]
470 pre_load = ["author", "branch", "date", "message"]
471 show_revs = pull_request_at_ver.revisions
471 show_revs = pull_request_at_ver.revisions
472 for rev in show_revs:
472 for rev in show_revs:
473 comm = commits_source_repo.get_commit(
473 comm = commits_source_repo.get_commit(
474 commit_id=rev, pre_load=pre_load)
474 commit_id=rev, pre_load=pre_load)
475 c.commit_ranges.append(comm)
475 c.commit_ranges.append(comm)
476 commit_cache[comm.raw_id] = comm
476 commit_cache[comm.raw_id] = comm
477
477
478 # Order here matters, we first need to get target, and then
478 # Order here matters, we first need to get target, and then
479 # the source
479 # the source
480 target_commit = commits_source_repo.get_commit(
480 target_commit = commits_source_repo.get_commit(
481 commit_id=safe_str(target_ref_id))
481 commit_id=safe_str(target_ref_id))
482
482
483 source_commit = commits_source_repo.get_commit(
483 source_commit = commits_source_repo.get_commit(
484 commit_id=safe_str(source_ref_id))
484 commit_id=safe_str(source_ref_id))
485
485
486 except CommitDoesNotExistError:
486 except CommitDoesNotExistError:
487 log.warning(
487 log.warning(
488 'Failed to get commit from `{}` repo'.format(
488 'Failed to get commit from `{}` repo'.format(
489 commits_source_repo), exc_info=True)
489 commits_source_repo), exc_info=True)
490 except RepositoryRequirementError:
490 except RepositoryRequirementError:
491 log.warning(
491 log.warning(
492 'Failed to get all required data from repo', exc_info=True)
492 'Failed to get all required data from repo', exc_info=True)
493 c.missing_requirements = True
493 c.missing_requirements = True
494
494
495 c.ancestor = None # set it to None, to hide it from PR view
495 c.ancestor = None # set it to None, to hide it from PR view
496
496
497 try:
497 try:
498 ancestor_id = source_scm.get_common_ancestor(
498 ancestor_id = source_scm.get_common_ancestor(
499 source_commit.raw_id, target_commit.raw_id, target_scm)
499 source_commit.raw_id, target_commit.raw_id, target_scm)
500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 except Exception:
501 except Exception:
502 c.ancestor_commit = None
502 c.ancestor_commit = None
503
503
504 c.statuses = source_repo.statuses(
504 c.statuses = source_repo.statuses(
505 [x.raw_id for x in c.commit_ranges])
505 [x.raw_id for x in c.commit_ranges])
506
506
507 # auto collapse if we have more than limit
507 # auto collapse if we have more than limit
508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 c.compare_mode = compare
510 c.compare_mode = compare
511
511
512 # diff_limit is the old behavior, will cut off the whole diff
512 # diff_limit is the old behavior, will cut off the whole diff
513 # if the limit is applied otherwise will just hide the
513 # if the limit is applied otherwise will just hide the
514 # big files from the front-end
514 # big files from the front-end
515 diff_limit = c.visual.cut_off_limit_diff
515 diff_limit = c.visual.cut_off_limit_diff
516 file_limit = c.visual.cut_off_limit_file
516 file_limit = c.visual.cut_off_limit_file
517
517
518 c.missing_commits = False
518 c.missing_commits = False
519 if (c.missing_requirements
519 if (c.missing_requirements
520 or isinstance(source_commit, EmptyCommit)
520 or isinstance(source_commit, EmptyCommit)
521 or source_commit == target_commit):
521 or source_commit == target_commit):
522
522
523 c.missing_commits = True
523 c.missing_commits = True
524 else:
524 else:
525
525
526 c.diffset = self._get_diffset(
526 c.diffset = self._get_diffset(
527 c.source_repo.repo_name, commits_source_repo,
527 c.source_repo.repo_name, commits_source_repo,
528 source_ref_id, target_ref_id,
528 source_ref_id, target_ref_id,
529 target_commit, source_commit,
529 target_commit, source_commit,
530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531
531
532 c.limited_diff = c.diffset.limited_diff
532 c.limited_diff = c.diffset.limited_diff
533
533
534 # calculate removed files that are bound to comments
534 # calculate removed files that are bound to comments
535 comment_deleted_files = [
535 comment_deleted_files = [
536 fname for fname in display_inline_comments
536 fname for fname in display_inline_comments
537 if fname not in c.diffset.file_stats]
537 if fname not in c.diffset.file_stats]
538
538
539 c.deleted_files_comments = collections.defaultdict(dict)
539 c.deleted_files_comments = collections.defaultdict(dict)
540 for fname, per_line_comments in display_inline_comments.items():
540 for fname, per_line_comments in display_inline_comments.items():
541 if fname in comment_deleted_files:
541 if fname in comment_deleted_files:
542 c.deleted_files_comments[fname]['stats'] = 0
542 c.deleted_files_comments[fname]['stats'] = 0
543 c.deleted_files_comments[fname]['comments'] = list()
543 c.deleted_files_comments[fname]['comments'] = list()
544 for lno, comments in per_line_comments.items():
544 for lno, comments in per_line_comments.items():
545 c.deleted_files_comments[fname]['comments'].extend(
545 c.deleted_files_comments[fname]['comments'].extend(
546 comments)
546 comments)
547
547
548 # this is a hack to properly display links, when creating PR, the
548 # this is a hack to properly display links, when creating PR, the
549 # compare view and others uses different notation, and
549 # compare view and others uses different notation, and
550 # compare_commits.mako renders links based on the target_repo.
550 # compare_commits.mako renders links based on the target_repo.
551 # We need to swap that here to generate it properly on the html side
551 # We need to swap that here to generate it properly on the html side
552 c.target_repo = c.source_repo
552 c.target_repo = c.source_repo
553
553
554 c.commit_statuses = ChangesetStatus.STATUSES
554 c.commit_statuses = ChangesetStatus.STATUSES
555
555
556 c.show_version_changes = not pr_closed
556 c.show_version_changes = not pr_closed
557 if c.show_version_changes:
557 if c.show_version_changes:
558 cur_obj = pull_request_at_ver
558 cur_obj = pull_request_at_ver
559 prev_obj = prev_pull_request_at_ver
559 prev_obj = prev_pull_request_at_ver
560
560
561 old_commit_ids = prev_obj.revisions
561 old_commit_ids = prev_obj.revisions
562 new_commit_ids = cur_obj.revisions
562 new_commit_ids = cur_obj.revisions
563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 old_commit_ids, new_commit_ids)
564 old_commit_ids, new_commit_ids)
565 c.commit_changes_summary = commit_changes
565 c.commit_changes_summary = commit_changes
566
566
567 # calculate the diff for commits between versions
567 # calculate the diff for commits between versions
568 c.commit_changes = []
568 c.commit_changes = []
569 mark = lambda cs, fw: list(
569 mark = lambda cs, fw: list(
570 h.itertools.izip_longest([], cs, fillvalue=fw))
570 h.itertools.izip_longest([], cs, fillvalue=fw))
571 for c_type, raw_id in mark(commit_changes.added, 'a') \
571 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 + mark(commit_changes.removed, 'r') \
572 + mark(commit_changes.removed, 'r') \
573 + mark(commit_changes.common, 'c'):
573 + mark(commit_changes.common, 'c'):
574
574
575 if raw_id in commit_cache:
575 if raw_id in commit_cache:
576 commit = commit_cache[raw_id]
576 commit = commit_cache[raw_id]
577 else:
577 else:
578 try:
578 try:
579 commit = commits_source_repo.get_commit(raw_id)
579 commit = commits_source_repo.get_commit(raw_id)
580 except CommitDoesNotExistError:
580 except CommitDoesNotExistError:
581 # in case we fail extracting still use "dummy" commit
581 # in case we fail extracting still use "dummy" commit
582 # for display in commit diff
582 # for display in commit diff
583 commit = h.AttributeDict(
583 commit = h.AttributeDict(
584 {'raw_id': raw_id,
584 {'raw_id': raw_id,
585 'message': 'EMPTY or MISSING COMMIT'})
585 'message': 'EMPTY or MISSING COMMIT'})
586 c.commit_changes.append([c_type, commit])
586 c.commit_changes.append([c_type, commit])
587
587
588 # current user review statuses for each version
588 # current user review statuses for each version
589 c.review_versions = {}
589 c.review_versions = {}
590 if self._rhodecode_user.user_id in allowed_reviewers:
590 if self._rhodecode_user.user_id in allowed_reviewers:
591 for co in general_comments:
591 for co in general_comments:
592 if co.author.user_id == self._rhodecode_user.user_id:
592 if co.author.user_id == self._rhodecode_user.user_id:
593 # each comment has a status change
593 # each comment has a status change
594 status = co.status_change
594 status = co.status_change
595 if status:
595 if status:
596 _ver_pr = status[0].comment.pull_request_version_id
596 _ver_pr = status[0].comment.pull_request_version_id
597 c.review_versions[_ver_pr] = status[0]
597 c.review_versions[_ver_pr] = status[0]
598
598
599 return self._get_template_context(c)
599 return self._get_template_context(c)
600
600
601 def assure_not_empty_repo(self):
601 def assure_not_empty_repo(self):
602 _ = self.request.translate
602 _ = self.request.translate
603
603
604 try:
604 try:
605 self.db_repo.scm_instance().get_commit()
605 self.db_repo.scm_instance().get_commit()
606 except EmptyRepositoryError:
606 except EmptyRepositoryError:
607 h.flash(h.literal(_('There are no commits yet')),
607 h.flash(h.literal(_('There are no commits yet')),
608 category='warning')
608 category='warning')
609 raise HTTPFound(
609 raise HTTPFound(
610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611
611
612 @LoginRequired()
612 @LoginRequired()
613 @NotAnonymous()
613 @NotAnonymous()
614 @HasRepoPermissionAnyDecorator(
614 @HasRepoPermissionAnyDecorator(
615 'repository.read', 'repository.write', 'repository.admin')
615 'repository.read', 'repository.write', 'repository.admin')
616 @view_config(
616 @view_config(
617 route_name='pullrequest_new', request_method='GET',
617 route_name='pullrequest_new', request_method='GET',
618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 def pull_request_new(self):
619 def pull_request_new(self):
620 _ = self.request.translate
620 _ = self.request.translate
621 c = self.load_default_context()
621 c = self.load_default_context()
622
622
623 self.assure_not_empty_repo()
623 self.assure_not_empty_repo()
624 source_repo = self.db_repo
624 source_repo = self.db_repo
625
625
626 commit_id = self.request.GET.get('commit')
626 commit_id = self.request.GET.get('commit')
627 branch_ref = self.request.GET.get('branch')
627 branch_ref = self.request.GET.get('branch')
628 bookmark_ref = self.request.GET.get('bookmark')
628 bookmark_ref = self.request.GET.get('bookmark')
629
629
630 try:
630 try:
631 source_repo_data = PullRequestModel().generate_repo_data(
631 source_repo_data = PullRequestModel().generate_repo_data(
632 source_repo, commit_id=commit_id,
632 source_repo, commit_id=commit_id,
633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 except CommitDoesNotExistError as e:
634 except CommitDoesNotExistError as e:
635 log.exception(e)
635 log.exception(e)
636 h.flash(_('Commit does not exist'), 'error')
636 h.flash(_('Commit does not exist'), 'error')
637 raise HTTPFound(
637 raise HTTPFound(
638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639
639
640 default_target_repo = source_repo
640 default_target_repo = source_repo
641
641
642 if source_repo.parent:
642 if source_repo.parent:
643 parent_vcs_obj = source_repo.parent.scm_instance()
643 parent_vcs_obj = source_repo.parent.scm_instance()
644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 # change default if we have a parent repo
645 # change default if we have a parent repo
646 default_target_repo = source_repo.parent
646 default_target_repo = source_repo.parent
647
647
648 target_repo_data = PullRequestModel().generate_repo_data(
648 target_repo_data = PullRequestModel().generate_repo_data(
649 default_target_repo, translator=self.request.translate)
649 default_target_repo, translator=self.request.translate)
650
650
651 selected_source_ref = source_repo_data['refs']['selected_ref']
651 selected_source_ref = source_repo_data['refs']['selected_ref']
652
652
653 title_source_ref = selected_source_ref.split(':', 2)[1]
653 title_source_ref = selected_source_ref.split(':', 2)[1]
654 c.default_title = PullRequestModel().generate_pullrequest_title(
654 c.default_title = PullRequestModel().generate_pullrequest_title(
655 source=source_repo.repo_name,
655 source=source_repo.repo_name,
656 source_ref=title_source_ref,
656 source_ref=title_source_ref,
657 target=default_target_repo.repo_name
657 target=default_target_repo.repo_name
658 )
658 )
659
659
660 c.default_repo_data = {
660 c.default_repo_data = {
661 'source_repo_name': source_repo.repo_name,
661 'source_repo_name': source_repo.repo_name,
662 'source_refs_json': json.dumps(source_repo_data),
662 'source_refs_json': json.dumps(source_repo_data),
663 'target_repo_name': default_target_repo.repo_name,
663 'target_repo_name': default_target_repo.repo_name,
664 'target_refs_json': json.dumps(target_repo_data),
664 'target_refs_json': json.dumps(target_repo_data),
665 }
665 }
666 c.default_source_ref = selected_source_ref
666 c.default_source_ref = selected_source_ref
667
667
668 return self._get_template_context(c)
668 return self._get_template_context(c)
669
669
670 @LoginRequired()
670 @LoginRequired()
671 @NotAnonymous()
671 @NotAnonymous()
672 @HasRepoPermissionAnyDecorator(
672 @HasRepoPermissionAnyDecorator(
673 'repository.read', 'repository.write', 'repository.admin')
673 'repository.read', 'repository.write', 'repository.admin')
674 @view_config(
674 @view_config(
675 route_name='pullrequest_repo_refs', request_method='GET',
675 route_name='pullrequest_repo_refs', request_method='GET',
676 renderer='json_ext', xhr=True)
676 renderer='json_ext', xhr=True)
677 def pull_request_repo_refs(self):
677 def pull_request_repo_refs(self):
678 target_repo_name = self.request.matchdict['target_repo_name']
678 target_repo_name = self.request.matchdict['target_repo_name']
679 repo = Repository.get_by_repo_name(target_repo_name)
679 repo = Repository.get_by_repo_name(target_repo_name)
680 if not repo:
680 if not repo:
681 raise HTTPNotFound()
681 raise HTTPNotFound()
682 return PullRequestModel().generate_repo_data(
682 return PullRequestModel().generate_repo_data(
683 repo, translator=self.request.translate)
683 repo, translator=self.request.translate)
684
684
685 @LoginRequired()
685 @LoginRequired()
686 @NotAnonymous()
686 @NotAnonymous()
687 @HasRepoPermissionAnyDecorator(
687 @HasRepoPermissionAnyDecorator(
688 'repository.read', 'repository.write', 'repository.admin')
688 'repository.read', 'repository.write', 'repository.admin')
689 @view_config(
689 @view_config(
690 route_name='pullrequest_repo_destinations', request_method='GET',
690 route_name='pullrequest_repo_destinations', request_method='GET',
691 renderer='json_ext', xhr=True)
691 renderer='json_ext', xhr=True)
692 def pull_request_repo_destinations(self):
692 def pull_request_repo_destinations(self):
693 _ = self.request.translate
693 _ = self.request.translate
694 filter_query = self.request.GET.get('query')
694 filter_query = self.request.GET.get('query')
695
695
696 query = Repository.query() \
696 query = Repository.query() \
697 .order_by(func.length(Repository.repo_name)) \
697 .order_by(func.length(Repository.repo_name)) \
698 .filter(
698 .filter(
699 or_(Repository.repo_name == self.db_repo.repo_name,
699 or_(Repository.repo_name == self.db_repo.repo_name,
700 Repository.fork_id == self.db_repo.repo_id))
700 Repository.fork_id == self.db_repo.repo_id))
701
701
702 if filter_query:
702 if filter_query:
703 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
703 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
704 query = query.filter(
704 query = query.filter(
705 Repository.repo_name.ilike(ilike_expression))
705 Repository.repo_name.ilike(ilike_expression))
706
706
707 add_parent = False
707 add_parent = False
708 if self.db_repo.parent:
708 if self.db_repo.parent:
709 if filter_query in self.db_repo.parent.repo_name:
709 if filter_query in self.db_repo.parent.repo_name:
710 parent_vcs_obj = self.db_repo.parent.scm_instance()
710 parent_vcs_obj = self.db_repo.parent.scm_instance()
711 if parent_vcs_obj and not parent_vcs_obj.is_empty():
711 if parent_vcs_obj and not parent_vcs_obj.is_empty():
712 add_parent = True
712 add_parent = True
713
713
714 limit = 20 - 1 if add_parent else 20
714 limit = 20 - 1 if add_parent else 20
715 all_repos = query.limit(limit).all()
715 all_repos = query.limit(limit).all()
716 if add_parent:
716 if add_parent:
717 all_repos += [self.db_repo.parent]
717 all_repos += [self.db_repo.parent]
718
718
719 repos = []
719 repos = []
720 for obj in ScmModel().get_repos(all_repos):
720 for obj in ScmModel().get_repos(all_repos):
721 repos.append({
721 repos.append({
722 'id': obj['name'],
722 'id': obj['name'],
723 'text': obj['name'],
723 'text': obj['name'],
724 'type': 'repo',
724 'type': 'repo',
725 'obj': obj['dbrepo']
725 'obj': obj['dbrepo']
726 })
726 })
727
727
728 data = {
728 data = {
729 'more': False,
729 'more': False,
730 'results': [{
730 'results': [{
731 'text': _('Repositories'),
731 'text': _('Repositories'),
732 'children': repos
732 'children': repos
733 }] if repos else []
733 }] if repos else []
734 }
734 }
735 return data
735 return data
736
736
737 @LoginRequired()
737 @LoginRequired()
738 @NotAnonymous()
738 @NotAnonymous()
739 @HasRepoPermissionAnyDecorator(
739 @HasRepoPermissionAnyDecorator(
740 'repository.read', 'repository.write', 'repository.admin')
740 'repository.read', 'repository.write', 'repository.admin')
741 @CSRFRequired()
741 @CSRFRequired()
742 @view_config(
742 @view_config(
743 route_name='pullrequest_create', request_method='POST',
743 route_name='pullrequest_create', request_method='POST',
744 renderer=None)
744 renderer=None)
745 def pull_request_create(self):
745 def pull_request_create(self):
746 _ = self.request.translate
746 _ = self.request.translate
747 self.assure_not_empty_repo()
747 self.assure_not_empty_repo()
748
748
749 controls = peppercorn.parse(self.request.POST.items())
749 controls = peppercorn.parse(self.request.POST.items())
750
750
751 try:
751 try:
752 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
752 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
753 except formencode.Invalid as errors:
753 except formencode.Invalid as errors:
754 if errors.error_dict.get('revisions'):
754 if errors.error_dict.get('revisions'):
755 msg = 'Revisions: %s' % errors.error_dict['revisions']
755 msg = 'Revisions: %s' % errors.error_dict['revisions']
756 elif errors.error_dict.get('pullrequest_title'):
756 elif errors.error_dict.get('pullrequest_title'):
757 msg = _('Pull request requires a title with min. 3 chars')
757 msg = _('Pull request requires a title with min. 3 chars')
758 else:
758 else:
759 msg = _('Error creating pull request: {}').format(errors)
759 msg = _('Error creating pull request: {}').format(errors)
760 log.exception(msg)
760 log.exception(msg)
761 h.flash(msg, 'error')
761 h.flash(msg, 'error')
762
762
763 # would rather just go back to form ...
763 # would rather just go back to form ...
764 raise HTTPFound(
764 raise HTTPFound(
765 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
765 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
766
766
767 source_repo = _form['source_repo']
767 source_repo = _form['source_repo']
768 source_ref = _form['source_ref']
768 source_ref = _form['source_ref']
769 target_repo = _form['target_repo']
769 target_repo = _form['target_repo']
770 target_ref = _form['target_ref']
770 target_ref = _form['target_ref']
771 commit_ids = _form['revisions'][::-1]
771 commit_ids = _form['revisions'][::-1]
772
772
773 # find the ancestor for this pr
773 # find the ancestor for this pr
774 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
774 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
775 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
775 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
776
776
777 # re-check permissions again here
777 # re-check permissions again here
778 # source_repo we must have read permissions
778 # source_repo we must have read permissions
779
779
780 source_perm = HasRepoPermissionAny(
780 source_perm = HasRepoPermissionAny(
781 'repository.read',
781 'repository.read',
782 'repository.write', 'repository.admin')(source_db_repo.repo_name)
782 'repository.write', 'repository.admin')(source_db_repo.repo_name)
783 if not source_perm:
783 if not source_perm:
784 msg = _('Not Enough permissions to source repo `{}`.'.format(
784 msg = _('Not Enough permissions to source repo `{}`.'.format(
785 source_db_repo.repo_name))
785 source_db_repo.repo_name))
786 h.flash(msg, category='error')
786 h.flash(msg, category='error')
787 # copy the args back to redirect
787 # copy the args back to redirect
788 org_query = self.request.GET.mixed()
788 org_query = self.request.GET.mixed()
789 raise HTTPFound(
789 raise HTTPFound(
790 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
790 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
791 _query=org_query))
791 _query=org_query))
792
792
793 # target repo we must have write permissions, and also later on
793 # target repo we must have write permissions, and also later on
794 # we want to check branch permissions here
794 # we want to check branch permissions here
795 target_perm = HasRepoPermissionAny(
795 target_perm = HasRepoPermissionAny(
796 'repository.write', 'repository.admin')(target_db_repo.repo_name)
796 'repository.write', 'repository.admin')(target_db_repo.repo_name)
797 if not target_perm:
797 if not target_perm:
798 msg = _('Not Enough permissions to target repo `{}`.'.format(
798 msg = _('Not Enough permissions to target repo `{}`.'.format(
799 target_db_repo.repo_name))
799 target_db_repo.repo_name))
800 h.flash(msg, category='error')
800 h.flash(msg, category='error')
801 # copy the args back to redirect
801 # copy the args back to redirect
802 org_query = self.request.GET.mixed()
802 org_query = self.request.GET.mixed()
803 raise HTTPFound(
803 raise HTTPFound(
804 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
804 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
805 _query=org_query))
805 _query=org_query))
806
806
807 source_scm = source_db_repo.scm_instance()
807 source_scm = source_db_repo.scm_instance()
808 target_scm = target_db_repo.scm_instance()
808 target_scm = target_db_repo.scm_instance()
809
809
810 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
810 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
811 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
811 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
812
812
813 ancestor = source_scm.get_common_ancestor(
813 ancestor = source_scm.get_common_ancestor(
814 source_commit.raw_id, target_commit.raw_id, target_scm)
814 source_commit.raw_id, target_commit.raw_id, target_scm)
815
815
816 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
816 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
817 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
817 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
818
818
819 pullrequest_title = _form['pullrequest_title']
819 pullrequest_title = _form['pullrequest_title']
820 title_source_ref = source_ref.split(':', 2)[1]
820 title_source_ref = source_ref.split(':', 2)[1]
821 if not pullrequest_title:
821 if not pullrequest_title:
822 pullrequest_title = PullRequestModel().generate_pullrequest_title(
822 pullrequest_title = PullRequestModel().generate_pullrequest_title(
823 source=source_repo,
823 source=source_repo,
824 source_ref=title_source_ref,
824 source_ref=title_source_ref,
825 target=target_repo
825 target=target_repo
826 )
826 )
827
827
828 description = _form['pullrequest_desc']
828 description = _form['pullrequest_desc']
829
829
830 get_default_reviewers_data, validate_default_reviewers = \
830 get_default_reviewers_data, validate_default_reviewers = \
831 PullRequestModel().get_reviewer_functions()
831 PullRequestModel().get_reviewer_functions()
832
832
833 # recalculate reviewers logic, to make sure we can validate this
833 # recalculate reviewers logic, to make sure we can validate this
834 reviewer_rules = get_default_reviewers_data(
834 reviewer_rules = get_default_reviewers_data(
835 self._rhodecode_db_user, source_db_repo,
835 self._rhodecode_db_user, source_db_repo,
836 source_commit, target_db_repo, target_commit)
836 source_commit, target_db_repo, target_commit)
837
837
838 given_reviewers = _form['review_members']
838 given_reviewers = _form['review_members']
839 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
839 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
840
840
841 try:
841 try:
842 pull_request = PullRequestModel().create(
842 pull_request = PullRequestModel().create(
843 self._rhodecode_user.user_id, source_repo, source_ref,
843 self._rhodecode_user.user_id, source_repo, source_ref,
844 target_repo, target_ref, commit_ids, reviewers,
844 target_repo, target_ref, commit_ids, reviewers,
845 pullrequest_title, description, reviewer_rules
845 pullrequest_title, description, reviewer_rules
846 )
846 )
847 Session().commit()
847 Session().commit()
848
848
849 h.flash(_('Successfully opened new pull request'),
849 h.flash(_('Successfully opened new pull request'),
850 category='success')
850 category='success')
851 except Exception:
851 except Exception:
852 msg = _('Error occurred during creation of this pull request.')
852 msg = _('Error occurred during creation of this pull request.')
853 log.exception(msg)
853 log.exception(msg)
854 h.flash(msg, category='error')
854 h.flash(msg, category='error')
855
855
856 # copy the args back to redirect
856 # copy the args back to redirect
857 org_query = self.request.GET.mixed()
857 org_query = self.request.GET.mixed()
858 raise HTTPFound(
858 raise HTTPFound(
859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
860 _query=org_query))
860 _query=org_query))
861
861
862 raise HTTPFound(
862 raise HTTPFound(
863 h.route_path('pullrequest_show', repo_name=target_repo,
863 h.route_path('pullrequest_show', repo_name=target_repo,
864 pull_request_id=pull_request.pull_request_id))
864 pull_request_id=pull_request.pull_request_id))
865
865
866 @LoginRequired()
866 @LoginRequired()
867 @NotAnonymous()
867 @NotAnonymous()
868 @HasRepoPermissionAnyDecorator(
868 @HasRepoPermissionAnyDecorator(
869 'repository.read', 'repository.write', 'repository.admin')
869 'repository.read', 'repository.write', 'repository.admin')
870 @CSRFRequired()
870 @CSRFRequired()
871 @view_config(
871 @view_config(
872 route_name='pullrequest_update', request_method='POST',
872 route_name='pullrequest_update', request_method='POST',
873 renderer='json_ext')
873 renderer='json_ext')
874 def pull_request_update(self):
874 def pull_request_update(self):
875 pull_request = PullRequest.get_or_404(
875 pull_request = PullRequest.get_or_404(
876 self.request.matchdict['pull_request_id'])
876 self.request.matchdict['pull_request_id'])
877
877
878 # only owner or admin can update it
878 # only owner or admin can update it
879 allowed_to_update = PullRequestModel().check_user_update(
879 allowed_to_update = PullRequestModel().check_user_update(
880 pull_request, self._rhodecode_user)
880 pull_request, self._rhodecode_user)
881 if allowed_to_update:
881 if allowed_to_update:
882 controls = peppercorn.parse(self.request.POST.items())
882 controls = peppercorn.parse(self.request.POST.items())
883
883
884 if 'review_members' in controls:
884 if 'review_members' in controls:
885 self._update_reviewers(
885 self._update_reviewers(
886 pull_request, controls['review_members'],
886 pull_request, controls['review_members'],
887 pull_request.reviewer_data)
887 pull_request.reviewer_data)
888 elif str2bool(self.request.POST.get('update_commits', 'false')):
888 elif str2bool(self.request.POST.get('update_commits', 'false')):
889 self._update_commits(pull_request)
889 self._update_commits(pull_request)
890 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
890 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
891 self._edit_pull_request(pull_request)
891 self._edit_pull_request(pull_request)
892 else:
892 else:
893 raise HTTPBadRequest()
893 raise HTTPBadRequest()
894 return True
894 return True
895 raise HTTPForbidden()
895 raise HTTPForbidden()
896
896
897 def _edit_pull_request(self, pull_request):
897 def _edit_pull_request(self, pull_request):
898 _ = self.request.translate
898 _ = self.request.translate
899 try:
899 try:
900 PullRequestModel().edit(
900 PullRequestModel().edit(
901 pull_request, self.request.POST.get('title'),
901 pull_request, self.request.POST.get('title'),
902 self.request.POST.get('description'), self._rhodecode_user)
902 self.request.POST.get('description'), self._rhodecode_user)
903 except ValueError:
903 except ValueError:
904 msg = _(u'Cannot update closed pull requests.')
904 msg = _(u'Cannot update closed pull requests.')
905 h.flash(msg, category='error')
905 h.flash(msg, category='error')
906 return
906 return
907 else:
907 else:
908 Session().commit()
908 Session().commit()
909
909
910 msg = _(u'Pull request title & description updated.')
910 msg = _(u'Pull request title & description updated.')
911 h.flash(msg, category='success')
911 h.flash(msg, category='success')
912 return
912 return
913
913
914 def _update_commits(self, pull_request):
914 def _update_commits(self, pull_request):
915 _ = self.request.translate
915 _ = self.request.translate
916 resp = PullRequestModel().update_commits(pull_request)
916 resp = PullRequestModel().update_commits(pull_request)
917
917
918 if resp.executed:
918 if resp.executed:
919
919
920 if resp.target_changed and resp.source_changed:
920 if resp.target_changed and resp.source_changed:
921 changed = 'target and source repositories'
921 changed = 'target and source repositories'
922 elif resp.target_changed and not resp.source_changed:
922 elif resp.target_changed and not resp.source_changed:
923 changed = 'target repository'
923 changed = 'target repository'
924 elif not resp.target_changed and resp.source_changed:
924 elif not resp.target_changed and resp.source_changed:
925 changed = 'source repository'
925 changed = 'source repository'
926 else:
926 else:
927 changed = 'nothing'
927 changed = 'nothing'
928
928
929 msg = _(
929 msg = _(
930 u'Pull request updated to "{source_commit_id}" with '
930 u'Pull request updated to "{source_commit_id}" with '
931 u'{count_added} added, {count_removed} removed commits. '
931 u'{count_added} added, {count_removed} removed commits. '
932 u'Source of changes: {change_source}')
932 u'Source of changes: {change_source}')
933 msg = msg.format(
933 msg = msg.format(
934 source_commit_id=pull_request.source_ref_parts.commit_id,
934 source_commit_id=pull_request.source_ref_parts.commit_id,
935 count_added=len(resp.changes.added),
935 count_added=len(resp.changes.added),
936 count_removed=len(resp.changes.removed),
936 count_removed=len(resp.changes.removed),
937 change_source=changed)
937 change_source=changed)
938 h.flash(msg, category='success')
938 h.flash(msg, category='success')
939
939
940 channel = '/repo${}$/pr/{}'.format(
940 channel = '/repo${}$/pr/{}'.format(
941 pull_request.target_repo.repo_name,
941 pull_request.target_repo.repo_name,
942 pull_request.pull_request_id)
942 pull_request.pull_request_id)
943 message = msg + (
943 message = msg + (
944 ' - <a onclick="window.location.reload()">'
944 ' - <a onclick="window.location.reload()">'
945 '<strong>{}</strong></a>'.format(_('Reload page')))
945 '<strong>{}</strong></a>'.format(_('Reload page')))
946 channelstream.post_message(
946 channelstream.post_message(
947 channel, message, self._rhodecode_user.username,
947 channel, message, self._rhodecode_user.username,
948 registry=self.request.registry)
948 registry=self.request.registry)
949 else:
949 else:
950 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
950 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
951 warning_reasons = [
951 warning_reasons = [
952 UpdateFailureReason.NO_CHANGE,
952 UpdateFailureReason.NO_CHANGE,
953 UpdateFailureReason.WRONG_REF_TYPE,
953 UpdateFailureReason.WRONG_REF_TYPE,
954 ]
954 ]
955 category = 'warning' if resp.reason in warning_reasons else 'error'
955 category = 'warning' if resp.reason in warning_reasons else 'error'
956 h.flash(msg, category=category)
956 h.flash(msg, category=category)
957
957
958 @LoginRequired()
958 @LoginRequired()
959 @NotAnonymous()
959 @NotAnonymous()
960 @HasRepoPermissionAnyDecorator(
960 @HasRepoPermissionAnyDecorator(
961 'repository.read', 'repository.write', 'repository.admin')
961 'repository.read', 'repository.write', 'repository.admin')
962 @CSRFRequired()
962 @CSRFRequired()
963 @view_config(
963 @view_config(
964 route_name='pullrequest_merge', request_method='POST',
964 route_name='pullrequest_merge', request_method='POST',
965 renderer='json_ext')
965 renderer='json_ext')
966 def pull_request_merge(self):
966 def pull_request_merge(self):
967 """
967 """
968 Merge will perform a server-side merge of the specified
968 Merge will perform a server-side merge of the specified
969 pull request, if the pull request is approved and mergeable.
969 pull request, if the pull request is approved and mergeable.
970 After successful merging, the pull request is automatically
970 After successful merging, the pull request is automatically
971 closed, with a relevant comment.
971 closed, with a relevant comment.
972 """
972 """
973 pull_request = PullRequest.get_or_404(
973 pull_request = PullRequest.get_or_404(
974 self.request.matchdict['pull_request_id'])
974 self.request.matchdict['pull_request_id'])
975
975
976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
977 translator=self.request.translate)
977 translator=self.request.translate)
978 merge_possible = not check.failed
978 merge_possible = not check.failed
979
979
980 for err_type, error_msg in check.errors:
980 for err_type, error_msg in check.errors:
981 h.flash(error_msg, category=err_type)
981 h.flash(error_msg, category=err_type)
982
982
983 if merge_possible:
983 if merge_possible:
984 log.debug("Pre-conditions checked, trying to merge.")
984 log.debug("Pre-conditions checked, trying to merge.")
985 extras = vcs_operation_context(
985 extras = vcs_operation_context(
986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
987 username=self._rhodecode_db_user.username, action='push',
987 username=self._rhodecode_db_user.username, action='push',
988 scm=pull_request.target_repo.repo_type)
988 scm=pull_request.target_repo.repo_type)
989 self._merge_pull_request(
989 self._merge_pull_request(
990 pull_request, self._rhodecode_db_user, extras)
990 pull_request, self._rhodecode_db_user, extras)
991 else:
991 else:
992 log.debug("Pre-conditions failed, NOT merging.")
992 log.debug("Pre-conditions failed, NOT merging.")
993
993
994 raise HTTPFound(
994 raise HTTPFound(
995 h.route_path('pullrequest_show',
995 h.route_path('pullrequest_show',
996 repo_name=pull_request.target_repo.repo_name,
996 repo_name=pull_request.target_repo.repo_name,
997 pull_request_id=pull_request.pull_request_id))
997 pull_request_id=pull_request.pull_request_id))
998
998
999 def _merge_pull_request(self, pull_request, user, extras):
999 def _merge_pull_request(self, pull_request, user, extras):
1000 _ = self.request.translate
1000 _ = self.request.translate
1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1002
1002
1003 if merge_resp.executed:
1003 if merge_resp.executed:
1004 log.debug("The merge was successful, closing the pull request.")
1004 log.debug("The merge was successful, closing the pull request.")
1005 PullRequestModel().close_pull_request(
1005 PullRequestModel().close_pull_request(
1006 pull_request.pull_request_id, user)
1006 pull_request.pull_request_id, user)
1007 Session().commit()
1007 Session().commit()
1008 msg = _('Pull request was successfully merged and closed.')
1008 msg = _('Pull request was successfully merged and closed.')
1009 h.flash(msg, category='success')
1009 h.flash(msg, category='success')
1010 else:
1010 else:
1011 log.debug(
1011 log.debug(
1012 "The merge was not successful. Merge response: %s",
1012 "The merge was not successful. Merge response: %s",
1013 merge_resp)
1013 merge_resp)
1014 msg = PullRequestModel().merge_status_message(
1014 msg = PullRequestModel().merge_status_message(
1015 merge_resp.failure_reason)
1015 merge_resp.failure_reason)
1016 h.flash(msg, category='error')
1016 h.flash(msg, category='error')
1017
1017
1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1019 _ = self.request.translate
1019 _ = self.request.translate
1020 get_default_reviewers_data, validate_default_reviewers = \
1020 get_default_reviewers_data, validate_default_reviewers = \
1021 PullRequestModel().get_reviewer_functions()
1021 PullRequestModel().get_reviewer_functions()
1022
1022
1023 try:
1023 try:
1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1025 except ValueError as e:
1025 except ValueError as e:
1026 log.error('Reviewers Validation: {}'.format(e))
1026 log.error('Reviewers Validation: {}'.format(e))
1027 h.flash(e, category='error')
1027 h.flash(e, category='error')
1028 return
1028 return
1029
1029
1030 PullRequestModel().update_reviewers(
1030 PullRequestModel().update_reviewers(
1031 pull_request, reviewers, self._rhodecode_user)
1031 pull_request, reviewers, self._rhodecode_user)
1032 h.flash(_('Pull request reviewers updated.'), category='success')
1032 h.flash(_('Pull request reviewers updated.'), category='success')
1033 Session().commit()
1033 Session().commit()
1034
1034
1035 @LoginRequired()
1035 @LoginRequired()
1036 @NotAnonymous()
1036 @NotAnonymous()
1037 @HasRepoPermissionAnyDecorator(
1037 @HasRepoPermissionAnyDecorator(
1038 'repository.read', 'repository.write', 'repository.admin')
1038 'repository.read', 'repository.write', 'repository.admin')
1039 @CSRFRequired()
1039 @CSRFRequired()
1040 @view_config(
1040 @view_config(
1041 route_name='pullrequest_delete', request_method='POST',
1041 route_name='pullrequest_delete', request_method='POST',
1042 renderer='json_ext')
1042 renderer='json_ext')
1043 def pull_request_delete(self):
1043 def pull_request_delete(self):
1044 _ = self.request.translate
1044 _ = self.request.translate
1045
1045
1046 pull_request = PullRequest.get_or_404(
1046 pull_request = PullRequest.get_or_404(
1047 self.request.matchdict['pull_request_id'])
1047 self.request.matchdict['pull_request_id'])
1048
1048
1049 pr_closed = pull_request.is_closed()
1049 pr_closed = pull_request.is_closed()
1050 allowed_to_delete = PullRequestModel().check_user_delete(
1050 allowed_to_delete = PullRequestModel().check_user_delete(
1051 pull_request, self._rhodecode_user) and not pr_closed
1051 pull_request, self._rhodecode_user) and not pr_closed
1052
1052
1053 # only owner can delete it !
1053 # only owner can delete it !
1054 if allowed_to_delete:
1054 if allowed_to_delete:
1055 PullRequestModel().delete(pull_request, self._rhodecode_user)
1055 PullRequestModel().delete(pull_request, self._rhodecode_user)
1056 Session().commit()
1056 Session().commit()
1057 h.flash(_('Successfully deleted pull request'),
1057 h.flash(_('Successfully deleted pull request'),
1058 category='success')
1058 category='success')
1059 raise HTTPFound(h.route_path('pullrequest_show_all',
1059 raise HTTPFound(h.route_path('pullrequest_show_all',
1060 repo_name=self.db_repo_name))
1060 repo_name=self.db_repo_name))
1061
1061
1062 log.warning('user %s tried to delete pull request without access',
1062 log.warning('user %s tried to delete pull request without access',
1063 self._rhodecode_user)
1063 self._rhodecode_user)
1064 raise HTTPNotFound()
1064 raise HTTPNotFound()
1065
1065
1066 @LoginRequired()
1066 @LoginRequired()
1067 @NotAnonymous()
1067 @NotAnonymous()
1068 @HasRepoPermissionAnyDecorator(
1068 @HasRepoPermissionAnyDecorator(
1069 'repository.read', 'repository.write', 'repository.admin')
1069 'repository.read', 'repository.write', 'repository.admin')
1070 @CSRFRequired()
1070 @CSRFRequired()
1071 @view_config(
1071 @view_config(
1072 route_name='pullrequest_comment_create', request_method='POST',
1072 route_name='pullrequest_comment_create', request_method='POST',
1073 renderer='json_ext')
1073 renderer='json_ext')
1074 def pull_request_comment_create(self):
1074 def pull_request_comment_create(self):
1075 _ = self.request.translate
1075 _ = self.request.translate
1076
1076
1077 pull_request = PullRequest.get_or_404(
1077 pull_request = PullRequest.get_or_404(
1078 self.request.matchdict['pull_request_id'])
1078 self.request.matchdict['pull_request_id'])
1079 pull_request_id = pull_request.pull_request_id
1079 pull_request_id = pull_request.pull_request_id
1080
1080
1081 if pull_request.is_closed():
1081 if pull_request.is_closed():
1082 log.debug('comment: forbidden because pull request is closed')
1082 log.debug('comment: forbidden because pull request is closed')
1083 raise HTTPForbidden()
1083 raise HTTPForbidden()
1084
1084
1085 allowed_to_comment = PullRequestModel().check_user_comment(
1085 allowed_to_comment = PullRequestModel().check_user_comment(
1086 pull_request, self._rhodecode_user)
1086 pull_request, self._rhodecode_user)
1087 if not allowed_to_comment:
1087 if not allowed_to_comment:
1088 log.debug(
1088 log.debug(
1089 'comment: forbidden because pull request is from forbidden repo')
1089 'comment: forbidden because pull request is from forbidden repo')
1090 raise HTTPForbidden()
1090 raise HTTPForbidden()
1091
1091
1092 c = self.load_default_context()
1092 c = self.load_default_context()
1093
1093
1094 status = self.request.POST.get('changeset_status', None)
1094 status = self.request.POST.get('changeset_status', None)
1095 text = self.request.POST.get('text')
1095 text = self.request.POST.get('text')
1096 comment_type = self.request.POST.get('comment_type')
1096 comment_type = self.request.POST.get('comment_type')
1097 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1097 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1098 close_pull_request = self.request.POST.get('close_pull_request')
1098 close_pull_request = self.request.POST.get('close_pull_request')
1099
1099
1100 # the logic here should work like following, if we submit close
1100 # the logic here should work like following, if we submit close
1101 # pr comment, use `close_pull_request_with_comment` function
1101 # pr comment, use `close_pull_request_with_comment` function
1102 # else handle regular comment logic
1102 # else handle regular comment logic
1103
1103
1104 if close_pull_request:
1104 if close_pull_request:
1105 # only owner or admin or person with write permissions
1105 # only owner or admin or person with write permissions
1106 allowed_to_close = PullRequestModel().check_user_update(
1106 allowed_to_close = PullRequestModel().check_user_update(
1107 pull_request, self._rhodecode_user)
1107 pull_request, self._rhodecode_user)
1108 if not allowed_to_close:
1108 if not allowed_to_close:
1109 log.debug('comment: forbidden because not allowed to close '
1109 log.debug('comment: forbidden because not allowed to close '
1110 'pull request %s', pull_request_id)
1110 'pull request %s', pull_request_id)
1111 raise HTTPForbidden()
1111 raise HTTPForbidden()
1112 comment, status = PullRequestModel().close_pull_request_with_comment(
1112 comment, status = PullRequestModel().close_pull_request_with_comment(
1113 pull_request, self._rhodecode_user, self.db_repo, message=text)
1113 pull_request, self._rhodecode_user, self.db_repo, message=text)
1114 Session().flush()
1114 Session().flush()
1115 events.trigger(
1115 events.trigger(
1116 events.PullRequestCommentEvent(pull_request, comment))
1116 events.PullRequestCommentEvent(pull_request, comment))
1117
1117
1118 else:
1118 else:
1119 # regular comment case, could be inline, or one with status.
1119 # regular comment case, could be inline, or one with status.
1120 # for that one we check also permissions
1120 # for that one we check also permissions
1121
1121
1122 allowed_to_change_status = PullRequestModel().check_user_change_status(
1122 allowed_to_change_status = PullRequestModel().check_user_change_status(
1123 pull_request, self._rhodecode_user)
1123 pull_request, self._rhodecode_user)
1124
1124
1125 if status and allowed_to_change_status:
1125 if status and allowed_to_change_status:
1126 message = (_('Status change %(transition_icon)s %(status)s')
1126 message = (_('Status change %(transition_icon)s %(status)s')
1127 % {'transition_icon': '>',
1127 % {'transition_icon': '>',
1128 'status': ChangesetStatus.get_status_lbl(status)})
1128 'status': ChangesetStatus.get_status_lbl(status)})
1129 text = text or message
1129 text = text or message
1130
1130
1131 comment = CommentsModel().create(
1131 comment = CommentsModel().create(
1132 text=text,
1132 text=text,
1133 repo=self.db_repo.repo_id,
1133 repo=self.db_repo.repo_id,
1134 user=self._rhodecode_user.user_id,
1134 user=self._rhodecode_user.user_id,
1135 pull_request=pull_request,
1135 pull_request=pull_request,
1136 f_path=self.request.POST.get('f_path'),
1136 f_path=self.request.POST.get('f_path'),
1137 line_no=self.request.POST.get('line'),
1137 line_no=self.request.POST.get('line'),
1138 status_change=(ChangesetStatus.get_status_lbl(status)
1138 status_change=(ChangesetStatus.get_status_lbl(status)
1139 if status and allowed_to_change_status else None),
1139 if status and allowed_to_change_status else None),
1140 status_change_type=(status
1140 status_change_type=(status
1141 if status and allowed_to_change_status else None),
1141 if status and allowed_to_change_status else None),
1142 comment_type=comment_type,
1142 comment_type=comment_type,
1143 resolves_comment_id=resolves_comment_id
1143 resolves_comment_id=resolves_comment_id
1144 )
1144 )
1145
1145
1146 if allowed_to_change_status:
1146 if allowed_to_change_status:
1147 # calculate old status before we change it
1147 # calculate old status before we change it
1148 old_calculated_status = pull_request.calculated_review_status()
1148 old_calculated_status = pull_request.calculated_review_status()
1149
1149
1150 # get status if set !
1150 # get status if set !
1151 if status:
1151 if status:
1152 ChangesetStatusModel().set_status(
1152 ChangesetStatusModel().set_status(
1153 self.db_repo.repo_id,
1153 self.db_repo.repo_id,
1154 status,
1154 status,
1155 self._rhodecode_user.user_id,
1155 self._rhodecode_user.user_id,
1156 comment,
1156 comment,
1157 pull_request=pull_request
1157 pull_request=pull_request
1158 )
1158 )
1159
1159
1160 Session().flush()
1160 Session().flush()
1161 events.trigger(
1161 events.trigger(
1162 events.PullRequestCommentEvent(pull_request, comment))
1162 events.PullRequestCommentEvent(pull_request, comment))
1163
1163
1164 # we now calculate the status of pull request, and based on that
1164 # we now calculate the status of pull request, and based on that
1165 # calculation we set the commits status
1165 # calculation we set the commits status
1166 calculated_status = pull_request.calculated_review_status()
1166 calculated_status = pull_request.calculated_review_status()
1167 if old_calculated_status != calculated_status:
1167 if old_calculated_status != calculated_status:
1168 PullRequestModel()._trigger_pull_request_hook(
1168 PullRequestModel()._trigger_pull_request_hook(
1169 pull_request, self._rhodecode_user, 'review_status_change')
1169 pull_request, self._rhodecode_user, 'review_status_change')
1170
1170
1171 Session().commit()
1171 Session().commit()
1172
1172
1173 data = {
1173 data = {
1174 'target_id': h.safeid(h.safe_unicode(
1174 'target_id': h.safeid(h.safe_unicode(
1175 self.request.POST.get('f_path'))),
1175 self.request.POST.get('f_path'))),
1176 }
1176 }
1177 if comment:
1177 if comment:
1178 c.co = comment
1178 c.co = comment
1179 rendered_comment = render(
1179 rendered_comment = render(
1180 'rhodecode:templates/changeset/changeset_comment_block.mako',
1180 'rhodecode:templates/changeset/changeset_comment_block.mako',
1181 self._get_template_context(c), self.request)
1181 self._get_template_context(c), self.request)
1182
1182
1183 data.update(comment.get_dict())
1183 data.update(comment.get_dict())
1184 data.update({'rendered_text': rendered_comment})
1184 data.update({'rendered_text': rendered_comment})
1185
1185
1186 return data
1186 return data
1187
1187
1188 @LoginRequired()
1188 @LoginRequired()
1189 @NotAnonymous()
1189 @NotAnonymous()
1190 @HasRepoPermissionAnyDecorator(
1190 @HasRepoPermissionAnyDecorator(
1191 'repository.read', 'repository.write', 'repository.admin')
1191 'repository.read', 'repository.write', 'repository.admin')
1192 @CSRFRequired()
1192 @CSRFRequired()
1193 @view_config(
1193 @view_config(
1194 route_name='pullrequest_comment_delete', request_method='POST',
1194 route_name='pullrequest_comment_delete', request_method='POST',
1195 renderer='json_ext')
1195 renderer='json_ext')
1196 def pull_request_comment_delete(self):
1196 def pull_request_comment_delete(self):
1197 pull_request = PullRequest.get_or_404(
1197 pull_request = PullRequest.get_or_404(
1198 self.request.matchdict['pull_request_id'])
1198 self.request.matchdict['pull_request_id'])
1199
1199
1200 comment = ChangesetComment.get_or_404(
1200 comment = ChangesetComment.get_or_404(
1201 self.request.matchdict['comment_id'])
1201 self.request.matchdict['comment_id'])
1202 comment_id = comment.comment_id
1202 comment_id = comment.comment_id
1203
1203
1204 if pull_request.is_closed():
1204 if pull_request.is_closed():
1205 log.debug('comment: forbidden because pull request is closed')
1205 log.debug('comment: forbidden because pull request is closed')
1206 raise HTTPForbidden()
1206 raise HTTPForbidden()
1207
1207
1208 if not comment:
1208 if not comment:
1209 log.debug('Comment with id:%s not found, skipping', comment_id)
1209 log.debug('Comment with id:%s not found, skipping', comment_id)
1210 # comment already deleted in another call probably
1210 # comment already deleted in another call probably
1211 return True
1211 return True
1212
1212
1213 if comment.pull_request.is_closed():
1213 if comment.pull_request.is_closed():
1214 # don't allow deleting comments on closed pull request
1214 # don't allow deleting comments on closed pull request
1215 raise HTTPForbidden()
1215 raise HTTPForbidden()
1216
1216
1217 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1217 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1218 super_admin = h.HasPermissionAny('hg.admin')()
1218 super_admin = h.HasPermissionAny('hg.admin')()
1219 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1219 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1220 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1220 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1221 comment_repo_admin = is_repo_admin and is_repo_comment
1221 comment_repo_admin = is_repo_admin and is_repo_comment
1222
1222
1223 if super_admin or comment_owner or comment_repo_admin:
1223 if super_admin or comment_owner or comment_repo_admin:
1224 old_calculated_status = comment.pull_request.calculated_review_status()
1224 old_calculated_status = comment.pull_request.calculated_review_status()
1225 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1225 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1226 Session().commit()
1226 Session().commit()
1227 calculated_status = comment.pull_request.calculated_review_status()
1227 calculated_status = comment.pull_request.calculated_review_status()
1228 if old_calculated_status != calculated_status:
1228 if old_calculated_status != calculated_status:
1229 PullRequestModel()._trigger_pull_request_hook(
1229 PullRequestModel()._trigger_pull_request_hook(
1230 comment.pull_request, self._rhodecode_user, 'review_status_change')
1230 comment.pull_request, self._rhodecode_user, 'review_status_change')
1231 return True
1231 return True
1232 else:
1232 else:
1233 log.warning('No permissions for user %s to delete comment_id: %s',
1233 log.warning('No permissions for user %s to delete comment_id: %s',
1234 self._rhodecode_db_user, comment_id)
1234 self._rhodecode_db_user, comment_id)
1235 raise HTTPNotFound()
1235 raise HTTPNotFound()
@@ -1,2101 +1,2102 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 from collections import OrderedDict
39 from collections import OrderedDict
40
40
41 import pygments
41 import pygments
42 import itertools
42 import itertools
43 import fnmatch
43 import fnmatch
44
44
45 from datetime import datetime
45 from datetime import datetime
46 from functools import partial
46 from functools import partial
47 from pygments.formatters.html import HtmlFormatter
47 from pygments.formatters.html import HtmlFormatter
48 from pygments import highlight as code_highlight
48 from pygments import highlight as code_highlight
49 from pygments.lexers import (
49 from pygments.lexers import (
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51
51
52 from pyramid.threadlocal import get_current_request
52 from pyramid.threadlocal import get_current_request
53
53
54 from webhelpers.html import literal, HTML, escape
54 from webhelpers.html import literal, HTML, escape
55 from webhelpers.html.tools import *
55 from webhelpers.html.tools import *
56 from webhelpers.html.builder import make_tag
56 from webhelpers.html.builder import make_tag
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 submit, text, password, textarea, title, ul, xml_declaration, radio
60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
65 replace_whitespace, urlify, truncate, wrap_paragraphs
65 replace_whitespace, urlify, truncate, wrap_paragraphs
66 from webhelpers.date import time_ago_in_words
66 from webhelpers.date import time_ago_in_words
67 from webhelpers.paginate import Page as _Page
67 from webhelpers.paginate import Page as _Page
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
69 convert_boolean_attrs, NotGiven, _make_safe_id_component
70 from webhelpers2.number import format_byte_size
70 from webhelpers2.number import format_byte_size
71
71
72 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.action_parser import action_parser
73 from rhodecode.lib.ext_json import json
73 from rhodecode.lib.ext_json import json
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
77 AttributeDict, safe_int, md5, md5_safe
77 AttributeDict, safe_int, md5, md5_safe
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
82 from rhodecode.model.changeset_status import ChangesetStatusModel
82 from rhodecode.model.changeset_status import ChangesetStatusModel
83 from rhodecode.model.db import Permission, User, Repository
83 from rhodecode.model.db import Permission, User, Repository
84 from rhodecode.model.repo_group import RepoGroupModel
84 from rhodecode.model.repo_group import RepoGroupModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
85 from rhodecode.model.settings import IssueTrackerSettingsModel
86
86
87 log = logging.getLogger(__name__)
87 log = logging.getLogger(__name__)
88
88
89
89
90 DEFAULT_USER = User.DEFAULT_USER
90 DEFAULT_USER = User.DEFAULT_USER
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
92
92
93
93
94 def url(*args, **kw):
94 def url(*args, **kw):
95 from pylons import url as pylons_url
95 from pylons import url as pylons_url
96 return pylons_url(*args, **kw)
96 return pylons_url(*args, **kw)
97
97
98
98
99 def asset(path, ver=None, **kwargs):
99 def asset(path, ver=None, **kwargs):
100 """
100 """
101 Helper to generate a static asset file path for rhodecode assets
101 Helper to generate a static asset file path for rhodecode assets
102
102
103 eg. h.asset('images/image.png', ver='3923')
103 eg. h.asset('images/image.png', ver='3923')
104
104
105 :param path: path of asset
105 :param path: path of asset
106 :param ver: optional version query param to append as ?ver=
106 :param ver: optional version query param to append as ?ver=
107 """
107 """
108 request = get_current_request()
108 request = get_current_request()
109 query = {}
109 query = {}
110 query.update(kwargs)
110 query.update(kwargs)
111 if ver:
111 if ver:
112 query = {'ver': ver}
112 query = {'ver': ver}
113 return request.static_path(
113 return request.static_path(
114 'rhodecode:public/{}'.format(path), _query=query)
114 'rhodecode:public/{}'.format(path), _query=query)
115
115
116
116
117 default_html_escape_table = {
117 default_html_escape_table = {
118 ord('&'): u'&amp;',
118 ord('&'): u'&amp;',
119 ord('<'): u'&lt;',
119 ord('<'): u'&lt;',
120 ord('>'): u'&gt;',
120 ord('>'): u'&gt;',
121 ord('"'): u'&quot;',
121 ord('"'): u'&quot;',
122 ord("'"): u'&#39;',
122 ord("'"): u'&#39;',
123 }
123 }
124
124
125
125
126 def html_escape(text, html_escape_table=default_html_escape_table):
126 def html_escape(text, html_escape_table=default_html_escape_table):
127 """Produce entities within text."""
127 """Produce entities within text."""
128 return text.translate(html_escape_table)
128 return text.translate(html_escape_table)
129
129
130
130
131 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
131 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
132 """
132 """
133 Truncate string ``s`` at the first occurrence of ``sub``.
133 Truncate string ``s`` at the first occurrence of ``sub``.
134
134
135 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
135 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
136 """
136 """
137 suffix_if_chopped = suffix_if_chopped or ''
137 suffix_if_chopped = suffix_if_chopped or ''
138 pos = s.find(sub)
138 pos = s.find(sub)
139 if pos == -1:
139 if pos == -1:
140 return s
140 return s
141
141
142 if inclusive:
142 if inclusive:
143 pos += len(sub)
143 pos += len(sub)
144
144
145 chopped = s[:pos]
145 chopped = s[:pos]
146 left = s[pos:].strip()
146 left = s[pos:].strip()
147
147
148 if left and suffix_if_chopped:
148 if left and suffix_if_chopped:
149 chopped += suffix_if_chopped
149 chopped += suffix_if_chopped
150
150
151 return chopped
151 return chopped
152
152
153
153
154 def shorter(text, size=20):
154 def shorter(text, size=20):
155 postfix = '...'
155 postfix = '...'
156 if len(text) > size:
156 if len(text) > size:
157 return text[:size - len(postfix)] + postfix
157 return text[:size - len(postfix)] + postfix
158 return text
158 return text
159
159
160
160
161 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
161 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
162 """
162 """
163 Reset button
163 Reset button
164 """
164 """
165 _set_input_attrs(attrs, type, name, value)
165 _set_input_attrs(attrs, type, name, value)
166 _set_id_attr(attrs, id, name)
166 _set_id_attr(attrs, id, name)
167 convert_boolean_attrs(attrs, ["disabled"])
167 convert_boolean_attrs(attrs, ["disabled"])
168 return HTML.input(**attrs)
168 return HTML.input(**attrs)
169
169
170 reset = _reset
170 reset = _reset
171 safeid = _make_safe_id_component
171 safeid = _make_safe_id_component
172
172
173
173
174 def branding(name, length=40):
174 def branding(name, length=40):
175 return truncate(name, length, indicator="")
175 return truncate(name, length, indicator="")
176
176
177
177
178 def FID(raw_id, path):
178 def FID(raw_id, path):
179 """
179 """
180 Creates a unique ID for filenode based on it's hash of path and commit
180 Creates a unique ID for filenode based on it's hash of path and commit
181 it's safe to use in urls
181 it's safe to use in urls
182
182
183 :param raw_id:
183 :param raw_id:
184 :param path:
184 :param path:
185 """
185 """
186
186
187 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
187 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
188
188
189
189
190 class _GetError(object):
190 class _GetError(object):
191 """Get error from form_errors, and represent it as span wrapped error
191 """Get error from form_errors, and represent it as span wrapped error
192 message
192 message
193
193
194 :param field_name: field to fetch errors for
194 :param field_name: field to fetch errors for
195 :param form_errors: form errors dict
195 :param form_errors: form errors dict
196 """
196 """
197
197
198 def __call__(self, field_name, form_errors):
198 def __call__(self, field_name, form_errors):
199 tmpl = """<span class="error_msg">%s</span>"""
199 tmpl = """<span class="error_msg">%s</span>"""
200 if form_errors and field_name in form_errors:
200 if form_errors and field_name in form_errors:
201 return literal(tmpl % form_errors.get(field_name))
201 return literal(tmpl % form_errors.get(field_name))
202
202
203 get_error = _GetError()
203 get_error = _GetError()
204
204
205
205
206 class _ToolTip(object):
206 class _ToolTip(object):
207
207
208 def __call__(self, tooltip_title, trim_at=50):
208 def __call__(self, tooltip_title, trim_at=50):
209 """
209 """
210 Special function just to wrap our text into nice formatted
210 Special function just to wrap our text into nice formatted
211 autowrapped text
211 autowrapped text
212
212
213 :param tooltip_title:
213 :param tooltip_title:
214 """
214 """
215 tooltip_title = escape(tooltip_title)
215 tooltip_title = escape(tooltip_title)
216 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
216 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
217 return tooltip_title
217 return tooltip_title
218 tooltip = _ToolTip()
218 tooltip = _ToolTip()
219
219
220
220
221 def files_breadcrumbs(repo_name, commit_id, file_path):
221 def files_breadcrumbs(repo_name, commit_id, file_path):
222 if isinstance(file_path, str):
222 if isinstance(file_path, str):
223 file_path = safe_unicode(file_path)
223 file_path = safe_unicode(file_path)
224
224
225 # TODO: johbo: Is this always a url like path, or is this operating
225 # TODO: johbo: Is this always a url like path, or is this operating
226 # system dependent?
226 # system dependent?
227 path_segments = file_path.split('/')
227 path_segments = file_path.split('/')
228
228
229 repo_name_html = escape(repo_name)
229 repo_name_html = escape(repo_name)
230 if len(path_segments) == 1 and path_segments[0] == '':
230 if len(path_segments) == 1 and path_segments[0] == '':
231 url_segments = [repo_name_html]
231 url_segments = [repo_name_html]
232 else:
232 else:
233 url_segments = [
233 url_segments = [
234 link_to(
234 link_to(
235 repo_name_html,
235 repo_name_html,
236 route_path(
236 route_path(
237 'repo_files',
237 'repo_files',
238 repo_name=repo_name,
238 repo_name=repo_name,
239 commit_id=commit_id,
239 commit_id=commit_id,
240 f_path=''),
240 f_path=''),
241 class_='pjax-link')]
241 class_='pjax-link')]
242
242
243 last_cnt = len(path_segments) - 1
243 last_cnt = len(path_segments) - 1
244 for cnt, segment in enumerate(path_segments):
244 for cnt, segment in enumerate(path_segments):
245 if not segment:
245 if not segment:
246 continue
246 continue
247 segment_html = escape(segment)
247 segment_html = escape(segment)
248
248
249 if cnt != last_cnt:
249 if cnt != last_cnt:
250 url_segments.append(
250 url_segments.append(
251 link_to(
251 link_to(
252 segment_html,
252 segment_html,
253 route_path(
253 route_path(
254 'repo_files',
254 'repo_files',
255 repo_name=repo_name,
255 repo_name=repo_name,
256 commit_id=commit_id,
256 commit_id=commit_id,
257 f_path='/'.join(path_segments[:cnt + 1])),
257 f_path='/'.join(path_segments[:cnt + 1])),
258 class_='pjax-link'))
258 class_='pjax-link'))
259 else:
259 else:
260 url_segments.append(segment_html)
260 url_segments.append(segment_html)
261
261
262 return literal('/'.join(url_segments))
262 return literal('/'.join(url_segments))
263
263
264
264
265 class CodeHtmlFormatter(HtmlFormatter):
265 class CodeHtmlFormatter(HtmlFormatter):
266 """
266 """
267 My code Html Formatter for source codes
267 My code Html Formatter for source codes
268 """
268 """
269
269
270 def wrap(self, source, outfile):
270 def wrap(self, source, outfile):
271 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
271 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
272
272
273 def _wrap_code(self, source):
273 def _wrap_code(self, source):
274 for cnt, it in enumerate(source):
274 for cnt, it in enumerate(source):
275 i, t = it
275 i, t = it
276 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
276 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
277 yield i, t
277 yield i, t
278
278
279 def _wrap_tablelinenos(self, inner):
279 def _wrap_tablelinenos(self, inner):
280 dummyoutfile = StringIO.StringIO()
280 dummyoutfile = StringIO.StringIO()
281 lncount = 0
281 lncount = 0
282 for t, line in inner:
282 for t, line in inner:
283 if t:
283 if t:
284 lncount += 1
284 lncount += 1
285 dummyoutfile.write(line)
285 dummyoutfile.write(line)
286
286
287 fl = self.linenostart
287 fl = self.linenostart
288 mw = len(str(lncount + fl - 1))
288 mw = len(str(lncount + fl - 1))
289 sp = self.linenospecial
289 sp = self.linenospecial
290 st = self.linenostep
290 st = self.linenostep
291 la = self.lineanchors
291 la = self.lineanchors
292 aln = self.anchorlinenos
292 aln = self.anchorlinenos
293 nocls = self.noclasses
293 nocls = self.noclasses
294 if sp:
294 if sp:
295 lines = []
295 lines = []
296
296
297 for i in range(fl, fl + lncount):
297 for i in range(fl, fl + lncount):
298 if i % st == 0:
298 if i % st == 0:
299 if i % sp == 0:
299 if i % sp == 0:
300 if aln:
300 if aln:
301 lines.append('<a href="#%s%d" class="special">%*d</a>' %
301 lines.append('<a href="#%s%d" class="special">%*d</a>' %
302 (la, i, mw, i))
302 (la, i, mw, i))
303 else:
303 else:
304 lines.append('<span class="special">%*d</span>' % (mw, i))
304 lines.append('<span class="special">%*d</span>' % (mw, i))
305 else:
305 else:
306 if aln:
306 if aln:
307 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
307 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
308 else:
308 else:
309 lines.append('%*d' % (mw, i))
309 lines.append('%*d' % (mw, i))
310 else:
310 else:
311 lines.append('')
311 lines.append('')
312 ls = '\n'.join(lines)
312 ls = '\n'.join(lines)
313 else:
313 else:
314 lines = []
314 lines = []
315 for i in range(fl, fl + lncount):
315 for i in range(fl, fl + lncount):
316 if i % st == 0:
316 if i % st == 0:
317 if aln:
317 if aln:
318 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
318 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
319 else:
319 else:
320 lines.append('%*d' % (mw, i))
320 lines.append('%*d' % (mw, i))
321 else:
321 else:
322 lines.append('')
322 lines.append('')
323 ls = '\n'.join(lines)
323 ls = '\n'.join(lines)
324
324
325 # in case you wonder about the seemingly redundant <div> here: since the
325 # in case you wonder about the seemingly redundant <div> here: since the
326 # content in the other cell also is wrapped in a div, some browsers in
326 # content in the other cell also is wrapped in a div, some browsers in
327 # some configurations seem to mess up the formatting...
327 # some configurations seem to mess up the formatting...
328 if nocls:
328 if nocls:
329 yield 0, ('<table class="%stable">' % self.cssclass +
329 yield 0, ('<table class="%stable">' % self.cssclass +
330 '<tr><td><div class="linenodiv" '
330 '<tr><td><div class="linenodiv" '
331 'style="background-color: #f0f0f0; padding-right: 10px">'
331 'style="background-color: #f0f0f0; padding-right: 10px">'
332 '<pre style="line-height: 125%">' +
332 '<pre style="line-height: 125%">' +
333 ls + '</pre></div></td><td id="hlcode" class="code">')
333 ls + '</pre></div></td><td id="hlcode" class="code">')
334 else:
334 else:
335 yield 0, ('<table class="%stable">' % self.cssclass +
335 yield 0, ('<table class="%stable">' % self.cssclass +
336 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
336 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
337 ls + '</pre></div></td><td id="hlcode" class="code">')
337 ls + '</pre></div></td><td id="hlcode" class="code">')
338 yield 0, dummyoutfile.getvalue()
338 yield 0, dummyoutfile.getvalue()
339 yield 0, '</td></tr></table>'
339 yield 0, '</td></tr></table>'
340
340
341
341
342 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
342 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
343 def __init__(self, **kw):
343 def __init__(self, **kw):
344 # only show these line numbers if set
344 # only show these line numbers if set
345 self.only_lines = kw.pop('only_line_numbers', [])
345 self.only_lines = kw.pop('only_line_numbers', [])
346 self.query_terms = kw.pop('query_terms', [])
346 self.query_terms = kw.pop('query_terms', [])
347 self.max_lines = kw.pop('max_lines', 5)
347 self.max_lines = kw.pop('max_lines', 5)
348 self.line_context = kw.pop('line_context', 3)
348 self.line_context = kw.pop('line_context', 3)
349 self.url = kw.pop('url', None)
349 self.url = kw.pop('url', None)
350
350
351 super(CodeHtmlFormatter, self).__init__(**kw)
351 super(CodeHtmlFormatter, self).__init__(**kw)
352
352
353 def _wrap_code(self, source):
353 def _wrap_code(self, source):
354 for cnt, it in enumerate(source):
354 for cnt, it in enumerate(source):
355 i, t = it
355 i, t = it
356 t = '<pre>%s</pre>' % t
356 t = '<pre>%s</pre>' % t
357 yield i, t
357 yield i, t
358
358
359 def _wrap_tablelinenos(self, inner):
359 def _wrap_tablelinenos(self, inner):
360 yield 0, '<table class="code-highlight %stable">' % self.cssclass
360 yield 0, '<table class="code-highlight %stable">' % self.cssclass
361
361
362 last_shown_line_number = 0
362 last_shown_line_number = 0
363 current_line_number = 1
363 current_line_number = 1
364
364
365 for t, line in inner:
365 for t, line in inner:
366 if not t:
366 if not t:
367 yield t, line
367 yield t, line
368 continue
368 continue
369
369
370 if current_line_number in self.only_lines:
370 if current_line_number in self.only_lines:
371 if last_shown_line_number + 1 != current_line_number:
371 if last_shown_line_number + 1 != current_line_number:
372 yield 0, '<tr>'
372 yield 0, '<tr>'
373 yield 0, '<td class="line">...</td>'
373 yield 0, '<td class="line">...</td>'
374 yield 0, '<td id="hlcode" class="code"></td>'
374 yield 0, '<td id="hlcode" class="code"></td>'
375 yield 0, '</tr>'
375 yield 0, '</tr>'
376
376
377 yield 0, '<tr>'
377 yield 0, '<tr>'
378 if self.url:
378 if self.url:
379 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
379 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
380 self.url, current_line_number, current_line_number)
380 self.url, current_line_number, current_line_number)
381 else:
381 else:
382 yield 0, '<td class="line"><a href="">%i</a></td>' % (
382 yield 0, '<td class="line"><a href="">%i</a></td>' % (
383 current_line_number)
383 current_line_number)
384 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
384 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
385 yield 0, '</tr>'
385 yield 0, '</tr>'
386
386
387 last_shown_line_number = current_line_number
387 last_shown_line_number = current_line_number
388
388
389 current_line_number += 1
389 current_line_number += 1
390
390
391
391
392 yield 0, '</table>'
392 yield 0, '</table>'
393
393
394
394
395 def extract_phrases(text_query):
395 def extract_phrases(text_query):
396 """
396 """
397 Extracts phrases from search term string making sure phrases
397 Extracts phrases from search term string making sure phrases
398 contained in double quotes are kept together - and discarding empty values
398 contained in double quotes are kept together - and discarding empty values
399 or fully whitespace values eg.
399 or fully whitespace values eg.
400
400
401 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
401 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
402
402
403 """
403 """
404
404
405 in_phrase = False
405 in_phrase = False
406 buf = ''
406 buf = ''
407 phrases = []
407 phrases = []
408 for char in text_query:
408 for char in text_query:
409 if in_phrase:
409 if in_phrase:
410 if char == '"': # end phrase
410 if char == '"': # end phrase
411 phrases.append(buf)
411 phrases.append(buf)
412 buf = ''
412 buf = ''
413 in_phrase = False
413 in_phrase = False
414 continue
414 continue
415 else:
415 else:
416 buf += char
416 buf += char
417 continue
417 continue
418 else:
418 else:
419 if char == '"': # start phrase
419 if char == '"': # start phrase
420 in_phrase = True
420 in_phrase = True
421 phrases.append(buf)
421 phrases.append(buf)
422 buf = ''
422 buf = ''
423 continue
423 continue
424 elif char == ' ':
424 elif char == ' ':
425 phrases.append(buf)
425 phrases.append(buf)
426 buf = ''
426 buf = ''
427 continue
427 continue
428 else:
428 else:
429 buf += char
429 buf += char
430
430
431 phrases.append(buf)
431 phrases.append(buf)
432 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
432 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
433 return phrases
433 return phrases
434
434
435
435
436 def get_matching_offsets(text, phrases):
436 def get_matching_offsets(text, phrases):
437 """
437 """
438 Returns a list of string offsets in `text` that the list of `terms` match
438 Returns a list of string offsets in `text` that the list of `terms` match
439
439
440 >>> get_matching_offsets('some text here', ['some', 'here'])
440 >>> get_matching_offsets('some text here', ['some', 'here'])
441 [(0, 4), (10, 14)]
441 [(0, 4), (10, 14)]
442
442
443 """
443 """
444 offsets = []
444 offsets = []
445 for phrase in phrases:
445 for phrase in phrases:
446 for match in re.finditer(phrase, text):
446 for match in re.finditer(phrase, text):
447 offsets.append((match.start(), match.end()))
447 offsets.append((match.start(), match.end()))
448
448
449 return offsets
449 return offsets
450
450
451
451
452 def normalize_text_for_matching(x):
452 def normalize_text_for_matching(x):
453 """
453 """
454 Replaces all non alnum characters to spaces and lower cases the string,
454 Replaces all non alnum characters to spaces and lower cases the string,
455 useful for comparing two text strings without punctuation
455 useful for comparing two text strings without punctuation
456 """
456 """
457 return re.sub(r'[^\w]', ' ', x.lower())
457 return re.sub(r'[^\w]', ' ', x.lower())
458
458
459
459
460 def get_matching_line_offsets(lines, terms):
460 def get_matching_line_offsets(lines, terms):
461 """ Return a set of `lines` indices (starting from 1) matching a
461 """ Return a set of `lines` indices (starting from 1) matching a
462 text search query, along with `context` lines above/below matching lines
462 text search query, along with `context` lines above/below matching lines
463
463
464 :param lines: list of strings representing lines
464 :param lines: list of strings representing lines
465 :param terms: search term string to match in lines eg. 'some text'
465 :param terms: search term string to match in lines eg. 'some text'
466 :param context: number of lines above/below a matching line to add to result
466 :param context: number of lines above/below a matching line to add to result
467 :param max_lines: cut off for lines of interest
467 :param max_lines: cut off for lines of interest
468 eg.
468 eg.
469
469
470 text = '''
470 text = '''
471 words words words
471 words words words
472 words words words
472 words words words
473 some text some
473 some text some
474 words words words
474 words words words
475 words words words
475 words words words
476 text here what
476 text here what
477 '''
477 '''
478 get_matching_line_offsets(text, 'text', context=1)
478 get_matching_line_offsets(text, 'text', context=1)
479 {3: [(5, 9)], 6: [(0, 4)]]
479 {3: [(5, 9)], 6: [(0, 4)]]
480
480
481 """
481 """
482 matching_lines = {}
482 matching_lines = {}
483 phrases = [normalize_text_for_matching(phrase)
483 phrases = [normalize_text_for_matching(phrase)
484 for phrase in extract_phrases(terms)]
484 for phrase in extract_phrases(terms)]
485
485
486 for line_index, line in enumerate(lines, start=1):
486 for line_index, line in enumerate(lines, start=1):
487 match_offsets = get_matching_offsets(
487 match_offsets = get_matching_offsets(
488 normalize_text_for_matching(line), phrases)
488 normalize_text_for_matching(line), phrases)
489 if match_offsets:
489 if match_offsets:
490 matching_lines[line_index] = match_offsets
490 matching_lines[line_index] = match_offsets
491
491
492 return matching_lines
492 return matching_lines
493
493
494
494
495 def hsv_to_rgb(h, s, v):
495 def hsv_to_rgb(h, s, v):
496 """ Convert hsv color values to rgb """
496 """ Convert hsv color values to rgb """
497
497
498 if s == 0.0:
498 if s == 0.0:
499 return v, v, v
499 return v, v, v
500 i = int(h * 6.0) # XXX assume int() truncates!
500 i = int(h * 6.0) # XXX assume int() truncates!
501 f = (h * 6.0) - i
501 f = (h * 6.0) - i
502 p = v * (1.0 - s)
502 p = v * (1.0 - s)
503 q = v * (1.0 - s * f)
503 q = v * (1.0 - s * f)
504 t = v * (1.0 - s * (1.0 - f))
504 t = v * (1.0 - s * (1.0 - f))
505 i = i % 6
505 i = i % 6
506 if i == 0:
506 if i == 0:
507 return v, t, p
507 return v, t, p
508 if i == 1:
508 if i == 1:
509 return q, v, p
509 return q, v, p
510 if i == 2:
510 if i == 2:
511 return p, v, t
511 return p, v, t
512 if i == 3:
512 if i == 3:
513 return p, q, v
513 return p, q, v
514 if i == 4:
514 if i == 4:
515 return t, p, v
515 return t, p, v
516 if i == 5:
516 if i == 5:
517 return v, p, q
517 return v, p, q
518
518
519
519
520 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
520 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
521 """
521 """
522 Generator for getting n of evenly distributed colors using
522 Generator for getting n of evenly distributed colors using
523 hsv color and golden ratio. It always return same order of colors
523 hsv color and golden ratio. It always return same order of colors
524
524
525 :param n: number of colors to generate
525 :param n: number of colors to generate
526 :param saturation: saturation of returned colors
526 :param saturation: saturation of returned colors
527 :param lightness: lightness of returned colors
527 :param lightness: lightness of returned colors
528 :returns: RGB tuple
528 :returns: RGB tuple
529 """
529 """
530
530
531 golden_ratio = 0.618033988749895
531 golden_ratio = 0.618033988749895
532 h = 0.22717784590367374
532 h = 0.22717784590367374
533
533
534 for _ in xrange(n):
534 for _ in xrange(n):
535 h += golden_ratio
535 h += golden_ratio
536 h %= 1
536 h %= 1
537 HSV_tuple = [h, saturation, lightness]
537 HSV_tuple = [h, saturation, lightness]
538 RGB_tuple = hsv_to_rgb(*HSV_tuple)
538 RGB_tuple = hsv_to_rgb(*HSV_tuple)
539 yield map(lambda x: str(int(x * 256)), RGB_tuple)
539 yield map(lambda x: str(int(x * 256)), RGB_tuple)
540
540
541
541
542 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
542 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
543 """
543 """
544 Returns a function which when called with an argument returns a unique
544 Returns a function which when called with an argument returns a unique
545 color for that argument, eg.
545 color for that argument, eg.
546
546
547 :param n: number of colors to generate
547 :param n: number of colors to generate
548 :param saturation: saturation of returned colors
548 :param saturation: saturation of returned colors
549 :param lightness: lightness of returned colors
549 :param lightness: lightness of returned colors
550 :returns: css RGB string
550 :returns: css RGB string
551
551
552 >>> color_hash = color_hasher()
552 >>> color_hash = color_hasher()
553 >>> color_hash('hello')
553 >>> color_hash('hello')
554 'rgb(34, 12, 59)'
554 'rgb(34, 12, 59)'
555 >>> color_hash('hello')
555 >>> color_hash('hello')
556 'rgb(34, 12, 59)'
556 'rgb(34, 12, 59)'
557 >>> color_hash('other')
557 >>> color_hash('other')
558 'rgb(90, 224, 159)'
558 'rgb(90, 224, 159)'
559 """
559 """
560
560
561 color_dict = {}
561 color_dict = {}
562 cgenerator = unique_color_generator(
562 cgenerator = unique_color_generator(
563 saturation=saturation, lightness=lightness)
563 saturation=saturation, lightness=lightness)
564
564
565 def get_color_string(thing):
565 def get_color_string(thing):
566 if thing in color_dict:
566 if thing in color_dict:
567 col = color_dict[thing]
567 col = color_dict[thing]
568 else:
568 else:
569 col = color_dict[thing] = cgenerator.next()
569 col = color_dict[thing] = cgenerator.next()
570 return "rgb(%s)" % (', '.join(col))
570 return "rgb(%s)" % (', '.join(col))
571
571
572 return get_color_string
572 return get_color_string
573
573
574
574
575 def get_lexer_safe(mimetype=None, filepath=None):
575 def get_lexer_safe(mimetype=None, filepath=None):
576 """
576 """
577 Tries to return a relevant pygments lexer using mimetype/filepath name,
577 Tries to return a relevant pygments lexer using mimetype/filepath name,
578 defaulting to plain text if none could be found
578 defaulting to plain text if none could be found
579 """
579 """
580 lexer = None
580 lexer = None
581 try:
581 try:
582 if mimetype:
582 if mimetype:
583 lexer = get_lexer_for_mimetype(mimetype)
583 lexer = get_lexer_for_mimetype(mimetype)
584 if not lexer:
584 if not lexer:
585 lexer = get_lexer_for_filename(filepath)
585 lexer = get_lexer_for_filename(filepath)
586 except pygments.util.ClassNotFound:
586 except pygments.util.ClassNotFound:
587 pass
587 pass
588
588
589 if not lexer:
589 if not lexer:
590 lexer = get_lexer_by_name('text')
590 lexer = get_lexer_by_name('text')
591
591
592 return lexer
592 return lexer
593
593
594
594
595 def get_lexer_for_filenode(filenode):
595 def get_lexer_for_filenode(filenode):
596 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
596 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
597 return lexer
597 return lexer
598
598
599
599
600 def pygmentize(filenode, **kwargs):
600 def pygmentize(filenode, **kwargs):
601 """
601 """
602 pygmentize function using pygments
602 pygmentize function using pygments
603
603
604 :param filenode:
604 :param filenode:
605 """
605 """
606 lexer = get_lexer_for_filenode(filenode)
606 lexer = get_lexer_for_filenode(filenode)
607 return literal(code_highlight(filenode.content, lexer,
607 return literal(code_highlight(filenode.content, lexer,
608 CodeHtmlFormatter(**kwargs)))
608 CodeHtmlFormatter(**kwargs)))
609
609
610
610
611 def is_following_repo(repo_name, user_id):
611 def is_following_repo(repo_name, user_id):
612 from rhodecode.model.scm import ScmModel
612 from rhodecode.model.scm import ScmModel
613 return ScmModel().is_following_repo(repo_name, user_id)
613 return ScmModel().is_following_repo(repo_name, user_id)
614
614
615
615
616 class _Message(object):
616 class _Message(object):
617 """A message returned by ``Flash.pop_messages()``.
617 """A message returned by ``Flash.pop_messages()``.
618
618
619 Converting the message to a string returns the message text. Instances
619 Converting the message to a string returns the message text. Instances
620 also have the following attributes:
620 also have the following attributes:
621
621
622 * ``message``: the message text.
622 * ``message``: the message text.
623 * ``category``: the category specified when the message was created.
623 * ``category``: the category specified when the message was created.
624 """
624 """
625
625
626 def __init__(self, category, message):
626 def __init__(self, category, message):
627 self.category = category
627 self.category = category
628 self.message = message
628 self.message = message
629
629
630 def __str__(self):
630 def __str__(self):
631 return self.message
631 return self.message
632
632
633 __unicode__ = __str__
633 __unicode__ = __str__
634
634
635 def __html__(self):
635 def __html__(self):
636 return escape(safe_unicode(self.message))
636 return escape(safe_unicode(self.message))
637
637
638
638
639 class Flash(object):
639 class Flash(object):
640 # List of allowed categories. If None, allow any category.
640 # List of allowed categories. If None, allow any category.
641 categories = ["warning", "notice", "error", "success"]
641 categories = ["warning", "notice", "error", "success"]
642
642
643 # Default category if none is specified.
643 # Default category if none is specified.
644 default_category = "notice"
644 default_category = "notice"
645
645
646 def __init__(self, session_key="flash", categories=None,
646 def __init__(self, session_key="flash", categories=None,
647 default_category=None):
647 default_category=None):
648 """
648 """
649 Instantiate a ``Flash`` object.
649 Instantiate a ``Flash`` object.
650
650
651 ``session_key`` is the key to save the messages under in the user's
651 ``session_key`` is the key to save the messages under in the user's
652 session.
652 session.
653
653
654 ``categories`` is an optional list which overrides the default list
654 ``categories`` is an optional list which overrides the default list
655 of categories.
655 of categories.
656
656
657 ``default_category`` overrides the default category used for messages
657 ``default_category`` overrides the default category used for messages
658 when none is specified.
658 when none is specified.
659 """
659 """
660 self.session_key = session_key
660 self.session_key = session_key
661 if categories is not None:
661 if categories is not None:
662 self.categories = categories
662 self.categories = categories
663 if default_category is not None:
663 if default_category is not None:
664 self.default_category = default_category
664 self.default_category = default_category
665 if self.categories and self.default_category not in self.categories:
665 if self.categories and self.default_category not in self.categories:
666 raise ValueError(
666 raise ValueError(
667 "unrecognized default category %r" % (self.default_category,))
667 "unrecognized default category %r" % (self.default_category,))
668
668
669 def pop_messages(self, session=None, request=None):
669 def pop_messages(self, session=None, request=None):
670 """
670 """
671 Return all accumulated messages and delete them from the session.
671 Return all accumulated messages and delete them from the session.
672
672
673 The return value is a list of ``Message`` objects.
673 The return value is a list of ``Message`` objects.
674 """
674 """
675 messages = []
675 messages = []
676
676
677 if not session:
677 if not session:
678 if not request:
678 if not request:
679 request = get_current_request()
679 request = get_current_request()
680 session = request.session
680 session = request.session
681
681
682 # Pop the 'old' pylons flash messages. They are tuples of the form
682 # Pop the 'old' pylons flash messages. They are tuples of the form
683 # (category, message)
683 # (category, message)
684 for cat, msg in session.pop(self.session_key, []):
684 for cat, msg in session.pop(self.session_key, []):
685 messages.append(_Message(cat, msg))
685 messages.append(_Message(cat, msg))
686
686
687 # Pop the 'new' pyramid flash messages for each category as list
687 # Pop the 'new' pyramid flash messages for each category as list
688 # of strings.
688 # of strings.
689 for cat in self.categories:
689 for cat in self.categories:
690 for msg in session.pop_flash(queue=cat):
690 for msg in session.pop_flash(queue=cat):
691 messages.append(_Message(cat, msg))
691 messages.append(_Message(cat, msg))
692 # Map messages from the default queue to the 'notice' category.
692 # Map messages from the default queue to the 'notice' category.
693 for msg in session.pop_flash():
693 for msg in session.pop_flash():
694 messages.append(_Message('notice', msg))
694 messages.append(_Message('notice', msg))
695
695
696 session.save()
696 session.save()
697 return messages
697 return messages
698
698
699 def json_alerts(self, session=None, request=None):
699 def json_alerts(self, session=None, request=None):
700 payloads = []
700 payloads = []
701 messages = flash.pop_messages(session=session, request=request)
701 messages = flash.pop_messages(session=session, request=request)
702 if messages:
702 if messages:
703 for message in messages:
703 for message in messages:
704 subdata = {}
704 subdata = {}
705 if hasattr(message.message, 'rsplit'):
705 if hasattr(message.message, 'rsplit'):
706 flash_data = message.message.rsplit('|DELIM|', 1)
706 flash_data = message.message.rsplit('|DELIM|', 1)
707 org_message = flash_data[0]
707 org_message = flash_data[0]
708 if len(flash_data) > 1:
708 if len(flash_data) > 1:
709 subdata = json.loads(flash_data[1])
709 subdata = json.loads(flash_data[1])
710 else:
710 else:
711 org_message = message.message
711 org_message = message.message
712 payloads.append({
712 payloads.append({
713 'message': {
713 'message': {
714 'message': u'{}'.format(org_message),
714 'message': u'{}'.format(org_message),
715 'level': message.category,
715 'level': message.category,
716 'force': True,
716 'force': True,
717 'subdata': subdata
717 'subdata': subdata
718 }
718 }
719 })
719 })
720 return json.dumps(payloads)
720 return json.dumps(payloads)
721
721
722 def __call__(self, message, category=None, ignore_duplicate=False,
722 def __call__(self, message, category=None, ignore_duplicate=False,
723 session=None, request=None):
723 session=None, request=None):
724
724
725 if not session:
725 if not session:
726 if not request:
726 if not request:
727 request = get_current_request()
727 request = get_current_request()
728 session = request.session
728 session = request.session
729
729
730 session.flash(
730 session.flash(
731 message, queue=category, allow_duplicate=not ignore_duplicate)
731 message, queue=category, allow_duplicate=not ignore_duplicate)
732
732
733
733
734 flash = Flash()
734 flash = Flash()
735
735
736 #==============================================================================
736 #==============================================================================
737 # SCM FILTERS available via h.
737 # SCM FILTERS available via h.
738 #==============================================================================
738 #==============================================================================
739 from rhodecode.lib.vcs.utils import author_name, author_email
739 from rhodecode.lib.vcs.utils import author_name, author_email
740 from rhodecode.lib.utils2 import credentials_filter, age as _age
740 from rhodecode.lib.utils2 import credentials_filter, age as _age
741 from rhodecode.model.db import User, ChangesetStatus
741 from rhodecode.model.db import User, ChangesetStatus
742
742
743 age = _age
743 age = _age
744 capitalize = lambda x: x.capitalize()
744 capitalize = lambda x: x.capitalize()
745 email = author_email
745 email = author_email
746 short_id = lambda x: x[:12]
746 short_id = lambda x: x[:12]
747 hide_credentials = lambda x: ''.join(credentials_filter(x))
747 hide_credentials = lambda x: ''.join(credentials_filter(x))
748
748
749
749
750 def age_component(datetime_iso, value=None, time_is_local=False):
750 def age_component(datetime_iso, value=None, time_is_local=False):
751 title = value or format_date(datetime_iso)
751 title = value or format_date(datetime_iso)
752 tzinfo = '+00:00'
752 tzinfo = '+00:00'
753
753
754 # detect if we have a timezone info, otherwise, add it
754 # detect if we have a timezone info, otherwise, add it
755 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
755 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
756 if time_is_local:
756 if time_is_local:
757 tzinfo = time.strftime("+%H:%M",
757 tzinfo = time.strftime("+%H:%M",
758 time.gmtime(
758 time.gmtime(
759 (datetime.now() - datetime.utcnow()).seconds + 1
759 (datetime.now() - datetime.utcnow()).seconds + 1
760 )
760 )
761 )
761 )
762
762
763 return literal(
763 return literal(
764 '<time class="timeago tooltip" '
764 '<time class="timeago tooltip" '
765 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
765 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
766 datetime_iso, title, tzinfo))
766 datetime_iso, title, tzinfo))
767
767
768
768
769 def _shorten_commit_id(commit_id):
769 def _shorten_commit_id(commit_id):
770 from rhodecode import CONFIG
770 from rhodecode import CONFIG
771 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
771 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
772 return commit_id[:def_len]
772 return commit_id[:def_len]
773
773
774
774
775 def show_id(commit):
775 def show_id(commit):
776 """
776 """
777 Configurable function that shows ID
777 Configurable function that shows ID
778 by default it's r123:fffeeefffeee
778 by default it's r123:fffeeefffeee
779
779
780 :param commit: commit instance
780 :param commit: commit instance
781 """
781 """
782 from rhodecode import CONFIG
782 from rhodecode import CONFIG
783 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
783 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
784
784
785 raw_id = _shorten_commit_id(commit.raw_id)
785 raw_id = _shorten_commit_id(commit.raw_id)
786 if show_idx:
786 if show_idx:
787 return 'r%s:%s' % (commit.idx, raw_id)
787 return 'r%s:%s' % (commit.idx, raw_id)
788 else:
788 else:
789 return '%s' % (raw_id, )
789 return '%s' % (raw_id, )
790
790
791
791
792 def format_date(date):
792 def format_date(date):
793 """
793 """
794 use a standardized formatting for dates used in RhodeCode
794 use a standardized formatting for dates used in RhodeCode
795
795
796 :param date: date/datetime object
796 :param date: date/datetime object
797 :return: formatted date
797 :return: formatted date
798 """
798 """
799
799
800 if date:
800 if date:
801 _fmt = "%a, %d %b %Y %H:%M:%S"
801 _fmt = "%a, %d %b %Y %H:%M:%S"
802 return safe_unicode(date.strftime(_fmt))
802 return safe_unicode(date.strftime(_fmt))
803
803
804 return u""
804 return u""
805
805
806
806
807 class _RepoChecker(object):
807 class _RepoChecker(object):
808
808
809 def __init__(self, backend_alias):
809 def __init__(self, backend_alias):
810 self._backend_alias = backend_alias
810 self._backend_alias = backend_alias
811
811
812 def __call__(self, repository):
812 def __call__(self, repository):
813 if hasattr(repository, 'alias'):
813 if hasattr(repository, 'alias'):
814 _type = repository.alias
814 _type = repository.alias
815 elif hasattr(repository, 'repo_type'):
815 elif hasattr(repository, 'repo_type'):
816 _type = repository.repo_type
816 _type = repository.repo_type
817 else:
817 else:
818 _type = repository
818 _type = repository
819 return _type == self._backend_alias
819 return _type == self._backend_alias
820
820
821 is_git = _RepoChecker('git')
821 is_git = _RepoChecker('git')
822 is_hg = _RepoChecker('hg')
822 is_hg = _RepoChecker('hg')
823 is_svn = _RepoChecker('svn')
823 is_svn = _RepoChecker('svn')
824
824
825
825
826 def get_repo_type_by_name(repo_name):
826 def get_repo_type_by_name(repo_name):
827 repo = Repository.get_by_repo_name(repo_name)
827 repo = Repository.get_by_repo_name(repo_name)
828 return repo.repo_type
828 return repo.repo_type
829
829
830
830
831 def is_svn_without_proxy(repository):
831 def is_svn_without_proxy(repository):
832 if is_svn(repository):
832 if is_svn(repository):
833 from rhodecode.model.settings import VcsSettingsModel
833 from rhodecode.model.settings import VcsSettingsModel
834 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
834 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
835 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
835 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
836 return False
836 return False
837
837
838
838
839 def discover_user(author):
839 def discover_user(author):
840 """
840 """
841 Tries to discover RhodeCode User based on the autho string. Author string
841 Tries to discover RhodeCode User based on the autho string. Author string
842 is typically `FirstName LastName <email@address.com>`
842 is typically `FirstName LastName <email@address.com>`
843 """
843 """
844
844
845 # if author is already an instance use it for extraction
845 # if author is already an instance use it for extraction
846 if isinstance(author, User):
846 if isinstance(author, User):
847 return author
847 return author
848
848
849 # Valid email in the attribute passed, see if they're in the system
849 # Valid email in the attribute passed, see if they're in the system
850 _email = author_email(author)
850 _email = author_email(author)
851 if _email != '':
851 if _email != '':
852 user = User.get_by_email(_email, case_insensitive=True, cache=True)
852 user = User.get_by_email(_email, case_insensitive=True, cache=True)
853 if user is not None:
853 if user is not None:
854 return user
854 return user
855
855
856 # Maybe it's a username, we try to extract it and fetch by username ?
856 # Maybe it's a username, we try to extract it and fetch by username ?
857 _author = author_name(author)
857 _author = author_name(author)
858 user = User.get_by_username(_author, case_insensitive=True, cache=True)
858 user = User.get_by_username(_author, case_insensitive=True, cache=True)
859 if user is not None:
859 if user is not None:
860 return user
860 return user
861
861
862 return None
862 return None
863
863
864
864
865 def email_or_none(author):
865 def email_or_none(author):
866 # extract email from the commit string
866 # extract email from the commit string
867 _email = author_email(author)
867 _email = author_email(author)
868
868
869 # If we have an email, use it, otherwise
869 # If we have an email, use it, otherwise
870 # see if it contains a username we can get an email from
870 # see if it contains a username we can get an email from
871 if _email != '':
871 if _email != '':
872 return _email
872 return _email
873 else:
873 else:
874 user = User.get_by_username(
874 user = User.get_by_username(
875 author_name(author), case_insensitive=True, cache=True)
875 author_name(author), case_insensitive=True, cache=True)
876
876
877 if user is not None:
877 if user is not None:
878 return user.email
878 return user.email
879
879
880 # No valid email, not a valid user in the system, none!
880 # No valid email, not a valid user in the system, none!
881 return None
881 return None
882
882
883
883
884 def link_to_user(author, length=0, **kwargs):
884 def link_to_user(author, length=0, **kwargs):
885 user = discover_user(author)
885 user = discover_user(author)
886 # user can be None, but if we have it already it means we can re-use it
886 # user can be None, but if we have it already it means we can re-use it
887 # in the person() function, so we save 1 intensive-query
887 # in the person() function, so we save 1 intensive-query
888 if user:
888 if user:
889 author = user
889 author = user
890
890
891 display_person = person(author, 'username_or_name_or_email')
891 display_person = person(author, 'username_or_name_or_email')
892 if length:
892 if length:
893 display_person = shorter(display_person, length)
893 display_person = shorter(display_person, length)
894
894
895 if user:
895 if user:
896 return link_to(
896 return link_to(
897 escape(display_person),
897 escape(display_person),
898 route_path('user_profile', username=user.username),
898 route_path('user_profile', username=user.username),
899 **kwargs)
899 **kwargs)
900 else:
900 else:
901 return escape(display_person)
901 return escape(display_person)
902
902
903
903
904 def person(author, show_attr="username_and_name"):
904 def person(author, show_attr="username_and_name"):
905 user = discover_user(author)
905 user = discover_user(author)
906 if user:
906 if user:
907 return getattr(user, show_attr)
907 return getattr(user, show_attr)
908 else:
908 else:
909 _author = author_name(author)
909 _author = author_name(author)
910 _email = email(author)
910 _email = email(author)
911 return _author or _email
911 return _author or _email
912
912
913
913
914 def author_string(email):
914 def author_string(email):
915 if email:
915 if email:
916 user = User.get_by_email(email, case_insensitive=True, cache=True)
916 user = User.get_by_email(email, case_insensitive=True, cache=True)
917 if user:
917 if user:
918 if user.first_name or user.last_name:
918 if user.first_name or user.last_name:
919 return '%s %s &lt;%s&gt;' % (
919 return '%s %s &lt;%s&gt;' % (
920 user.first_name, user.last_name, email)
920 user.first_name, user.last_name, email)
921 else:
921 else:
922 return email
922 return email
923 else:
923 else:
924 return email
924 return email
925 else:
925 else:
926 return None
926 return None
927
927
928
928
929 def person_by_id(id_, show_attr="username_and_name"):
929 def person_by_id(id_, show_attr="username_and_name"):
930 # attr to return from fetched user
930 # attr to return from fetched user
931 person_getter = lambda usr: getattr(usr, show_attr)
931 person_getter = lambda usr: getattr(usr, show_attr)
932
932
933 #maybe it's an ID ?
933 #maybe it's an ID ?
934 if str(id_).isdigit() or isinstance(id_, int):
934 if str(id_).isdigit() or isinstance(id_, int):
935 id_ = int(id_)
935 id_ = int(id_)
936 user = User.get(id_)
936 user = User.get(id_)
937 if user is not None:
937 if user is not None:
938 return person_getter(user)
938 return person_getter(user)
939 return id_
939 return id_
940
940
941
941
942 def gravatar_with_user(request, author, show_disabled=False):
942 def gravatar_with_user(request, author, show_disabled=False):
943 _render = request.get_partial_renderer('base/base.mako')
943 _render = request.get_partial_renderer(
944 'rhodecode:templates/base/base.mako')
944 return _render('gravatar_with_user', author, show_disabled=show_disabled)
945 return _render('gravatar_with_user', author, show_disabled=show_disabled)
945
946
946
947
947 tags_paterns = OrderedDict((
948 tags_paterns = OrderedDict((
948 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
949 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
949 '<div class="metatag" tag="lang">\\2</div>')),
950 '<div class="metatag" tag="lang">\\2</div>')),
950
951
951 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
952 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
952 '<div class="metatag" tag="see">see: \\1 </div>')),
953 '<div class="metatag" tag="see">see: \\1 </div>')),
953
954
954 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
955 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
955 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
956 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
956
957
957 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
958 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
958 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
959 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
959
960
960 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
961 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
961 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
962 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
962
963
963 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
964 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
964 '<div class="metatag" tag="state \\1">\\1</div>')),
965 '<div class="metatag" tag="state \\1">\\1</div>')),
965
966
966 # label in grey
967 # label in grey
967 ('label', (re.compile(r'\[([a-z]+)\]'),
968 ('label', (re.compile(r'\[([a-z]+)\]'),
968 '<div class="metatag" tag="label">\\1</div>')),
969 '<div class="metatag" tag="label">\\1</div>')),
969
970
970 # generic catch all in grey
971 # generic catch all in grey
971 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
972 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
972 '<div class="metatag" tag="generic">\\1</div>')),
973 '<div class="metatag" tag="generic">\\1</div>')),
973 ))
974 ))
974
975
975
976
976 def extract_metatags(value):
977 def extract_metatags(value):
977 """
978 """
978 Extract supported meta-tags from given text value
979 Extract supported meta-tags from given text value
979 """
980 """
980 if not value:
981 if not value:
981 return ''
982 return ''
982
983
983 tags = []
984 tags = []
984 for key, val in tags_paterns.items():
985 for key, val in tags_paterns.items():
985 pat, replace_html = val
986 pat, replace_html = val
986 tags.extend([(key, x.group()) for x in pat.finditer(value)])
987 tags.extend([(key, x.group()) for x in pat.finditer(value)])
987 value = pat.sub('', value)
988 value = pat.sub('', value)
988
989
989 return tags, value
990 return tags, value
990
991
991
992
992 def style_metatag(tag_type, value):
993 def style_metatag(tag_type, value):
993 """
994 """
994 converts tags from value into html equivalent
995 converts tags from value into html equivalent
995 """
996 """
996 if not value:
997 if not value:
997 return ''
998 return ''
998
999
999 html_value = value
1000 html_value = value
1000 tag_data = tags_paterns.get(tag_type)
1001 tag_data = tags_paterns.get(tag_type)
1001 if tag_data:
1002 if tag_data:
1002 pat, replace_html = tag_data
1003 pat, replace_html = tag_data
1003 # convert to plain `unicode` instead of a markup tag to be used in
1004 # convert to plain `unicode` instead of a markup tag to be used in
1004 # regex expressions. safe_unicode doesn't work here
1005 # regex expressions. safe_unicode doesn't work here
1005 html_value = pat.sub(replace_html, unicode(value))
1006 html_value = pat.sub(replace_html, unicode(value))
1006
1007
1007 return html_value
1008 return html_value
1008
1009
1009
1010
1010 def bool2icon(value):
1011 def bool2icon(value):
1011 """
1012 """
1012 Returns boolean value of a given value, represented as html element with
1013 Returns boolean value of a given value, represented as html element with
1013 classes that will represent icons
1014 classes that will represent icons
1014
1015
1015 :param value: given value to convert to html node
1016 :param value: given value to convert to html node
1016 """
1017 """
1017
1018
1018 if value: # does bool conversion
1019 if value: # does bool conversion
1019 return HTML.tag('i', class_="icon-true")
1020 return HTML.tag('i', class_="icon-true")
1020 else: # not true as bool
1021 else: # not true as bool
1021 return HTML.tag('i', class_="icon-false")
1022 return HTML.tag('i', class_="icon-false")
1022
1023
1023
1024
1024 #==============================================================================
1025 #==============================================================================
1025 # PERMS
1026 # PERMS
1026 #==============================================================================
1027 #==============================================================================
1027 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1028 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1028 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1029 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1029 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1030 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1030 csrf_token_key
1031 csrf_token_key
1031
1032
1032
1033
1033 #==============================================================================
1034 #==============================================================================
1034 # GRAVATAR URL
1035 # GRAVATAR URL
1035 #==============================================================================
1036 #==============================================================================
1036 class InitialsGravatar(object):
1037 class InitialsGravatar(object):
1037 def __init__(self, email_address, first_name, last_name, size=30,
1038 def __init__(self, email_address, first_name, last_name, size=30,
1038 background=None, text_color='#fff'):
1039 background=None, text_color='#fff'):
1039 self.size = size
1040 self.size = size
1040 self.first_name = first_name
1041 self.first_name = first_name
1041 self.last_name = last_name
1042 self.last_name = last_name
1042 self.email_address = email_address
1043 self.email_address = email_address
1043 self.background = background or self.str2color(email_address)
1044 self.background = background or self.str2color(email_address)
1044 self.text_color = text_color
1045 self.text_color = text_color
1045
1046
1046 def get_color_bank(self):
1047 def get_color_bank(self):
1047 """
1048 """
1048 returns a predefined list of colors that gravatars can use.
1049 returns a predefined list of colors that gravatars can use.
1049 Those are randomized distinct colors that guarantee readability and
1050 Those are randomized distinct colors that guarantee readability and
1050 uniqueness.
1051 uniqueness.
1051
1052
1052 generated with: http://phrogz.net/css/distinct-colors.html
1053 generated with: http://phrogz.net/css/distinct-colors.html
1053 """
1054 """
1054 return [
1055 return [
1055 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1056 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1056 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1057 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1057 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1058 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1058 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1059 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1059 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1060 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1060 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1061 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1061 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1062 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1062 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1063 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1063 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1064 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1064 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1065 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1065 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1066 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1066 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1067 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1067 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1068 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1068 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1069 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1069 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1070 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1070 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1071 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1071 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1072 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1072 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1073 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1073 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1074 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1074 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1075 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1075 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1076 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1076 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1077 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1077 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1078 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1078 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1079 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1079 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1080 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1080 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1081 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1081 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1082 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1082 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1083 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1083 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1084 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1084 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1085 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1085 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1086 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1086 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1087 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1087 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1088 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1088 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1089 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1089 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1090 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1090 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1091 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1091 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1092 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1092 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1093 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1093 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1094 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1094 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1095 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1095 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1096 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1096 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1097 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1097 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1098 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1098 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1099 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1099 '#4f8c46', '#368dd9', '#5c0073'
1100 '#4f8c46', '#368dd9', '#5c0073'
1100 ]
1101 ]
1101
1102
1102 def rgb_to_hex_color(self, rgb_tuple):
1103 def rgb_to_hex_color(self, rgb_tuple):
1103 """
1104 """
1104 Converts an rgb_tuple passed to an hex color.
1105 Converts an rgb_tuple passed to an hex color.
1105
1106
1106 :param rgb_tuple: tuple with 3 ints represents rgb color space
1107 :param rgb_tuple: tuple with 3 ints represents rgb color space
1107 """
1108 """
1108 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1109 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1109
1110
1110 def email_to_int_list(self, email_str):
1111 def email_to_int_list(self, email_str):
1111 """
1112 """
1112 Get every byte of the hex digest value of email and turn it to integer.
1113 Get every byte of the hex digest value of email and turn it to integer.
1113 It's going to be always between 0-255
1114 It's going to be always between 0-255
1114 """
1115 """
1115 digest = md5_safe(email_str.lower())
1116 digest = md5_safe(email_str.lower())
1116 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1117 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1117
1118
1118 def pick_color_bank_index(self, email_str, color_bank):
1119 def pick_color_bank_index(self, email_str, color_bank):
1119 return self.email_to_int_list(email_str)[0] % len(color_bank)
1120 return self.email_to_int_list(email_str)[0] % len(color_bank)
1120
1121
1121 def str2color(self, email_str):
1122 def str2color(self, email_str):
1122 """
1123 """
1123 Tries to map in a stable algorithm an email to color
1124 Tries to map in a stable algorithm an email to color
1124
1125
1125 :param email_str:
1126 :param email_str:
1126 """
1127 """
1127 color_bank = self.get_color_bank()
1128 color_bank = self.get_color_bank()
1128 # pick position (module it's length so we always find it in the
1129 # pick position (module it's length so we always find it in the
1129 # bank even if it's smaller than 256 values
1130 # bank even if it's smaller than 256 values
1130 pos = self.pick_color_bank_index(email_str, color_bank)
1131 pos = self.pick_color_bank_index(email_str, color_bank)
1131 return color_bank[pos]
1132 return color_bank[pos]
1132
1133
1133 def normalize_email(self, email_address):
1134 def normalize_email(self, email_address):
1134 import unicodedata
1135 import unicodedata
1135 # default host used to fill in the fake/missing email
1136 # default host used to fill in the fake/missing email
1136 default_host = u'localhost'
1137 default_host = u'localhost'
1137
1138
1138 if not email_address:
1139 if not email_address:
1139 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1140 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1140
1141
1141 email_address = safe_unicode(email_address)
1142 email_address = safe_unicode(email_address)
1142
1143
1143 if u'@' not in email_address:
1144 if u'@' not in email_address:
1144 email_address = u'%s@%s' % (email_address, default_host)
1145 email_address = u'%s@%s' % (email_address, default_host)
1145
1146
1146 if email_address.endswith(u'@'):
1147 if email_address.endswith(u'@'):
1147 email_address = u'%s%s' % (email_address, default_host)
1148 email_address = u'%s%s' % (email_address, default_host)
1148
1149
1149 email_address = unicodedata.normalize('NFKD', email_address)\
1150 email_address = unicodedata.normalize('NFKD', email_address)\
1150 .encode('ascii', 'ignore')
1151 .encode('ascii', 'ignore')
1151 return email_address
1152 return email_address
1152
1153
1153 def get_initials(self):
1154 def get_initials(self):
1154 """
1155 """
1155 Returns 2 letter initials calculated based on the input.
1156 Returns 2 letter initials calculated based on the input.
1156 The algorithm picks first given email address, and takes first letter
1157 The algorithm picks first given email address, and takes first letter
1157 of part before @, and then the first letter of server name. In case
1158 of part before @, and then the first letter of server name. In case
1158 the part before @ is in a format of `somestring.somestring2` it replaces
1159 the part before @ is in a format of `somestring.somestring2` it replaces
1159 the server letter with first letter of somestring2
1160 the server letter with first letter of somestring2
1160
1161
1161 In case function was initialized with both first and lastname, this
1162 In case function was initialized with both first and lastname, this
1162 overrides the extraction from email by first letter of the first and
1163 overrides the extraction from email by first letter of the first and
1163 last name. We add special logic to that functionality, In case Full name
1164 last name. We add special logic to that functionality, In case Full name
1164 is compound, like Guido Von Rossum, we use last part of the last name
1165 is compound, like Guido Von Rossum, we use last part of the last name
1165 (Von Rossum) picking `R`.
1166 (Von Rossum) picking `R`.
1166
1167
1167 Function also normalizes the non-ascii characters to they ascii
1168 Function also normalizes the non-ascii characters to they ascii
1168 representation, eg Δ„ => A
1169 representation, eg Δ„ => A
1169 """
1170 """
1170 import unicodedata
1171 import unicodedata
1171 # replace non-ascii to ascii
1172 # replace non-ascii to ascii
1172 first_name = unicodedata.normalize(
1173 first_name = unicodedata.normalize(
1173 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1174 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1174 last_name = unicodedata.normalize(
1175 last_name = unicodedata.normalize(
1175 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1176 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1176
1177
1177 # do NFKD encoding, and also make sure email has proper format
1178 # do NFKD encoding, and also make sure email has proper format
1178 email_address = self.normalize_email(self.email_address)
1179 email_address = self.normalize_email(self.email_address)
1179
1180
1180 # first push the email initials
1181 # first push the email initials
1181 prefix, server = email_address.split('@', 1)
1182 prefix, server = email_address.split('@', 1)
1182
1183
1183 # check if prefix is maybe a 'first_name.last_name' syntax
1184 # check if prefix is maybe a 'first_name.last_name' syntax
1184 _dot_split = prefix.rsplit('.', 1)
1185 _dot_split = prefix.rsplit('.', 1)
1185 if len(_dot_split) == 2 and _dot_split[1]:
1186 if len(_dot_split) == 2 and _dot_split[1]:
1186 initials = [_dot_split[0][0], _dot_split[1][0]]
1187 initials = [_dot_split[0][0], _dot_split[1][0]]
1187 else:
1188 else:
1188 initials = [prefix[0], server[0]]
1189 initials = [prefix[0], server[0]]
1189
1190
1190 # then try to replace either first_name or last_name
1191 # then try to replace either first_name or last_name
1191 fn_letter = (first_name or " ")[0].strip()
1192 fn_letter = (first_name or " ")[0].strip()
1192 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1193 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1193
1194
1194 if fn_letter:
1195 if fn_letter:
1195 initials[0] = fn_letter
1196 initials[0] = fn_letter
1196
1197
1197 if ln_letter:
1198 if ln_letter:
1198 initials[1] = ln_letter
1199 initials[1] = ln_letter
1199
1200
1200 return ''.join(initials).upper()
1201 return ''.join(initials).upper()
1201
1202
1202 def get_img_data_by_type(self, font_family, img_type):
1203 def get_img_data_by_type(self, font_family, img_type):
1203 default_user = """
1204 default_user = """
1204 <svg xmlns="http://www.w3.org/2000/svg"
1205 <svg xmlns="http://www.w3.org/2000/svg"
1205 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1206 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1206 viewBox="-15 -10 439.165 429.164"
1207 viewBox="-15 -10 439.165 429.164"
1207
1208
1208 xml:space="preserve"
1209 xml:space="preserve"
1209 style="background:{background};" >
1210 style="background:{background};" >
1210
1211
1211 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1212 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1212 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1213 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1213 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1214 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1214 168.596,153.916,216.671,
1215 168.596,153.916,216.671,
1215 204.583,216.671z" fill="{text_color}"/>
1216 204.583,216.671z" fill="{text_color}"/>
1216 <path d="M407.164,374.717L360.88,
1217 <path d="M407.164,374.717L360.88,
1217 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1218 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1218 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1219 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1219 15.366-44.203,23.488-69.076,23.488c-24.877,
1220 15.366-44.203,23.488-69.076,23.488c-24.877,
1220 0-48.762-8.122-69.078-23.488
1221 0-48.762-8.122-69.078-23.488
1221 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1222 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1222 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1223 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1223 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1224 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1224 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1225 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1225 19.402-10.527 C409.699,390.129,
1226 19.402-10.527 C409.699,390.129,
1226 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1227 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1227 </svg>""".format(
1228 </svg>""".format(
1228 size=self.size,
1229 size=self.size,
1229 background='#979797', # @grey4
1230 background='#979797', # @grey4
1230 text_color=self.text_color,
1231 text_color=self.text_color,
1231 font_family=font_family)
1232 font_family=font_family)
1232
1233
1233 return {
1234 return {
1234 "default_user": default_user
1235 "default_user": default_user
1235 }[img_type]
1236 }[img_type]
1236
1237
1237 def get_img_data(self, svg_type=None):
1238 def get_img_data(self, svg_type=None):
1238 """
1239 """
1239 generates the svg metadata for image
1240 generates the svg metadata for image
1240 """
1241 """
1241
1242
1242 font_family = ','.join([
1243 font_family = ','.join([
1243 'proximanovaregular',
1244 'proximanovaregular',
1244 'Proxima Nova Regular',
1245 'Proxima Nova Regular',
1245 'Proxima Nova',
1246 'Proxima Nova',
1246 'Arial',
1247 'Arial',
1247 'Lucida Grande',
1248 'Lucida Grande',
1248 'sans-serif'
1249 'sans-serif'
1249 ])
1250 ])
1250 if svg_type:
1251 if svg_type:
1251 return self.get_img_data_by_type(font_family, svg_type)
1252 return self.get_img_data_by_type(font_family, svg_type)
1252
1253
1253 initials = self.get_initials()
1254 initials = self.get_initials()
1254 img_data = """
1255 img_data = """
1255 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1256 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1256 width="{size}" height="{size}"
1257 width="{size}" height="{size}"
1257 style="width: 100%; height: 100%; background-color: {background}"
1258 style="width: 100%; height: 100%; background-color: {background}"
1258 viewBox="0 0 {size} {size}">
1259 viewBox="0 0 {size} {size}">
1259 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1260 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1260 pointer-events="auto" fill="{text_color}"
1261 pointer-events="auto" fill="{text_color}"
1261 font-family="{font_family}"
1262 font-family="{font_family}"
1262 style="font-weight: 400; font-size: {f_size}px;">{text}
1263 style="font-weight: 400; font-size: {f_size}px;">{text}
1263 </text>
1264 </text>
1264 </svg>""".format(
1265 </svg>""".format(
1265 size=self.size,
1266 size=self.size,
1266 f_size=self.size/1.85, # scale the text inside the box nicely
1267 f_size=self.size/1.85, # scale the text inside the box nicely
1267 background=self.background,
1268 background=self.background,
1268 text_color=self.text_color,
1269 text_color=self.text_color,
1269 text=initials.upper(),
1270 text=initials.upper(),
1270 font_family=font_family)
1271 font_family=font_family)
1271
1272
1272 return img_data
1273 return img_data
1273
1274
1274 def generate_svg(self, svg_type=None):
1275 def generate_svg(self, svg_type=None):
1275 img_data = self.get_img_data(svg_type)
1276 img_data = self.get_img_data(svg_type)
1276 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1277 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1277
1278
1278
1279
1279 def initials_gravatar(email_address, first_name, last_name, size=30):
1280 def initials_gravatar(email_address, first_name, last_name, size=30):
1280 svg_type = None
1281 svg_type = None
1281 if email_address == User.DEFAULT_USER_EMAIL:
1282 if email_address == User.DEFAULT_USER_EMAIL:
1282 svg_type = 'default_user'
1283 svg_type = 'default_user'
1283 klass = InitialsGravatar(email_address, first_name, last_name, size)
1284 klass = InitialsGravatar(email_address, first_name, last_name, size)
1284 return klass.generate_svg(svg_type=svg_type)
1285 return klass.generate_svg(svg_type=svg_type)
1285
1286
1286
1287
1287 def gravatar_url(email_address, size=30, request=None):
1288 def gravatar_url(email_address, size=30, request=None):
1288 request = get_current_request()
1289 request = get_current_request()
1289 if request and hasattr(request, 'call_context'):
1290 if request and hasattr(request, 'call_context'):
1290 _use_gravatar = request.call_context.visual.use_gravatar
1291 _use_gravatar = request.call_context.visual.use_gravatar
1291 _gravatar_url = request.call_context.visual.gravatar_url
1292 _gravatar_url = request.call_context.visual.gravatar_url
1292 else:
1293 else:
1293 # doh, we need to re-import those to mock it later
1294 # doh, we need to re-import those to mock it later
1294 from pylons import tmpl_context as c
1295 from pylons import tmpl_context as c
1295
1296
1296 _use_gravatar = c.visual.use_gravatar
1297 _use_gravatar = c.visual.use_gravatar
1297 _gravatar_url = c.visual.gravatar_url
1298 _gravatar_url = c.visual.gravatar_url
1298
1299
1299 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1300 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1300
1301
1301 email_address = email_address or User.DEFAULT_USER_EMAIL
1302 email_address = email_address or User.DEFAULT_USER_EMAIL
1302 if isinstance(email_address, unicode):
1303 if isinstance(email_address, unicode):
1303 # hashlib crashes on unicode items
1304 # hashlib crashes on unicode items
1304 email_address = safe_str(email_address)
1305 email_address = safe_str(email_address)
1305
1306
1306 # empty email or default user
1307 # empty email or default user
1307 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1308 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1308 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1309 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1309
1310
1310 if _use_gravatar:
1311 if _use_gravatar:
1311 # TODO: Disuse pyramid thread locals. Think about another solution to
1312 # TODO: Disuse pyramid thread locals. Think about another solution to
1312 # get the host and schema here.
1313 # get the host and schema here.
1313 request = get_current_request()
1314 request = get_current_request()
1314 tmpl = safe_str(_gravatar_url)
1315 tmpl = safe_str(_gravatar_url)
1315 tmpl = tmpl.replace('{email}', email_address)\
1316 tmpl = tmpl.replace('{email}', email_address)\
1316 .replace('{md5email}', md5_safe(email_address.lower())) \
1317 .replace('{md5email}', md5_safe(email_address.lower())) \
1317 .replace('{netloc}', request.host)\
1318 .replace('{netloc}', request.host)\
1318 .replace('{scheme}', request.scheme)\
1319 .replace('{scheme}', request.scheme)\
1319 .replace('{size}', safe_str(size))
1320 .replace('{size}', safe_str(size))
1320 return tmpl
1321 return tmpl
1321 else:
1322 else:
1322 return initials_gravatar(email_address, '', '', size=size)
1323 return initials_gravatar(email_address, '', '', size=size)
1323
1324
1324
1325
1325 class Page(_Page):
1326 class Page(_Page):
1326 """
1327 """
1327 Custom pager to match rendering style with paginator
1328 Custom pager to match rendering style with paginator
1328 """
1329 """
1329
1330
1330 def _get_pos(self, cur_page, max_page, items):
1331 def _get_pos(self, cur_page, max_page, items):
1331 edge = (items / 2) + 1
1332 edge = (items / 2) + 1
1332 if (cur_page <= edge):
1333 if (cur_page <= edge):
1333 radius = max(items / 2, items - cur_page)
1334 radius = max(items / 2, items - cur_page)
1334 elif (max_page - cur_page) < edge:
1335 elif (max_page - cur_page) < edge:
1335 radius = (items - 1) - (max_page - cur_page)
1336 radius = (items - 1) - (max_page - cur_page)
1336 else:
1337 else:
1337 radius = items / 2
1338 radius = items / 2
1338
1339
1339 left = max(1, (cur_page - (radius)))
1340 left = max(1, (cur_page - (radius)))
1340 right = min(max_page, cur_page + (radius))
1341 right = min(max_page, cur_page + (radius))
1341 return left, cur_page, right
1342 return left, cur_page, right
1342
1343
1343 def _range(self, regexp_match):
1344 def _range(self, regexp_match):
1344 """
1345 """
1345 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1346 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1346
1347
1347 Arguments:
1348 Arguments:
1348
1349
1349 regexp_match
1350 regexp_match
1350 A "re" (regular expressions) match object containing the
1351 A "re" (regular expressions) match object containing the
1351 radius of linked pages around the current page in
1352 radius of linked pages around the current page in
1352 regexp_match.group(1) as a string
1353 regexp_match.group(1) as a string
1353
1354
1354 This function is supposed to be called as a callable in
1355 This function is supposed to be called as a callable in
1355 re.sub.
1356 re.sub.
1356
1357
1357 """
1358 """
1358 radius = int(regexp_match.group(1))
1359 radius = int(regexp_match.group(1))
1359
1360
1360 # Compute the first and last page number within the radius
1361 # Compute the first and last page number within the radius
1361 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1362 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1362 # -> leftmost_page = 5
1363 # -> leftmost_page = 5
1363 # -> rightmost_page = 9
1364 # -> rightmost_page = 9
1364 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1365 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1365 self.last_page,
1366 self.last_page,
1366 (radius * 2) + 1)
1367 (radius * 2) + 1)
1367 nav_items = []
1368 nav_items = []
1368
1369
1369 # Create a link to the first page (unless we are on the first page
1370 # Create a link to the first page (unless we are on the first page
1370 # or there would be no need to insert '..' spacers)
1371 # or there would be no need to insert '..' spacers)
1371 if self.page != self.first_page and self.first_page < leftmost_page:
1372 if self.page != self.first_page and self.first_page < leftmost_page:
1372 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1373 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1373
1374
1374 # Insert dots if there are pages between the first page
1375 # Insert dots if there are pages between the first page
1375 # and the currently displayed page range
1376 # and the currently displayed page range
1376 if leftmost_page - self.first_page > 1:
1377 if leftmost_page - self.first_page > 1:
1377 # Wrap in a SPAN tag if nolink_attr is set
1378 # Wrap in a SPAN tag if nolink_attr is set
1378 text = '..'
1379 text = '..'
1379 if self.dotdot_attr:
1380 if self.dotdot_attr:
1380 text = HTML.span(c=text, **self.dotdot_attr)
1381 text = HTML.span(c=text, **self.dotdot_attr)
1381 nav_items.append(text)
1382 nav_items.append(text)
1382
1383
1383 for thispage in xrange(leftmost_page, rightmost_page + 1):
1384 for thispage in xrange(leftmost_page, rightmost_page + 1):
1384 # Hilight the current page number and do not use a link
1385 # Hilight the current page number and do not use a link
1385 if thispage == self.page:
1386 if thispage == self.page:
1386 text = '%s' % (thispage,)
1387 text = '%s' % (thispage,)
1387 # Wrap in a SPAN tag if nolink_attr is set
1388 # Wrap in a SPAN tag if nolink_attr is set
1388 if self.curpage_attr:
1389 if self.curpage_attr:
1389 text = HTML.span(c=text, **self.curpage_attr)
1390 text = HTML.span(c=text, **self.curpage_attr)
1390 nav_items.append(text)
1391 nav_items.append(text)
1391 # Otherwise create just a link to that page
1392 # Otherwise create just a link to that page
1392 else:
1393 else:
1393 text = '%s' % (thispage,)
1394 text = '%s' % (thispage,)
1394 nav_items.append(self._pagerlink(thispage, text))
1395 nav_items.append(self._pagerlink(thispage, text))
1395
1396
1396 # Insert dots if there are pages between the displayed
1397 # Insert dots if there are pages between the displayed
1397 # page numbers and the end of the page range
1398 # page numbers and the end of the page range
1398 if self.last_page - rightmost_page > 1:
1399 if self.last_page - rightmost_page > 1:
1399 text = '..'
1400 text = '..'
1400 # Wrap in a SPAN tag if nolink_attr is set
1401 # Wrap in a SPAN tag if nolink_attr is set
1401 if self.dotdot_attr:
1402 if self.dotdot_attr:
1402 text = HTML.span(c=text, **self.dotdot_attr)
1403 text = HTML.span(c=text, **self.dotdot_attr)
1403 nav_items.append(text)
1404 nav_items.append(text)
1404
1405
1405 # Create a link to the very last page (unless we are on the last
1406 # Create a link to the very last page (unless we are on the last
1406 # page or there would be no need to insert '..' spacers)
1407 # page or there would be no need to insert '..' spacers)
1407 if self.page != self.last_page and rightmost_page < self.last_page:
1408 if self.page != self.last_page and rightmost_page < self.last_page:
1408 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1409 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1409
1410
1410 ## prerender links
1411 ## prerender links
1411 #_page_link = url.current()
1412 #_page_link = url.current()
1412 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1413 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1413 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1414 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1414 return self.separator.join(nav_items)
1415 return self.separator.join(nav_items)
1415
1416
1416 def pager(self, format='~2~', page_param='page', partial_param='partial',
1417 def pager(self, format='~2~', page_param='page', partial_param='partial',
1417 show_if_single_page=False, separator=' ', onclick=None,
1418 show_if_single_page=False, separator=' ', onclick=None,
1418 symbol_first='<<', symbol_last='>>',
1419 symbol_first='<<', symbol_last='>>',
1419 symbol_previous='<', symbol_next='>',
1420 symbol_previous='<', symbol_next='>',
1420 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1421 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1421 curpage_attr={'class': 'pager_curpage'},
1422 curpage_attr={'class': 'pager_curpage'},
1422 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1423 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1423
1424
1424 self.curpage_attr = curpage_attr
1425 self.curpage_attr = curpage_attr
1425 self.separator = separator
1426 self.separator = separator
1426 self.pager_kwargs = kwargs
1427 self.pager_kwargs = kwargs
1427 self.page_param = page_param
1428 self.page_param = page_param
1428 self.partial_param = partial_param
1429 self.partial_param = partial_param
1429 self.onclick = onclick
1430 self.onclick = onclick
1430 self.link_attr = link_attr
1431 self.link_attr = link_attr
1431 self.dotdot_attr = dotdot_attr
1432 self.dotdot_attr = dotdot_attr
1432
1433
1433 # Don't show navigator if there is no more than one page
1434 # Don't show navigator if there is no more than one page
1434 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1435 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1435 return ''
1436 return ''
1436
1437
1437 from string import Template
1438 from string import Template
1438 # Replace ~...~ in token format by range of pages
1439 # Replace ~...~ in token format by range of pages
1439 result = re.sub(r'~(\d+)~', self._range, format)
1440 result = re.sub(r'~(\d+)~', self._range, format)
1440
1441
1441 # Interpolate '%' variables
1442 # Interpolate '%' variables
1442 result = Template(result).safe_substitute({
1443 result = Template(result).safe_substitute({
1443 'first_page': self.first_page,
1444 'first_page': self.first_page,
1444 'last_page': self.last_page,
1445 'last_page': self.last_page,
1445 'page': self.page,
1446 'page': self.page,
1446 'page_count': self.page_count,
1447 'page_count': self.page_count,
1447 'items_per_page': self.items_per_page,
1448 'items_per_page': self.items_per_page,
1448 'first_item': self.first_item,
1449 'first_item': self.first_item,
1449 'last_item': self.last_item,
1450 'last_item': self.last_item,
1450 'item_count': self.item_count,
1451 'item_count': self.item_count,
1451 'link_first': self.page > self.first_page and \
1452 'link_first': self.page > self.first_page and \
1452 self._pagerlink(self.first_page, symbol_first) or '',
1453 self._pagerlink(self.first_page, symbol_first) or '',
1453 'link_last': self.page < self.last_page and \
1454 'link_last': self.page < self.last_page and \
1454 self._pagerlink(self.last_page, symbol_last) or '',
1455 self._pagerlink(self.last_page, symbol_last) or '',
1455 'link_previous': self.previous_page and \
1456 'link_previous': self.previous_page and \
1456 self._pagerlink(self.previous_page, symbol_previous) \
1457 self._pagerlink(self.previous_page, symbol_previous) \
1457 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1458 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1458 'link_next': self.next_page and \
1459 'link_next': self.next_page and \
1459 self._pagerlink(self.next_page, symbol_next) \
1460 self._pagerlink(self.next_page, symbol_next) \
1460 or HTML.span(symbol_next, class_="pg-next disabled")
1461 or HTML.span(symbol_next, class_="pg-next disabled")
1461 })
1462 })
1462
1463
1463 return literal(result)
1464 return literal(result)
1464
1465
1465
1466
1466 #==============================================================================
1467 #==============================================================================
1467 # REPO PAGER, PAGER FOR REPOSITORY
1468 # REPO PAGER, PAGER FOR REPOSITORY
1468 #==============================================================================
1469 #==============================================================================
1469 class RepoPage(Page):
1470 class RepoPage(Page):
1470
1471
1471 def __init__(self, collection, page=1, items_per_page=20,
1472 def __init__(self, collection, page=1, items_per_page=20,
1472 item_count=None, url=None, **kwargs):
1473 item_count=None, url=None, **kwargs):
1473
1474
1474 """Create a "RepoPage" instance. special pager for paging
1475 """Create a "RepoPage" instance. special pager for paging
1475 repository
1476 repository
1476 """
1477 """
1477 self._url_generator = url
1478 self._url_generator = url
1478
1479
1479 # Safe the kwargs class-wide so they can be used in the pager() method
1480 # Safe the kwargs class-wide so they can be used in the pager() method
1480 self.kwargs = kwargs
1481 self.kwargs = kwargs
1481
1482
1482 # Save a reference to the collection
1483 # Save a reference to the collection
1483 self.original_collection = collection
1484 self.original_collection = collection
1484
1485
1485 self.collection = collection
1486 self.collection = collection
1486
1487
1487 # The self.page is the number of the current page.
1488 # The self.page is the number of the current page.
1488 # The first page has the number 1!
1489 # The first page has the number 1!
1489 try:
1490 try:
1490 self.page = int(page) # make it int() if we get it as a string
1491 self.page = int(page) # make it int() if we get it as a string
1491 except (ValueError, TypeError):
1492 except (ValueError, TypeError):
1492 self.page = 1
1493 self.page = 1
1493
1494
1494 self.items_per_page = items_per_page
1495 self.items_per_page = items_per_page
1495
1496
1496 # Unless the user tells us how many items the collections has
1497 # Unless the user tells us how many items the collections has
1497 # we calculate that ourselves.
1498 # we calculate that ourselves.
1498 if item_count is not None:
1499 if item_count is not None:
1499 self.item_count = item_count
1500 self.item_count = item_count
1500 else:
1501 else:
1501 self.item_count = len(self.collection)
1502 self.item_count = len(self.collection)
1502
1503
1503 # Compute the number of the first and last available page
1504 # Compute the number of the first and last available page
1504 if self.item_count > 0:
1505 if self.item_count > 0:
1505 self.first_page = 1
1506 self.first_page = 1
1506 self.page_count = int(math.ceil(float(self.item_count) /
1507 self.page_count = int(math.ceil(float(self.item_count) /
1507 self.items_per_page))
1508 self.items_per_page))
1508 self.last_page = self.first_page + self.page_count - 1
1509 self.last_page = self.first_page + self.page_count - 1
1509
1510
1510 # Make sure that the requested page number is the range of
1511 # Make sure that the requested page number is the range of
1511 # valid pages
1512 # valid pages
1512 if self.page > self.last_page:
1513 if self.page > self.last_page:
1513 self.page = self.last_page
1514 self.page = self.last_page
1514 elif self.page < self.first_page:
1515 elif self.page < self.first_page:
1515 self.page = self.first_page
1516 self.page = self.first_page
1516
1517
1517 # Note: the number of items on this page can be less than
1518 # Note: the number of items on this page can be less than
1518 # items_per_page if the last page is not full
1519 # items_per_page if the last page is not full
1519 self.first_item = max(0, (self.item_count) - (self.page *
1520 self.first_item = max(0, (self.item_count) - (self.page *
1520 items_per_page))
1521 items_per_page))
1521 self.last_item = ((self.item_count - 1) - items_per_page *
1522 self.last_item = ((self.item_count - 1) - items_per_page *
1522 (self.page - 1))
1523 (self.page - 1))
1523
1524
1524 self.items = list(self.collection[self.first_item:self.last_item + 1])
1525 self.items = list(self.collection[self.first_item:self.last_item + 1])
1525
1526
1526 # Links to previous and next page
1527 # Links to previous and next page
1527 if self.page > self.first_page:
1528 if self.page > self.first_page:
1528 self.previous_page = self.page - 1
1529 self.previous_page = self.page - 1
1529 else:
1530 else:
1530 self.previous_page = None
1531 self.previous_page = None
1531
1532
1532 if self.page < self.last_page:
1533 if self.page < self.last_page:
1533 self.next_page = self.page + 1
1534 self.next_page = self.page + 1
1534 else:
1535 else:
1535 self.next_page = None
1536 self.next_page = None
1536
1537
1537 # No items available
1538 # No items available
1538 else:
1539 else:
1539 self.first_page = None
1540 self.first_page = None
1540 self.page_count = 0
1541 self.page_count = 0
1541 self.last_page = None
1542 self.last_page = None
1542 self.first_item = None
1543 self.first_item = None
1543 self.last_item = None
1544 self.last_item = None
1544 self.previous_page = None
1545 self.previous_page = None
1545 self.next_page = None
1546 self.next_page = None
1546 self.items = []
1547 self.items = []
1547
1548
1548 # This is a subclass of the 'list' type. Initialise the list now.
1549 # This is a subclass of the 'list' type. Initialise the list now.
1549 list.__init__(self, reversed(self.items))
1550 list.__init__(self, reversed(self.items))
1550
1551
1551
1552
1552 def breadcrumb_repo_link(repo):
1553 def breadcrumb_repo_link(repo):
1553 """
1554 """
1554 Makes a breadcrumbs path link to repo
1555 Makes a breadcrumbs path link to repo
1555
1556
1556 ex::
1557 ex::
1557 group >> subgroup >> repo
1558 group >> subgroup >> repo
1558
1559
1559 :param repo: a Repository instance
1560 :param repo: a Repository instance
1560 """
1561 """
1561
1562
1562 path = [
1563 path = [
1563 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1564 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1564 for group in repo.groups_with_parents
1565 for group in repo.groups_with_parents
1565 ] + [
1566 ] + [
1566 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1567 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1567 ]
1568 ]
1568
1569
1569 return literal(' &raquo; '.join(path))
1570 return literal(' &raquo; '.join(path))
1570
1571
1571
1572
1572 def format_byte_size_binary(file_size):
1573 def format_byte_size_binary(file_size):
1573 """
1574 """
1574 Formats file/folder sizes to standard.
1575 Formats file/folder sizes to standard.
1575 """
1576 """
1576 if file_size is None:
1577 if file_size is None:
1577 file_size = 0
1578 file_size = 0
1578
1579
1579 formatted_size = format_byte_size(file_size, binary=True)
1580 formatted_size = format_byte_size(file_size, binary=True)
1580 return formatted_size
1581 return formatted_size
1581
1582
1582
1583
1583 def urlify_text(text_, safe=True):
1584 def urlify_text(text_, safe=True):
1584 """
1585 """
1585 Extrac urls from text and make html links out of them
1586 Extrac urls from text and make html links out of them
1586
1587
1587 :param text_:
1588 :param text_:
1588 """
1589 """
1589
1590
1590 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1591 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1591 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1592 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1592
1593
1593 def url_func(match_obj):
1594 def url_func(match_obj):
1594 url_full = match_obj.groups()[0]
1595 url_full = match_obj.groups()[0]
1595 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1596 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1596 _newtext = url_pat.sub(url_func, text_)
1597 _newtext = url_pat.sub(url_func, text_)
1597 if safe:
1598 if safe:
1598 return literal(_newtext)
1599 return literal(_newtext)
1599 return _newtext
1600 return _newtext
1600
1601
1601
1602
1602 def urlify_commits(text_, repository):
1603 def urlify_commits(text_, repository):
1603 """
1604 """
1604 Extract commit ids from text and make link from them
1605 Extract commit ids from text and make link from them
1605
1606
1606 :param text_:
1607 :param text_:
1607 :param repository: repo name to build the URL with
1608 :param repository: repo name to build the URL with
1608 """
1609 """
1609
1610
1610 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1611 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1611
1612
1612 def url_func(match_obj):
1613 def url_func(match_obj):
1613 commit_id = match_obj.groups()[1]
1614 commit_id = match_obj.groups()[1]
1614 pref = match_obj.groups()[0]
1615 pref = match_obj.groups()[0]
1615 suf = match_obj.groups()[2]
1616 suf = match_obj.groups()[2]
1616
1617
1617 tmpl = (
1618 tmpl = (
1618 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1619 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1619 '%(commit_id)s</a>%(suf)s'
1620 '%(commit_id)s</a>%(suf)s'
1620 )
1621 )
1621 return tmpl % {
1622 return tmpl % {
1622 'pref': pref,
1623 'pref': pref,
1623 'cls': 'revision-link',
1624 'cls': 'revision-link',
1624 'url': route_url('repo_commit', repo_name=repository,
1625 'url': route_url('repo_commit', repo_name=repository,
1625 commit_id=commit_id),
1626 commit_id=commit_id),
1626 'commit_id': commit_id,
1627 'commit_id': commit_id,
1627 'suf': suf
1628 'suf': suf
1628 }
1629 }
1629
1630
1630 newtext = URL_PAT.sub(url_func, text_)
1631 newtext = URL_PAT.sub(url_func, text_)
1631
1632
1632 return newtext
1633 return newtext
1633
1634
1634
1635
1635 def _process_url_func(match_obj, repo_name, uid, entry,
1636 def _process_url_func(match_obj, repo_name, uid, entry,
1636 return_raw_data=False, link_format='html'):
1637 return_raw_data=False, link_format='html'):
1637 pref = ''
1638 pref = ''
1638 if match_obj.group().startswith(' '):
1639 if match_obj.group().startswith(' '):
1639 pref = ' '
1640 pref = ' '
1640
1641
1641 issue_id = ''.join(match_obj.groups())
1642 issue_id = ''.join(match_obj.groups())
1642
1643
1643 if link_format == 'html':
1644 if link_format == 'html':
1644 tmpl = (
1645 tmpl = (
1645 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1646 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1646 '%(issue-prefix)s%(id-repr)s'
1647 '%(issue-prefix)s%(id-repr)s'
1647 '</a>')
1648 '</a>')
1648 elif link_format == 'rst':
1649 elif link_format == 'rst':
1649 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1650 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1650 elif link_format == 'markdown':
1651 elif link_format == 'markdown':
1651 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1652 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1652 else:
1653 else:
1653 raise ValueError('Bad link_format:{}'.format(link_format))
1654 raise ValueError('Bad link_format:{}'.format(link_format))
1654
1655
1655 (repo_name_cleaned,
1656 (repo_name_cleaned,
1656 parent_group_name) = RepoGroupModel().\
1657 parent_group_name) = RepoGroupModel().\
1657 _get_group_name_and_parent(repo_name)
1658 _get_group_name_and_parent(repo_name)
1658
1659
1659 # variables replacement
1660 # variables replacement
1660 named_vars = {
1661 named_vars = {
1661 'id': issue_id,
1662 'id': issue_id,
1662 'repo': repo_name,
1663 'repo': repo_name,
1663 'repo_name': repo_name_cleaned,
1664 'repo_name': repo_name_cleaned,
1664 'group_name': parent_group_name
1665 'group_name': parent_group_name
1665 }
1666 }
1666 # named regex variables
1667 # named regex variables
1667 named_vars.update(match_obj.groupdict())
1668 named_vars.update(match_obj.groupdict())
1668 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1669 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1669
1670
1670 data = {
1671 data = {
1671 'pref': pref,
1672 'pref': pref,
1672 'cls': 'issue-tracker-link',
1673 'cls': 'issue-tracker-link',
1673 'url': _url,
1674 'url': _url,
1674 'id-repr': issue_id,
1675 'id-repr': issue_id,
1675 'issue-prefix': entry['pref'],
1676 'issue-prefix': entry['pref'],
1676 'serv': entry['url'],
1677 'serv': entry['url'],
1677 }
1678 }
1678 if return_raw_data:
1679 if return_raw_data:
1679 return {
1680 return {
1680 'id': issue_id,
1681 'id': issue_id,
1681 'url': _url
1682 'url': _url
1682 }
1683 }
1683 return tmpl % data
1684 return tmpl % data
1684
1685
1685
1686
1686 def process_patterns(text_string, repo_name, link_format='html'):
1687 def process_patterns(text_string, repo_name, link_format='html'):
1687 allowed_formats = ['html', 'rst', 'markdown']
1688 allowed_formats = ['html', 'rst', 'markdown']
1688 if link_format not in allowed_formats:
1689 if link_format not in allowed_formats:
1689 raise ValueError('Link format can be only one of:{} got {}'.format(
1690 raise ValueError('Link format can be only one of:{} got {}'.format(
1690 allowed_formats, link_format))
1691 allowed_formats, link_format))
1691
1692
1692 repo = None
1693 repo = None
1693 if repo_name:
1694 if repo_name:
1694 # Retrieving repo_name to avoid invalid repo_name to explode on
1695 # Retrieving repo_name to avoid invalid repo_name to explode on
1695 # IssueTrackerSettingsModel but still passing invalid name further down
1696 # IssueTrackerSettingsModel but still passing invalid name further down
1696 repo = Repository.get_by_repo_name(repo_name, cache=True)
1697 repo = Repository.get_by_repo_name(repo_name, cache=True)
1697
1698
1698 settings_model = IssueTrackerSettingsModel(repo=repo)
1699 settings_model = IssueTrackerSettingsModel(repo=repo)
1699 active_entries = settings_model.get_settings(cache=True)
1700 active_entries = settings_model.get_settings(cache=True)
1700
1701
1701 issues_data = []
1702 issues_data = []
1702 newtext = text_string
1703 newtext = text_string
1703
1704
1704 for uid, entry in active_entries.items():
1705 for uid, entry in active_entries.items():
1705 log.debug('found issue tracker entry with uid %s' % (uid,))
1706 log.debug('found issue tracker entry with uid %s' % (uid,))
1706
1707
1707 if not (entry['pat'] and entry['url']):
1708 if not (entry['pat'] and entry['url']):
1708 log.debug('skipping due to missing data')
1709 log.debug('skipping due to missing data')
1709 continue
1710 continue
1710
1711
1711 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1712 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1712 % (uid, entry['pat'], entry['url'], entry['pref']))
1713 % (uid, entry['pat'], entry['url'], entry['pref']))
1713
1714
1714 try:
1715 try:
1715 pattern = re.compile(r'%s' % entry['pat'])
1716 pattern = re.compile(r'%s' % entry['pat'])
1716 except re.error:
1717 except re.error:
1717 log.exception(
1718 log.exception(
1718 'issue tracker pattern: `%s` failed to compile',
1719 'issue tracker pattern: `%s` failed to compile',
1719 entry['pat'])
1720 entry['pat'])
1720 continue
1721 continue
1721
1722
1722 data_func = partial(
1723 data_func = partial(
1723 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1724 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1724 return_raw_data=True)
1725 return_raw_data=True)
1725
1726
1726 for match_obj in pattern.finditer(text_string):
1727 for match_obj in pattern.finditer(text_string):
1727 issues_data.append(data_func(match_obj))
1728 issues_data.append(data_func(match_obj))
1728
1729
1729 url_func = partial(
1730 url_func = partial(
1730 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1731 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1731 link_format=link_format)
1732 link_format=link_format)
1732
1733
1733 newtext = pattern.sub(url_func, newtext)
1734 newtext = pattern.sub(url_func, newtext)
1734 log.debug('processed prefix:uid `%s`' % (uid,))
1735 log.debug('processed prefix:uid `%s`' % (uid,))
1735
1736
1736 return newtext, issues_data
1737 return newtext, issues_data
1737
1738
1738
1739
1739 def urlify_commit_message(commit_text, repository=None):
1740 def urlify_commit_message(commit_text, repository=None):
1740 """
1741 """
1741 Parses given text message and makes proper links.
1742 Parses given text message and makes proper links.
1742 issues are linked to given issue-server, and rest is a commit link
1743 issues are linked to given issue-server, and rest is a commit link
1743
1744
1744 :param commit_text:
1745 :param commit_text:
1745 :param repository:
1746 :param repository:
1746 """
1747 """
1747 from pylons import url # doh, we need to re-import url to mock it later
1748 from pylons import url # doh, we need to re-import url to mock it later
1748
1749
1749 def escaper(string):
1750 def escaper(string):
1750 return string.replace('<', '&lt;').replace('>', '&gt;')
1751 return string.replace('<', '&lt;').replace('>', '&gt;')
1751
1752
1752 newtext = escaper(commit_text)
1753 newtext = escaper(commit_text)
1753
1754
1754 # extract http/https links and make them real urls
1755 # extract http/https links and make them real urls
1755 newtext = urlify_text(newtext, safe=False)
1756 newtext = urlify_text(newtext, safe=False)
1756
1757
1757 # urlify commits - extract commit ids and make link out of them, if we have
1758 # urlify commits - extract commit ids and make link out of them, if we have
1758 # the scope of repository present.
1759 # the scope of repository present.
1759 if repository:
1760 if repository:
1760 newtext = urlify_commits(newtext, repository)
1761 newtext = urlify_commits(newtext, repository)
1761
1762
1762 # process issue tracker patterns
1763 # process issue tracker patterns
1763 newtext, issues = process_patterns(newtext, repository or '')
1764 newtext, issues = process_patterns(newtext, repository or '')
1764
1765
1765 return literal(newtext)
1766 return literal(newtext)
1766
1767
1767
1768
1768 def render_binary(repo_name, file_obj):
1769 def render_binary(repo_name, file_obj):
1769 """
1770 """
1770 Choose how to render a binary file
1771 Choose how to render a binary file
1771 """
1772 """
1772 filename = file_obj.name
1773 filename = file_obj.name
1773
1774
1774 # images
1775 # images
1775 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1776 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1776 if fnmatch.fnmatch(filename, pat=ext):
1777 if fnmatch.fnmatch(filename, pat=ext):
1777 alt = filename
1778 alt = filename
1778 src = route_path(
1779 src = route_path(
1779 'repo_file_raw', repo_name=repo_name,
1780 'repo_file_raw', repo_name=repo_name,
1780 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1781 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1781 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1782 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1782
1783
1783
1784
1784 def renderer_from_filename(filename, exclude=None):
1785 def renderer_from_filename(filename, exclude=None):
1785 """
1786 """
1786 choose a renderer based on filename, this works only for text based files
1787 choose a renderer based on filename, this works only for text based files
1787 """
1788 """
1788
1789
1789 # ipython
1790 # ipython
1790 for ext in ['*.ipynb']:
1791 for ext in ['*.ipynb']:
1791 if fnmatch.fnmatch(filename, pat=ext):
1792 if fnmatch.fnmatch(filename, pat=ext):
1792 return 'jupyter'
1793 return 'jupyter'
1793
1794
1794 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1795 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1795 if is_markup:
1796 if is_markup:
1796 return is_markup
1797 return is_markup
1797 return None
1798 return None
1798
1799
1799
1800
1800 def render(source, renderer='rst', mentions=False, relative_urls=None,
1801 def render(source, renderer='rst', mentions=False, relative_urls=None,
1801 repo_name=None):
1802 repo_name=None):
1802
1803
1803 def maybe_convert_relative_links(html_source):
1804 def maybe_convert_relative_links(html_source):
1804 if relative_urls:
1805 if relative_urls:
1805 return relative_links(html_source, relative_urls)
1806 return relative_links(html_source, relative_urls)
1806 return html_source
1807 return html_source
1807
1808
1808 if renderer == 'rst':
1809 if renderer == 'rst':
1809 if repo_name:
1810 if repo_name:
1810 # process patterns on comments if we pass in repo name
1811 # process patterns on comments if we pass in repo name
1811 source, issues = process_patterns(
1812 source, issues = process_patterns(
1812 source, repo_name, link_format='rst')
1813 source, repo_name, link_format='rst')
1813
1814
1814 return literal(
1815 return literal(
1815 '<div class="rst-block">%s</div>' %
1816 '<div class="rst-block">%s</div>' %
1816 maybe_convert_relative_links(
1817 maybe_convert_relative_links(
1817 MarkupRenderer.rst(source, mentions=mentions)))
1818 MarkupRenderer.rst(source, mentions=mentions)))
1818 elif renderer == 'markdown':
1819 elif renderer == 'markdown':
1819 if repo_name:
1820 if repo_name:
1820 # process patterns on comments if we pass in repo name
1821 # process patterns on comments if we pass in repo name
1821 source, issues = process_patterns(
1822 source, issues = process_patterns(
1822 source, repo_name, link_format='markdown')
1823 source, repo_name, link_format='markdown')
1823
1824
1824 return literal(
1825 return literal(
1825 '<div class="markdown-block">%s</div>' %
1826 '<div class="markdown-block">%s</div>' %
1826 maybe_convert_relative_links(
1827 maybe_convert_relative_links(
1827 MarkupRenderer.markdown(source, flavored=True,
1828 MarkupRenderer.markdown(source, flavored=True,
1828 mentions=mentions)))
1829 mentions=mentions)))
1829 elif renderer == 'jupyter':
1830 elif renderer == 'jupyter':
1830 return literal(
1831 return literal(
1831 '<div class="ipynb">%s</div>' %
1832 '<div class="ipynb">%s</div>' %
1832 maybe_convert_relative_links(
1833 maybe_convert_relative_links(
1833 MarkupRenderer.jupyter(source)))
1834 MarkupRenderer.jupyter(source)))
1834
1835
1835 # None means just show the file-source
1836 # None means just show the file-source
1836 return None
1837 return None
1837
1838
1838
1839
1839 def commit_status(repo, commit_id):
1840 def commit_status(repo, commit_id):
1840 return ChangesetStatusModel().get_status(repo, commit_id)
1841 return ChangesetStatusModel().get_status(repo, commit_id)
1841
1842
1842
1843
1843 def commit_status_lbl(commit_status):
1844 def commit_status_lbl(commit_status):
1844 return dict(ChangesetStatus.STATUSES).get(commit_status)
1845 return dict(ChangesetStatus.STATUSES).get(commit_status)
1845
1846
1846
1847
1847 def commit_time(repo_name, commit_id):
1848 def commit_time(repo_name, commit_id):
1848 repo = Repository.get_by_repo_name(repo_name)
1849 repo = Repository.get_by_repo_name(repo_name)
1849 commit = repo.get_commit(commit_id=commit_id)
1850 commit = repo.get_commit(commit_id=commit_id)
1850 return commit.date
1851 return commit.date
1851
1852
1852
1853
1853 def get_permission_name(key):
1854 def get_permission_name(key):
1854 return dict(Permission.PERMS).get(key)
1855 return dict(Permission.PERMS).get(key)
1855
1856
1856
1857
1857 def journal_filter_help(request):
1858 def journal_filter_help(request):
1858 _ = request.translate
1859 _ = request.translate
1859
1860
1860 return _(
1861 return _(
1861 'Example filter terms:\n' +
1862 'Example filter terms:\n' +
1862 ' repository:vcs\n' +
1863 ' repository:vcs\n' +
1863 ' username:marcin\n' +
1864 ' username:marcin\n' +
1864 ' username:(NOT marcin)\n' +
1865 ' username:(NOT marcin)\n' +
1865 ' action:*push*\n' +
1866 ' action:*push*\n' +
1866 ' ip:127.0.0.1\n' +
1867 ' ip:127.0.0.1\n' +
1867 ' date:20120101\n' +
1868 ' date:20120101\n' +
1868 ' date:[20120101100000 TO 20120102]\n' +
1869 ' date:[20120101100000 TO 20120102]\n' +
1869 '\n' +
1870 '\n' +
1870 'Generate wildcards using \'*\' character:\n' +
1871 'Generate wildcards using \'*\' character:\n' +
1871 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1872 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1872 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1873 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1873 '\n' +
1874 '\n' +
1874 'Optional AND / OR operators in queries\n' +
1875 'Optional AND / OR operators in queries\n' +
1875 ' "repository:vcs OR repository:test"\n' +
1876 ' "repository:vcs OR repository:test"\n' +
1876 ' "username:test AND repository:test*"\n'
1877 ' "username:test AND repository:test*"\n'
1877 )
1878 )
1878
1879
1879
1880
1880 def search_filter_help(searcher, request):
1881 def search_filter_help(searcher, request):
1881 _ = request.translate
1882 _ = request.translate
1882
1883
1883 terms = ''
1884 terms = ''
1884 return _(
1885 return _(
1885 'Example filter terms for `{searcher}` search:\n' +
1886 'Example filter terms for `{searcher}` search:\n' +
1886 '{terms}\n' +
1887 '{terms}\n' +
1887 'Generate wildcards using \'*\' character:\n' +
1888 'Generate wildcards using \'*\' character:\n' +
1888 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1889 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1889 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1890 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1890 '\n' +
1891 '\n' +
1891 'Optional AND / OR operators in queries\n' +
1892 'Optional AND / OR operators in queries\n' +
1892 ' "repo_name:vcs OR repo_name:test"\n' +
1893 ' "repo_name:vcs OR repo_name:test"\n' +
1893 ' "owner:test AND repo_name:test*"\n' +
1894 ' "owner:test AND repo_name:test*"\n' +
1894 'More: {search_doc}'
1895 'More: {search_doc}'
1895 ).format(searcher=searcher.name,
1896 ).format(searcher=searcher.name,
1896 terms=terms, search_doc=searcher.query_lang_doc)
1897 terms=terms, search_doc=searcher.query_lang_doc)
1897
1898
1898
1899
1899 def not_mapped_error(repo_name):
1900 def not_mapped_error(repo_name):
1900 from rhodecode.translation import _
1901 from rhodecode.translation import _
1901 flash(_('%s repository is not mapped to db perhaps'
1902 flash(_('%s repository is not mapped to db perhaps'
1902 ' it was created or renamed from the filesystem'
1903 ' it was created or renamed from the filesystem'
1903 ' please run the application again'
1904 ' please run the application again'
1904 ' in order to rescan repositories') % repo_name, category='error')
1905 ' in order to rescan repositories') % repo_name, category='error')
1905
1906
1906
1907
1907 def ip_range(ip_addr):
1908 def ip_range(ip_addr):
1908 from rhodecode.model.db import UserIpMap
1909 from rhodecode.model.db import UserIpMap
1909 s, e = UserIpMap._get_ip_range(ip_addr)
1910 s, e = UserIpMap._get_ip_range(ip_addr)
1910 return '%s - %s' % (s, e)
1911 return '%s - %s' % (s, e)
1911
1912
1912
1913
1913 def form(url, method='post', needs_csrf_token=True, **attrs):
1914 def form(url, method='post', needs_csrf_token=True, **attrs):
1914 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1915 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1915 if method.lower() != 'get' and needs_csrf_token:
1916 if method.lower() != 'get' and needs_csrf_token:
1916 raise Exception(
1917 raise Exception(
1917 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1918 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1918 'CSRF token. If the endpoint does not require such token you can ' +
1919 'CSRF token. If the endpoint does not require such token you can ' +
1919 'explicitly set the parameter needs_csrf_token to false.')
1920 'explicitly set the parameter needs_csrf_token to false.')
1920
1921
1921 return wh_form(url, method=method, **attrs)
1922 return wh_form(url, method=method, **attrs)
1922
1923
1923
1924
1924 def secure_form(form_url, method="POST", multipart=False, **attrs):
1925 def secure_form(form_url, method="POST", multipart=False, **attrs):
1925 """Start a form tag that points the action to an url. This
1926 """Start a form tag that points the action to an url. This
1926 form tag will also include the hidden field containing
1927 form tag will also include the hidden field containing
1927 the auth token.
1928 the auth token.
1928
1929
1929 The url options should be given either as a string, or as a
1930 The url options should be given either as a string, or as a
1930 ``url()`` function. The method for the form defaults to POST.
1931 ``url()`` function. The method for the form defaults to POST.
1931
1932
1932 Options:
1933 Options:
1933
1934
1934 ``multipart``
1935 ``multipart``
1935 If set to True, the enctype is set to "multipart/form-data".
1936 If set to True, the enctype is set to "multipart/form-data".
1936 ``method``
1937 ``method``
1937 The method to use when submitting the form, usually either
1938 The method to use when submitting the form, usually either
1938 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1939 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1939 hidden input with name _method is added to simulate the verb
1940 hidden input with name _method is added to simulate the verb
1940 over POST.
1941 over POST.
1941
1942
1942 """
1943 """
1943 from webhelpers.pylonslib.secure_form import insecure_form
1944 from webhelpers.pylonslib.secure_form import insecure_form
1944
1945
1945 session = None
1946 session = None
1946
1947
1947 # TODO(marcink): after pyramid migration require request variable ALWAYS
1948 # TODO(marcink): after pyramid migration require request variable ALWAYS
1948 if 'request' in attrs:
1949 if 'request' in attrs:
1949 session = attrs['request'].session
1950 session = attrs['request'].session
1950 del attrs['request']
1951 del attrs['request']
1951
1952
1952 form = insecure_form(form_url, method, multipart, **attrs)
1953 form = insecure_form(form_url, method, multipart, **attrs)
1953 token = literal(
1954 token = literal(
1954 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1955 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1955 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1956 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1956
1957
1957 return literal("%s\n%s" % (form, token))
1958 return literal("%s\n%s" % (form, token))
1958
1959
1959
1960
1960 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1961 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1961 select_html = select(name, selected, options, **attrs)
1962 select_html = select(name, selected, options, **attrs)
1962 select2 = """
1963 select2 = """
1963 <script>
1964 <script>
1964 $(document).ready(function() {
1965 $(document).ready(function() {
1965 $('#%s').select2({
1966 $('#%s').select2({
1966 containerCssClass: 'drop-menu',
1967 containerCssClass: 'drop-menu',
1967 dropdownCssClass: 'drop-menu-dropdown',
1968 dropdownCssClass: 'drop-menu-dropdown',
1968 dropdownAutoWidth: true%s
1969 dropdownAutoWidth: true%s
1969 });
1970 });
1970 });
1971 });
1971 </script>
1972 </script>
1972 """
1973 """
1973 filter_option = """,
1974 filter_option = """,
1974 minimumResultsForSearch: -1
1975 minimumResultsForSearch: -1
1975 """
1976 """
1976 input_id = attrs.get('id') or name
1977 input_id = attrs.get('id') or name
1977 filter_enabled = "" if enable_filter else filter_option
1978 filter_enabled = "" if enable_filter else filter_option
1978 select_script = literal(select2 % (input_id, filter_enabled))
1979 select_script = literal(select2 % (input_id, filter_enabled))
1979
1980
1980 return literal(select_html+select_script)
1981 return literal(select_html+select_script)
1981
1982
1982
1983
1983 def get_visual_attr(tmpl_context_var, attr_name):
1984 def get_visual_attr(tmpl_context_var, attr_name):
1984 """
1985 """
1985 A safe way to get a variable from visual variable of template context
1986 A safe way to get a variable from visual variable of template context
1986
1987
1987 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1988 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1988 :param attr_name: name of the attribute we fetch from the c.visual
1989 :param attr_name: name of the attribute we fetch from the c.visual
1989 """
1990 """
1990 visual = getattr(tmpl_context_var, 'visual', None)
1991 visual = getattr(tmpl_context_var, 'visual', None)
1991 if not visual:
1992 if not visual:
1992 return
1993 return
1993 else:
1994 else:
1994 return getattr(visual, attr_name, None)
1995 return getattr(visual, attr_name, None)
1995
1996
1996
1997
1997 def get_last_path_part(file_node):
1998 def get_last_path_part(file_node):
1998 if not file_node.path:
1999 if not file_node.path:
1999 return u''
2000 return u''
2000
2001
2001 path = safe_unicode(file_node.path.split('/')[-1])
2002 path = safe_unicode(file_node.path.split('/')[-1])
2002 return u'../' + path
2003 return u'../' + path
2003
2004
2004
2005
2005 def route_url(*args, **kwargs):
2006 def route_url(*args, **kwargs):
2006 """
2007 """
2007 Wrapper around pyramids `route_url` (fully qualified url) function.
2008 Wrapper around pyramids `route_url` (fully qualified url) function.
2008 It is used to generate URLs from within pylons views or templates.
2009 It is used to generate URLs from within pylons views or templates.
2009 This will be removed when pyramid migration if finished.
2010 This will be removed when pyramid migration if finished.
2010 """
2011 """
2011 req = get_current_request()
2012 req = get_current_request()
2012 return req.route_url(*args, **kwargs)
2013 return req.route_url(*args, **kwargs)
2013
2014
2014
2015
2015 def route_path(*args, **kwargs):
2016 def route_path(*args, **kwargs):
2016 """
2017 """
2017 Wrapper around pyramids `route_path` function. It is used to generate
2018 Wrapper around pyramids `route_path` function. It is used to generate
2018 URLs from within pylons views or templates. This will be removed when
2019 URLs from within pylons views or templates. This will be removed when
2019 pyramid migration if finished.
2020 pyramid migration if finished.
2020 """
2021 """
2021 req = get_current_request()
2022 req = get_current_request()
2022 return req.route_path(*args, **kwargs)
2023 return req.route_path(*args, **kwargs)
2023
2024
2024
2025
2025 def route_path_or_none(*args, **kwargs):
2026 def route_path_or_none(*args, **kwargs):
2026 try:
2027 try:
2027 return route_path(*args, **kwargs)
2028 return route_path(*args, **kwargs)
2028 except KeyError:
2029 except KeyError:
2029 return None
2030 return None
2030
2031
2031
2032
2032 def current_route_path(request, **kw):
2033 def current_route_path(request, **kw):
2033 new_args = request.GET.mixed()
2034 new_args = request.GET.mixed()
2034 new_args.update(kw)
2035 new_args.update(kw)
2035 return request.current_route_path(_query=new_args)
2036 return request.current_route_path(_query=new_args)
2036
2037
2037
2038
2038 def static_url(*args, **kwds):
2039 def static_url(*args, **kwds):
2039 """
2040 """
2040 Wrapper around pyramids `route_path` function. It is used to generate
2041 Wrapper around pyramids `route_path` function. It is used to generate
2041 URLs from within pylons views or templates. This will be removed when
2042 URLs from within pylons views or templates. This will be removed when
2042 pyramid migration if finished.
2043 pyramid migration if finished.
2043 """
2044 """
2044 req = get_current_request()
2045 req = get_current_request()
2045 return req.static_url(*args, **kwds)
2046 return req.static_url(*args, **kwds)
2046
2047
2047
2048
2048 def resource_path(*args, **kwds):
2049 def resource_path(*args, **kwds):
2049 """
2050 """
2050 Wrapper around pyramids `route_path` function. It is used to generate
2051 Wrapper around pyramids `route_path` function. It is used to generate
2051 URLs from within pylons views or templates. This will be removed when
2052 URLs from within pylons views or templates. This will be removed when
2052 pyramid migration if finished.
2053 pyramid migration if finished.
2053 """
2054 """
2054 req = get_current_request()
2055 req = get_current_request()
2055 return req.resource_path(*args, **kwds)
2056 return req.resource_path(*args, **kwds)
2056
2057
2057
2058
2058 def api_call_example(method, args):
2059 def api_call_example(method, args):
2059 """
2060 """
2060 Generates an API call example via CURL
2061 Generates an API call example via CURL
2061 """
2062 """
2062 args_json = json.dumps(OrderedDict([
2063 args_json = json.dumps(OrderedDict([
2063 ('id', 1),
2064 ('id', 1),
2064 ('auth_token', 'SECRET'),
2065 ('auth_token', 'SECRET'),
2065 ('method', method),
2066 ('method', method),
2066 ('args', args)
2067 ('args', args)
2067 ]))
2068 ]))
2068 return literal(
2069 return literal(
2069 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2070 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2070 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2071 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2071 "and needs to be of `api calls` role."
2072 "and needs to be of `api calls` role."
2072 .format(
2073 .format(
2073 api_url=route_url('apiv2'),
2074 api_url=route_url('apiv2'),
2074 token_url=route_url('my_account_auth_tokens'),
2075 token_url=route_url('my_account_auth_tokens'),
2075 data=args_json))
2076 data=args_json))
2076
2077
2077
2078
2078 def notification_description(notification, request):
2079 def notification_description(notification, request):
2079 """
2080 """
2080 Generate notification human readable description based on notification type
2081 Generate notification human readable description based on notification type
2081 """
2082 """
2082 from rhodecode.model.notification import NotificationModel
2083 from rhodecode.model.notification import NotificationModel
2083 return NotificationModel().make_description(
2084 return NotificationModel().make_description(
2084 notification, translate=request.translate)
2085 notification, translate=request.translate)
2085
2086
2086
2087
2087 def go_import_header(request, db_repo=None):
2088 def go_import_header(request, db_repo=None):
2088 """
2089 """
2089 Creates a header for go-import functionality in Go Lang
2090 Creates a header for go-import functionality in Go Lang
2090 """
2091 """
2091
2092
2092 if not db_repo:
2093 if not db_repo:
2093 return
2094 return
2094 if 'go-get' not in request.GET:
2095 if 'go-get' not in request.GET:
2095 return
2096 return
2096
2097
2097 clone_url = db_repo.clone_url()
2098 clone_url = db_repo.clone_url()
2098 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2099 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2099 # we have a repo and go-get flag,
2100 # we have a repo and go-get flag,
2100 return literal('<meta name="go-import" content="{} {} {}">'.format(
2101 return literal('<meta name="go-import" content="{} {} {}">'.format(
2101 prefix, db_repo.repo_type, clone_url))
2102 prefix, db_repo.repo_type, clone_url))
@@ -1,97 +1,98 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 from mako import exceptions
23 from mako import exceptions
24 from pyramid.renderers import get_renderer
24 from pyramid.renderers import get_renderer
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28
28
29 def get_partial_renderer(request, tmpl_name):
29 def get_partial_renderer(request, tmpl_name):
30 return PyramidPartialRenderer(request, tmpl_name=tmpl_name)
30 return PyramidPartialRenderer(request, tmpl_name=tmpl_name)
31
31
32
32
33 class PyramidPartialRenderer(object):
33 class PyramidPartialRenderer(object):
34
34
35 """
35 """
36 Partial renderer used to render chunks of html used in datagrids
36 Partial renderer used to render chunks of html used in datagrids
37 use like::
37 use like::
38
38
39 _renderer = request.get_partial_renderer('_dt/template_base.mako')
39 _renderer = request.get_partial_renderer(
40 'rhodecode:templates/_dt/template_base.mako')
40 _render('quick_menu', args, kwargs)
41 _render('quick_menu', args, kwargs)
41
42
42 :param tmpl_name: template path relate to /templates/ dir
43 :param tmpl_name: template path relate to /templates/ dir
43 """
44 """
44
45
45 def __init__(self, request, tmpl_name):
46 def __init__(self, request, tmpl_name):
46 self.tmpl_name = tmpl_name
47 self.tmpl_name = tmpl_name
47 self.request = request
48 self.request = request
48
49
49 def _mako_lookup(self):
50 def _mako_lookup(self):
50 _tmpl_lookup = get_renderer('root.mako').lookup
51 _tmpl_lookup = get_renderer('root.mako').lookup
51 return _tmpl_lookup.get_template(self.tmpl_name)
52 return _tmpl_lookup.get_template(self.tmpl_name)
52
53
53 def get_call_context(self):
54 def get_call_context(self):
54 return self.request.call_context
55 return self.request.call_context
55
56
56 def get_helpers(self):
57 def get_helpers(self):
57 from rhodecode.lib import helpers
58 from rhodecode.lib import helpers
58 return helpers
59 return helpers
59
60
60 def _update_kwargs_for_render(self, kwargs):
61 def _update_kwargs_for_render(self, kwargs):
61 """
62 """
62 Inject params required for Mako rendering
63 Inject params required for Mako rendering
63 """
64 """
64
65
65 _kwargs = {
66 _kwargs = {
66 '_': self.request.translate,
67 '_': self.request.translate,
67 '_ungettext': self.request.plularize,
68 '_ungettext': self.request.plularize,
68 'h': self.get_helpers(),
69 'h': self.get_helpers(),
69 'c': self.get_call_context(),
70 'c': self.get_call_context(),
70
71
71 'request': self.request,
72 'request': self.request,
72 }
73 }
73 _kwargs.update(kwargs)
74 _kwargs.update(kwargs)
74 return _kwargs
75 return _kwargs
75
76
76 def _render_with_exc(self, render_func, args, kwargs):
77 def _render_with_exc(self, render_func, args, kwargs):
77 try:
78 try:
78 return render_func.render(*args, **kwargs)
79 return render_func.render(*args, **kwargs)
79 except:
80 except:
80 log.error(exceptions.text_error_template().render())
81 log.error(exceptions.text_error_template().render())
81 raise
82 raise
82
83
83 def _get_template(self, template_obj, def_name):
84 def _get_template(self, template_obj, def_name):
84 if def_name:
85 if def_name:
85 tmpl = template_obj.get_def(def_name)
86 tmpl = template_obj.get_def(def_name)
86 else:
87 else:
87 tmpl = template_obj
88 tmpl = template_obj
88 return tmpl
89 return tmpl
89
90
90 def render(self, def_name, *args, **kwargs):
91 def render(self, def_name, *args, **kwargs):
91 lookup_obj = self._mako_lookup()
92 lookup_obj = self._mako_lookup()
92 tmpl = self._get_template(lookup_obj, def_name=def_name)
93 tmpl = self._get_template(lookup_obj, def_name=def_name)
93 kwargs = self._update_kwargs_for_render(kwargs)
94 kwargs = self._update_kwargs_for_render(kwargs)
94 return self._render_with_exc(tmpl, args, kwargs)
95 return self._render_with_exc(tmpl, args, kwargs)
95
96
96 def __call__(self, tmpl, *args, **kwargs):
97 def __call__(self, tmpl, *args, **kwargs):
97 return self.render(tmpl, *args, **kwargs)
98 return self.render(tmpl, *args, **kwargs)
@@ -1,377 +1,386 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Model for notifications
23 Model for notifications
24 """
24 """
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pyramid.threadlocal import get_current_request
29 from pyramid.threadlocal import get_current_request
30 from sqlalchemy.sql.expression import false, true
30 from sqlalchemy.sql.expression import false, true
31
31
32 import rhodecode
32 import rhodecode
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import Notification, User, UserNotification
35 from rhodecode.model.db import Notification, User, UserNotification
36 from rhodecode.model.meta import Session
36 from rhodecode.model.meta import Session
37 from rhodecode.translation import TranslationString
37 from rhodecode.translation import TranslationString
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class NotificationModel(BaseModel):
42 class NotificationModel(BaseModel):
43
43
44 cls = Notification
44 cls = Notification
45
45
46 def __get_notification(self, notification):
46 def __get_notification(self, notification):
47 if isinstance(notification, Notification):
47 if isinstance(notification, Notification):
48 return notification
48 return notification
49 elif isinstance(notification, (int, long)):
49 elif isinstance(notification, (int, long)):
50 return Notification.get(notification)
50 return Notification.get(notification)
51 else:
51 else:
52 if notification:
52 if notification:
53 raise Exception('notification must be int, long or Instance'
53 raise Exception('notification must be int, long or Instance'
54 ' of Notification got %s' % type(notification))
54 ' of Notification got %s' % type(notification))
55
55
56 def create(
56 def create(
57 self, created_by, notification_subject, notification_body,
57 self, created_by, notification_subject, notification_body,
58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 mention_recipients=None, with_email=True, email_kwargs=None):
59 mention_recipients=None, with_email=True, email_kwargs=None):
60 """
60 """
61
61
62 Creates notification of given type
62 Creates notification of given type
63
63
64 :param created_by: int, str or User instance. User who created this
64 :param created_by: int, str or User instance. User who created this
65 notification
65 notification
66 :param notification_subject: subject of notification itself
66 :param notification_subject: subject of notification itself
67 :param notification_body: body of notification text
67 :param notification_body: body of notification text
68 :param notification_type: type of notification, based on that we
68 :param notification_type: type of notification, based on that we
69 pick templates
69 pick templates
70
70
71 :param recipients: list of int, str or User objects, when None
71 :param recipients: list of int, str or User objects, when None
72 is given send to all admins
72 is given send to all admins
73 :param mention_recipients: list of int, str or User objects,
73 :param mention_recipients: list of int, str or User objects,
74 that were mentioned
74 that were mentioned
75 :param with_email: send email with this notification
75 :param with_email: send email with this notification
76 :param email_kwargs: dict with arguments to generate email
76 :param email_kwargs: dict with arguments to generate email
77 """
77 """
78
78
79 from rhodecode.lib.celerylib import tasks, run_task
79 from rhodecode.lib.celerylib import tasks, run_task
80
80
81 if recipients and not getattr(recipients, '__iter__', False):
81 if recipients and not getattr(recipients, '__iter__', False):
82 raise Exception('recipients must be an iterable object')
82 raise Exception('recipients must be an iterable object')
83
83
84 created_by_obj = self._get_user(created_by)
84 created_by_obj = self._get_user(created_by)
85 # default MAIN body if not given
85 # default MAIN body if not given
86 email_kwargs = email_kwargs or {'body': notification_body}
86 email_kwargs = email_kwargs or {'body': notification_body}
87 mention_recipients = mention_recipients or set()
87 mention_recipients = mention_recipients or set()
88
88
89 if not created_by_obj:
89 if not created_by_obj:
90 raise Exception('unknown user %s' % created_by)
90 raise Exception('unknown user %s' % created_by)
91
91
92 if recipients is None:
92 if recipients is None:
93 # recipients is None means to all admins
93 # recipients is None means to all admins
94 recipients_objs = User.query().filter(User.admin == true()).all()
94 recipients_objs = User.query().filter(User.admin == true()).all()
95 log.debug('sending notifications %s to admins: %s',
95 log.debug('sending notifications %s to admins: %s',
96 notification_type, recipients_objs)
96 notification_type, recipients_objs)
97 else:
97 else:
98 recipients_objs = []
98 recipients_objs = []
99 for u in recipients:
99 for u in recipients:
100 obj = self._get_user(u)
100 obj = self._get_user(u)
101 if obj:
101 if obj:
102 recipients_objs.append(obj)
102 recipients_objs.append(obj)
103 else: # we didn't find this user, log the error and carry on
103 else: # we didn't find this user, log the error and carry on
104 log.error('cannot notify unknown user %r', u)
104 log.error('cannot notify unknown user %r', u)
105
105
106 recipients_objs = set(recipients_objs)
106 recipients_objs = set(recipients_objs)
107 if not recipients_objs:
107 if not recipients_objs:
108 raise Exception('no valid recipients specified')
108 raise Exception('no valid recipients specified')
109
109
110 log.debug('sending notifications %s to %s',
110 log.debug('sending notifications %s to %s',
111 notification_type, recipients_objs)
111 notification_type, recipients_objs)
112
112
113 # add mentioned users into recipients
113 # add mentioned users into recipients
114 final_recipients = set(recipients_objs).union(mention_recipients)
114 final_recipients = set(recipients_objs).union(mention_recipients)
115 notification = Notification.create(
115 notification = Notification.create(
116 created_by=created_by_obj, subject=notification_subject,
116 created_by=created_by_obj, subject=notification_subject,
117 body=notification_body, recipients=final_recipients,
117 body=notification_body, recipients=final_recipients,
118 type_=notification_type
118 type_=notification_type
119 )
119 )
120
120
121 if not with_email: # skip sending email, and just create notification
121 if not with_email: # skip sending email, and just create notification
122 return notification
122 return notification
123
123
124 # don't send email to person who created this comment
124 # don't send email to person who created this comment
125 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
125 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
126
126
127 # now notify all recipients in question
127 # now notify all recipients in question
128
128
129 for recipient in rec_objs.union(mention_recipients):
129 for recipient in rec_objs.union(mention_recipients):
130 # inject current recipient
130 # inject current recipient
131 email_kwargs['recipient'] = recipient
131 email_kwargs['recipient'] = recipient
132 email_kwargs['mention'] = recipient in mention_recipients
132 email_kwargs['mention'] = recipient in mention_recipients
133 (subject, headers, email_body,
133 (subject, headers, email_body,
134 email_body_plaintext) = EmailNotificationModel().render_email(
134 email_body_plaintext) = EmailNotificationModel().render_email(
135 notification_type, **email_kwargs)
135 notification_type, **email_kwargs)
136
136
137 log.debug(
137 log.debug(
138 'Creating notification email task for user:`%s`', recipient)
138 'Creating notification email task for user:`%s`', recipient)
139 task = run_task(
139 task = run_task(
140 tasks.send_email, recipient.email, subject,
140 tasks.send_email, recipient.email, subject,
141 email_body_plaintext, email_body)
141 email_body_plaintext, email_body)
142 log.debug('Created email task: %s', task)
142 log.debug('Created email task: %s', task)
143
143
144 return notification
144 return notification
145
145
146 def delete(self, user, notification):
146 def delete(self, user, notification):
147 # we don't want to remove actual notification just the assignment
147 # we don't want to remove actual notification just the assignment
148 try:
148 try:
149 notification = self.__get_notification(notification)
149 notification = self.__get_notification(notification)
150 user = self._get_user(user)
150 user = self._get_user(user)
151 if notification and user:
151 if notification and user:
152 obj = UserNotification.query()\
152 obj = UserNotification.query()\
153 .filter(UserNotification.user == user)\
153 .filter(UserNotification.user == user)\
154 .filter(UserNotification.notification == notification)\
154 .filter(UserNotification.notification == notification)\
155 .one()
155 .one()
156 Session().delete(obj)
156 Session().delete(obj)
157 return True
157 return True
158 except Exception:
158 except Exception:
159 log.error(traceback.format_exc())
159 log.error(traceback.format_exc())
160 raise
160 raise
161
161
162 def get_for_user(self, user, filter_=None):
162 def get_for_user(self, user, filter_=None):
163 """
163 """
164 Get mentions for given user, filter them if filter dict is given
164 Get mentions for given user, filter them if filter dict is given
165 """
165 """
166 user = self._get_user(user)
166 user = self._get_user(user)
167
167
168 q = UserNotification.query()\
168 q = UserNotification.query()\
169 .filter(UserNotification.user == user)\
169 .filter(UserNotification.user == user)\
170 .join((
170 .join((
171 Notification, UserNotification.notification_id ==
171 Notification, UserNotification.notification_id ==
172 Notification.notification_id))
172 Notification.notification_id))
173 if filter_ == ['all']:
173 if filter_ == ['all']:
174 q = q # no filter
174 q = q # no filter
175 elif filter_ == ['unread']:
175 elif filter_ == ['unread']:
176 q = q.filter(UserNotification.read == false())
176 q = q.filter(UserNotification.read == false())
177 elif filter_:
177 elif filter_:
178 q = q.filter(Notification.type_.in_(filter_))
178 q = q.filter(Notification.type_.in_(filter_))
179
179
180 return q
180 return q
181
181
182 def mark_read(self, user, notification):
182 def mark_read(self, user, notification):
183 try:
183 try:
184 notification = self.__get_notification(notification)
184 notification = self.__get_notification(notification)
185 user = self._get_user(user)
185 user = self._get_user(user)
186 if notification and user:
186 if notification and user:
187 obj = UserNotification.query()\
187 obj = UserNotification.query()\
188 .filter(UserNotification.user == user)\
188 .filter(UserNotification.user == user)\
189 .filter(UserNotification.notification == notification)\
189 .filter(UserNotification.notification == notification)\
190 .one()
190 .one()
191 obj.read = True
191 obj.read = True
192 Session().add(obj)
192 Session().add(obj)
193 return True
193 return True
194 except Exception:
194 except Exception:
195 log.error(traceback.format_exc())
195 log.error(traceback.format_exc())
196 raise
196 raise
197
197
198 def mark_all_read_for_user(self, user, filter_=None):
198 def mark_all_read_for_user(self, user, filter_=None):
199 user = self._get_user(user)
199 user = self._get_user(user)
200 q = UserNotification.query()\
200 q = UserNotification.query()\
201 .filter(UserNotification.user == user)\
201 .filter(UserNotification.user == user)\
202 .filter(UserNotification.read == false())\
202 .filter(UserNotification.read == false())\
203 .join((
203 .join((
204 Notification, UserNotification.notification_id ==
204 Notification, UserNotification.notification_id ==
205 Notification.notification_id))
205 Notification.notification_id))
206 if filter_ == ['unread']:
206 if filter_ == ['unread']:
207 q = q.filter(UserNotification.read == false())
207 q = q.filter(UserNotification.read == false())
208 elif filter_:
208 elif filter_:
209 q = q.filter(Notification.type_.in_(filter_))
209 q = q.filter(Notification.type_.in_(filter_))
210
210
211 # this is a little inefficient but sqlalchemy doesn't support
211 # this is a little inefficient but sqlalchemy doesn't support
212 # update on joined tables :(
212 # update on joined tables :(
213 for obj in q.all():
213 for obj in q.all():
214 obj.read = True
214 obj.read = True
215 Session().add(obj)
215 Session().add(obj)
216
216
217 def get_unread_cnt_for_user(self, user):
217 def get_unread_cnt_for_user(self, user):
218 user = self._get_user(user)
218 user = self._get_user(user)
219 return UserNotification.query()\
219 return UserNotification.query()\
220 .filter(UserNotification.read == false())\
220 .filter(UserNotification.read == false())\
221 .filter(UserNotification.user == user).count()
221 .filter(UserNotification.user == user).count()
222
222
223 def get_unread_for_user(self, user):
223 def get_unread_for_user(self, user):
224 user = self._get_user(user)
224 user = self._get_user(user)
225 return [x.notification for x in UserNotification.query()
225 return [x.notification for x in UserNotification.query()
226 .filter(UserNotification.read == false())
226 .filter(UserNotification.read == false())
227 .filter(UserNotification.user == user).all()]
227 .filter(UserNotification.user == user).all()]
228
228
229 def get_user_notification(self, user, notification):
229 def get_user_notification(self, user, notification):
230 user = self._get_user(user)
230 user = self._get_user(user)
231 notification = self.__get_notification(notification)
231 notification = self.__get_notification(notification)
232
232
233 return UserNotification.query()\
233 return UserNotification.query()\
234 .filter(UserNotification.notification == notification)\
234 .filter(UserNotification.notification == notification)\
235 .filter(UserNotification.user == user).scalar()
235 .filter(UserNotification.user == user).scalar()
236
236
237 def make_description(self, notification, translate, show_age=True):
237 def make_description(self, notification, translate, show_age=True):
238 """
238 """
239 Creates a human readable description based on properties
239 Creates a human readable description based on properties
240 of notification object
240 of notification object
241 """
241 """
242 _ = translate
242 _ = translate
243 _map = {
243 _map = {
244 notification.TYPE_CHANGESET_COMMENT: [
244 notification.TYPE_CHANGESET_COMMENT: [
245 _('%(user)s commented on commit %(date_or_age)s'),
245 _('%(user)s commented on commit %(date_or_age)s'),
246 _('%(user)s commented on commit at %(date_or_age)s'),
246 _('%(user)s commented on commit at %(date_or_age)s'),
247 ],
247 ],
248 notification.TYPE_MESSAGE: [
248 notification.TYPE_MESSAGE: [
249 _('%(user)s sent message %(date_or_age)s'),
249 _('%(user)s sent message %(date_or_age)s'),
250 _('%(user)s sent message at %(date_or_age)s'),
250 _('%(user)s sent message at %(date_or_age)s'),
251 ],
251 ],
252 notification.TYPE_MENTION: [
252 notification.TYPE_MENTION: [
253 _('%(user)s mentioned you %(date_or_age)s'),
253 _('%(user)s mentioned you %(date_or_age)s'),
254 _('%(user)s mentioned you at %(date_or_age)s'),
254 _('%(user)s mentioned you at %(date_or_age)s'),
255 ],
255 ],
256 notification.TYPE_REGISTRATION: [
256 notification.TYPE_REGISTRATION: [
257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
259 ],
259 ],
260 notification.TYPE_PULL_REQUEST: [
260 notification.TYPE_PULL_REQUEST: [
261 _('%(user)s opened new pull request %(date_or_age)s'),
261 _('%(user)s opened new pull request %(date_or_age)s'),
262 _('%(user)s opened new pull request at %(date_or_age)s'),
262 _('%(user)s opened new pull request at %(date_or_age)s'),
263 ],
263 ],
264 notification.TYPE_PULL_REQUEST_COMMENT: [
264 notification.TYPE_PULL_REQUEST_COMMENT: [
265 _('%(user)s commented on pull request %(date_or_age)s'),
265 _('%(user)s commented on pull request %(date_or_age)s'),
266 _('%(user)s commented on pull request at %(date_or_age)s'),
266 _('%(user)s commented on pull request at %(date_or_age)s'),
267 ],
267 ],
268 }
268 }
269
269
270 templates = _map[notification.type_]
270 templates = _map[notification.type_]
271
271
272 if show_age:
272 if show_age:
273 template = templates[0]
273 template = templates[0]
274 date_or_age = h.age(notification.created_on)
274 date_or_age = h.age(notification.created_on)
275 if translate:
275 if translate:
276 date_or_age = translate(date_or_age)
276 date_or_age = translate(date_or_age)
277
277
278 if isinstance(date_or_age, TranslationString):
278 if isinstance(date_or_age, TranslationString):
279 date_or_age = date_or_age.interpolate()
279 date_or_age = date_or_age.interpolate()
280
280
281 else:
281 else:
282 template = templates[1]
282 template = templates[1]
283 date_or_age = h.format_date(notification.created_on)
283 date_or_age = h.format_date(notification.created_on)
284
284
285 return template % {
285 return template % {
286 'user': notification.created_by_user.username,
286 'user': notification.created_by_user.username,
287 'date_or_age': date_or_age,
287 'date_or_age': date_or_age,
288 }
288 }
289
289
290
290
291 class EmailNotificationModel(BaseModel):
291 class EmailNotificationModel(BaseModel):
292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
296 TYPE_MAIN = Notification.TYPE_MESSAGE
296 TYPE_MAIN = Notification.TYPE_MESSAGE
297
297
298 TYPE_PASSWORD_RESET = 'password_reset'
298 TYPE_PASSWORD_RESET = 'password_reset'
299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
300 TYPE_EMAIL_TEST = 'email_test'
300 TYPE_EMAIL_TEST = 'email_test'
301 TYPE_TEST = 'test'
301 TYPE_TEST = 'test'
302
302
303 email_types = {
303 email_types = {
304 TYPE_MAIN: 'email_templates/main.mako',
304 TYPE_MAIN:
305 TYPE_TEST: 'email_templates/test.mako',
305 'rhodecode:templates/email_templates/main.mako',
306 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
306 TYPE_TEST:
307 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
307 'rhodecode:templates/email_templates/test.mako',
308 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
308 TYPE_EMAIL_TEST:
309 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
309 'rhodecode:templates/email_templates/email_test.mako',
310 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
310 TYPE_REGISTRATION:
311 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
311 'rhodecode:templates/email_templates/user_registration.mako',
312 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
312 TYPE_PASSWORD_RESET:
313 'rhodecode:templates/email_templates/password_reset.mako',
314 TYPE_PASSWORD_RESET_CONFIRMATION:
315 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
316 TYPE_COMMIT_COMMENT:
317 'rhodecode:templates/email_templates/commit_comment.mako',
318 TYPE_PULL_REQUEST:
319 'rhodecode:templates/email_templates/pull_request_review.mako',
320 TYPE_PULL_REQUEST_COMMENT:
321 'rhodecode:templates/email_templates/pull_request_comment.mako',
313 }
322 }
314
323
315 def __init__(self):
324 def __init__(self):
316 """
325 """
317 Example usage::
326 Example usage::
318
327
319 (subject, headers, email_body,
328 (subject, headers, email_body,
320 email_body_plaintext) = EmailNotificationModel().render_email(
329 email_body_plaintext) = EmailNotificationModel().render_email(
321 EmailNotificationModel.TYPE_TEST, **email_kwargs)
330 EmailNotificationModel.TYPE_TEST, **email_kwargs)
322
331
323 """
332 """
324 super(EmailNotificationModel, self).__init__()
333 super(EmailNotificationModel, self).__init__()
325 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
334 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
326
335
327 def _update_kwargs_for_render(self, kwargs):
336 def _update_kwargs_for_render(self, kwargs):
328 """
337 """
329 Inject params required for Mako rendering
338 Inject params required for Mako rendering
330
339
331 :param kwargs:
340 :param kwargs:
332 """
341 """
333
342
334 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
343 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
335 instance_url = h.route_url('home')
344 instance_url = h.route_url('home')
336 _kwargs = {
345 _kwargs = {
337 'instance_url': instance_url,
346 'instance_url': instance_url,
338 'whitespace_filter': self.whitespace_filter
347 'whitespace_filter': self.whitespace_filter
339 }
348 }
340 _kwargs.update(kwargs)
349 _kwargs.update(kwargs)
341 return _kwargs
350 return _kwargs
342
351
343 def whitespace_filter(self, text):
352 def whitespace_filter(self, text):
344 return text.replace('\n', '').replace('\t', '')
353 return text.replace('\n', '').replace('\t', '')
345
354
346 def get_renderer(self, type_, request):
355 def get_renderer(self, type_, request):
347 template_name = self.email_types[type_]
356 template_name = self.email_types[type_]
348 return request.get_partial_renderer(template_name)
357 return request.get_partial_renderer(template_name)
349
358
350 def render_email(self, type_, **kwargs):
359 def render_email(self, type_, **kwargs):
351 """
360 """
352 renders template for email, and returns a tuple of
361 renders template for email, and returns a tuple of
353 (subject, email_headers, email_html_body, email_plaintext_body)
362 (subject, email_headers, email_html_body, email_plaintext_body)
354 """
363 """
355 # translator and helpers inject
364 # translator and helpers inject
356 _kwargs = self._update_kwargs_for_render(kwargs)
365 _kwargs = self._update_kwargs_for_render(kwargs)
357 request = get_current_request()
366 request = get_current_request()
358 email_template = self.get_renderer(type_, request=request)
367 email_template = self.get_renderer(type_, request=request)
359
368
360 subject = email_template.render('subject', **_kwargs)
369 subject = email_template.render('subject', **_kwargs)
361
370
362 try:
371 try:
363 headers = email_template.render('headers', **_kwargs)
372 headers = email_template.render('headers', **_kwargs)
364 except AttributeError:
373 except AttributeError:
365 # it's not defined in template, ok we can skip it
374 # it's not defined in template, ok we can skip it
366 headers = ''
375 headers = ''
367
376
368 try:
377 try:
369 body_plaintext = email_template.render('body_plaintext', **_kwargs)
378 body_plaintext = email_template.render('body_plaintext', **_kwargs)
370 except AttributeError:
379 except AttributeError:
371 # it's not defined in template, ok we can skip it
380 # it's not defined in template, ok we can skip it
372 body_plaintext = ''
381 body_plaintext = ''
373
382
374 # render WHOLE template
383 # render WHOLE template
375 body = email_template.render(None, **_kwargs)
384 body = email_template.render(None, **_kwargs)
376
385
377 return subject, headers, body, body_plaintext
386 return subject, headers, body, body_plaintext
@@ -1,1032 +1,1032 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 os
21 import os
22 import re
22 import re
23 import shutil
23 import shutil
24 import time
24 import time
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import datetime
27 import datetime
28
28
29 from pyramid.threadlocal import get_current_request
29 from pyramid.threadlocal import get_current_request
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.lib.auth import HasUserGroupPermissionAny
33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 from rhodecode.lib.caching_query import FromCache
34 from rhodecode.lib.caching_query import FromCache
35 from rhodecode.lib.exceptions import AttachedForksError
35 from rhodecode.lib.exceptions import AttachedForksError
36 from rhodecode.lib.hooks_base import log_delete_repository
36 from rhodecode.lib.hooks_base import log_delete_repository
37 from rhodecode.lib.user_log_filter import user_log_filter
37 from rhodecode.lib.user_log_filter import user_log_filter
38 from rhodecode.lib.utils import make_db_config
38 from rhodecode.lib.utils import make_db_config
39 from rhodecode.lib.utils2 import (
39 from rhodecode.lib.utils2 import (
40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
41 get_current_rhodecode_user, safe_int, datetime_to_time,
41 get_current_rhodecode_user, safe_int, datetime_to_time,
42 action_logger_generic)
42 action_logger_generic)
43 from rhodecode.lib.vcs.backends import get_backend
43 from rhodecode.lib.vcs.backends import get_backend
44 from rhodecode.model import BaseModel
44 from rhodecode.model import BaseModel
45 from rhodecode.model.db import (
45 from rhodecode.model.db import (
46 _hash_key, joinedload, or_, Repository, UserRepoToPerm, UserGroupRepoToPerm,
46 _hash_key, joinedload, or_, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49
49
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class RepoModel(BaseModel):
56 class RepoModel(BaseModel):
57
57
58 cls = Repository
58 cls = Repository
59
59
60 def _get_user_group(self, users_group):
60 def _get_user_group(self, users_group):
61 return self._get_instance(UserGroup, users_group,
61 return self._get_instance(UserGroup, users_group,
62 callback=UserGroup.get_by_group_name)
62 callback=UserGroup.get_by_group_name)
63
63
64 def _get_repo_group(self, repo_group):
64 def _get_repo_group(self, repo_group):
65 return self._get_instance(RepoGroup, repo_group,
65 return self._get_instance(RepoGroup, repo_group,
66 callback=RepoGroup.get_by_group_name)
66 callback=RepoGroup.get_by_group_name)
67
67
68 def _create_default_perms(self, repository, private):
68 def _create_default_perms(self, repository, private):
69 # create default permission
69 # create default permission
70 default = 'repository.read'
70 default = 'repository.read'
71 def_user = User.get_default_user()
71 def_user = User.get_default_user()
72 for p in def_user.user_perms:
72 for p in def_user.user_perms:
73 if p.permission.permission_name.startswith('repository.'):
73 if p.permission.permission_name.startswith('repository.'):
74 default = p.permission.permission_name
74 default = p.permission.permission_name
75 break
75 break
76
76
77 default_perm = 'repository.none' if private else default
77 default_perm = 'repository.none' if private else default
78
78
79 repo_to_perm = UserRepoToPerm()
79 repo_to_perm = UserRepoToPerm()
80 repo_to_perm.permission = Permission.get_by_key(default_perm)
80 repo_to_perm.permission = Permission.get_by_key(default_perm)
81
81
82 repo_to_perm.repository = repository
82 repo_to_perm.repository = repository
83 repo_to_perm.user_id = def_user.user_id
83 repo_to_perm.user_id = def_user.user_id
84
84
85 return repo_to_perm
85 return repo_to_perm
86
86
87 @LazyProperty
87 @LazyProperty
88 def repos_path(self):
88 def repos_path(self):
89 """
89 """
90 Gets the repositories root path from database
90 Gets the repositories root path from database
91 """
91 """
92 settings_model = VcsSettingsModel(sa=self.sa)
92 settings_model = VcsSettingsModel(sa=self.sa)
93 return settings_model.get_repos_location()
93 return settings_model.get_repos_location()
94
94
95 def get(self, repo_id, cache=False):
95 def get(self, repo_id, cache=False):
96 repo = self.sa.query(Repository) \
96 repo = self.sa.query(Repository) \
97 .filter(Repository.repo_id == repo_id)
97 .filter(Repository.repo_id == repo_id)
98
98
99 if cache:
99 if cache:
100 repo = repo.options(
100 repo = repo.options(
101 FromCache("sql_cache_short", "get_repo_%s" % repo_id))
101 FromCache("sql_cache_short", "get_repo_%s" % repo_id))
102 return repo.scalar()
102 return repo.scalar()
103
103
104 def get_repo(self, repository):
104 def get_repo(self, repository):
105 return self._get_repo(repository)
105 return self._get_repo(repository)
106
106
107 def get_by_repo_name(self, repo_name, cache=False):
107 def get_by_repo_name(self, repo_name, cache=False):
108 repo = self.sa.query(Repository) \
108 repo = self.sa.query(Repository) \
109 .filter(Repository.repo_name == repo_name)
109 .filter(Repository.repo_name == repo_name)
110
110
111 if cache:
111 if cache:
112 name_key = _hash_key(repo_name)
112 name_key = _hash_key(repo_name)
113 repo = repo.options(
113 repo = repo.options(
114 FromCache("sql_cache_short", "get_repo_%s" % name_key))
114 FromCache("sql_cache_short", "get_repo_%s" % name_key))
115 return repo.scalar()
115 return repo.scalar()
116
116
117 def _extract_id_from_repo_name(self, repo_name):
117 def _extract_id_from_repo_name(self, repo_name):
118 if repo_name.startswith('/'):
118 if repo_name.startswith('/'):
119 repo_name = repo_name.lstrip('/')
119 repo_name = repo_name.lstrip('/')
120 by_id_match = re.match(r'^_(\d{1,})', repo_name)
120 by_id_match = re.match(r'^_(\d{1,})', repo_name)
121 if by_id_match:
121 if by_id_match:
122 return by_id_match.groups()[0]
122 return by_id_match.groups()[0]
123
123
124 def get_repo_by_id(self, repo_name):
124 def get_repo_by_id(self, repo_name):
125 """
125 """
126 Extracts repo_name by id from special urls.
126 Extracts repo_name by id from special urls.
127 Example url is _11/repo_name
127 Example url is _11/repo_name
128
128
129 :param repo_name:
129 :param repo_name:
130 :return: repo object if matched else None
130 :return: repo object if matched else None
131 """
131 """
132
132
133 try:
133 try:
134 _repo_id = self._extract_id_from_repo_name(repo_name)
134 _repo_id = self._extract_id_from_repo_name(repo_name)
135 if _repo_id:
135 if _repo_id:
136 return self.get(_repo_id)
136 return self.get(_repo_id)
137 except Exception:
137 except Exception:
138 log.exception('Failed to extract repo_name from URL')
138 log.exception('Failed to extract repo_name from URL')
139
139
140 return None
140 return None
141
141
142 def get_repos_for_root(self, root, traverse=False):
142 def get_repos_for_root(self, root, traverse=False):
143 if traverse:
143 if traverse:
144 like_expression = u'{}%'.format(safe_unicode(root))
144 like_expression = u'{}%'.format(safe_unicode(root))
145 repos = Repository.query().filter(
145 repos = Repository.query().filter(
146 Repository.repo_name.like(like_expression)).all()
146 Repository.repo_name.like(like_expression)).all()
147 else:
147 else:
148 if root and not isinstance(root, RepoGroup):
148 if root and not isinstance(root, RepoGroup):
149 raise ValueError(
149 raise ValueError(
150 'Root must be an instance '
150 'Root must be an instance '
151 'of RepoGroup, got:{} instead'.format(type(root)))
151 'of RepoGroup, got:{} instead'.format(type(root)))
152 repos = Repository.query().filter(Repository.group == root).all()
152 repos = Repository.query().filter(Repository.group == root).all()
153 return repos
153 return repos
154
154
155 def get_url(self, repo, request=None, permalink=False):
155 def get_url(self, repo, request=None, permalink=False):
156 if not request:
156 if not request:
157 request = get_current_request()
157 request = get_current_request()
158
158
159 if not request:
159 if not request:
160 return
160 return
161
161
162 if permalink:
162 if permalink:
163 return request.route_url(
163 return request.route_url(
164 'repo_summary', repo_name=safe_str(repo.repo_id))
164 'repo_summary', repo_name=safe_str(repo.repo_id))
165 else:
165 else:
166 return request.route_url(
166 return request.route_url(
167 'repo_summary', repo_name=safe_str(repo.repo_name))
167 'repo_summary', repo_name=safe_str(repo.repo_name))
168
168
169 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
169 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
170 if not request:
170 if not request:
171 request = get_current_request()
171 request = get_current_request()
172
172
173 if not request:
173 if not request:
174 return
174 return
175
175
176 if permalink:
176 if permalink:
177 return request.route_url(
177 return request.route_url(
178 'repo_commit', repo_name=safe_str(repo.repo_id),
178 'repo_commit', repo_name=safe_str(repo.repo_id),
179 commit_id=commit_id)
179 commit_id=commit_id)
180
180
181 else:
181 else:
182 return request.route_url(
182 return request.route_url(
183 'repo_commit', repo_name=safe_str(repo.repo_name),
183 'repo_commit', repo_name=safe_str(repo.repo_name),
184 commit_id=commit_id)
184 commit_id=commit_id)
185
185
186 def get_repo_log(self, repo, filter_term):
186 def get_repo_log(self, repo, filter_term):
187 repo_log = UserLog.query()\
187 repo_log = UserLog.query()\
188 .filter(or_(UserLog.repository_id == repo.repo_id,
188 .filter(or_(UserLog.repository_id == repo.repo_id,
189 UserLog.repository_name == repo.repo_name))\
189 UserLog.repository_name == repo.repo_name))\
190 .options(joinedload(UserLog.user))\
190 .options(joinedload(UserLog.user))\
191 .options(joinedload(UserLog.repository))\
191 .options(joinedload(UserLog.repository))\
192 .order_by(UserLog.action_date.desc())
192 .order_by(UserLog.action_date.desc())
193
193
194 repo_log = user_log_filter(repo_log, filter_term)
194 repo_log = user_log_filter(repo_log, filter_term)
195 return repo_log
195 return repo_log
196
196
197 @classmethod
197 @classmethod
198 def update_repoinfo(cls, repositories=None):
198 def update_repoinfo(cls, repositories=None):
199 if not repositories:
199 if not repositories:
200 repositories = Repository.getAll()
200 repositories = Repository.getAll()
201 for repo in repositories:
201 for repo in repositories:
202 repo.update_commit_cache()
202 repo.update_commit_cache()
203
203
204 def get_repos_as_dict(self, repo_list=None, admin=False,
204 def get_repos_as_dict(self, repo_list=None, admin=False,
205 super_user_actions=False):
205 super_user_actions=False):
206 _render = get_current_request().get_partial_renderer(
206 _render = get_current_request().get_partial_renderer(
207 'data_table/_dt_elements.mako')
207 'rhodecode:templates/data_table/_dt_elements.mako')
208 c = _render.get_call_context()
208 c = _render.get_call_context()
209
209
210 def quick_menu(repo_name):
210 def quick_menu(repo_name):
211 return _render('quick_menu', repo_name)
211 return _render('quick_menu', repo_name)
212
212
213 def repo_lnk(name, rtype, rstate, private, fork_of):
213 def repo_lnk(name, rtype, rstate, private, fork_of):
214 return _render('repo_name', name, rtype, rstate, private, fork_of,
214 return _render('repo_name', name, rtype, rstate, private, fork_of,
215 short_name=not admin, admin=False)
215 short_name=not admin, admin=False)
216
216
217 def last_change(last_change):
217 def last_change(last_change):
218 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
218 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
219 last_change = last_change + datetime.timedelta(seconds=
219 last_change = last_change + datetime.timedelta(seconds=
220 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
220 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
221 return _render("last_change", last_change)
221 return _render("last_change", last_change)
222
222
223 def rss_lnk(repo_name):
223 def rss_lnk(repo_name):
224 return _render("rss", repo_name)
224 return _render("rss", repo_name)
225
225
226 def atom_lnk(repo_name):
226 def atom_lnk(repo_name):
227 return _render("atom", repo_name)
227 return _render("atom", repo_name)
228
228
229 def last_rev(repo_name, cs_cache):
229 def last_rev(repo_name, cs_cache):
230 return _render('revision', repo_name, cs_cache.get('revision'),
230 return _render('revision', repo_name, cs_cache.get('revision'),
231 cs_cache.get('raw_id'), cs_cache.get('author'),
231 cs_cache.get('raw_id'), cs_cache.get('author'),
232 cs_cache.get('message'))
232 cs_cache.get('message'))
233
233
234 def desc(desc):
234 def desc(desc):
235 return _render('repo_desc', desc, c.visual.stylify_metatags)
235 return _render('repo_desc', desc, c.visual.stylify_metatags)
236
236
237 def state(repo_state):
237 def state(repo_state):
238 return _render("repo_state", repo_state)
238 return _render("repo_state", repo_state)
239
239
240 def repo_actions(repo_name):
240 def repo_actions(repo_name):
241 return _render('repo_actions', repo_name, super_user_actions)
241 return _render('repo_actions', repo_name, super_user_actions)
242
242
243 def user_profile(username):
243 def user_profile(username):
244 return _render('user_profile', username)
244 return _render('user_profile', username)
245
245
246 repos_data = []
246 repos_data = []
247 for repo in repo_list:
247 for repo in repo_list:
248 cs_cache = repo.changeset_cache
248 cs_cache = repo.changeset_cache
249 row = {
249 row = {
250 "menu": quick_menu(repo.repo_name),
250 "menu": quick_menu(repo.repo_name),
251
251
252 "name": repo_lnk(repo.repo_name, repo.repo_type,
252 "name": repo_lnk(repo.repo_name, repo.repo_type,
253 repo.repo_state, repo.private, repo.fork),
253 repo.repo_state, repo.private, repo.fork),
254 "name_raw": repo.repo_name.lower(),
254 "name_raw": repo.repo_name.lower(),
255
255
256 "last_change": last_change(repo.last_db_change),
256 "last_change": last_change(repo.last_db_change),
257 "last_change_raw": datetime_to_time(repo.last_db_change),
257 "last_change_raw": datetime_to_time(repo.last_db_change),
258
258
259 "last_changeset": last_rev(repo.repo_name, cs_cache),
259 "last_changeset": last_rev(repo.repo_name, cs_cache),
260 "last_changeset_raw": cs_cache.get('revision'),
260 "last_changeset_raw": cs_cache.get('revision'),
261
261
262 "desc": desc(repo.description_safe),
262 "desc": desc(repo.description_safe),
263 "owner": user_profile(repo.user.username),
263 "owner": user_profile(repo.user.username),
264
264
265 "state": state(repo.repo_state),
265 "state": state(repo.repo_state),
266 "rss": rss_lnk(repo.repo_name),
266 "rss": rss_lnk(repo.repo_name),
267
267
268 "atom": atom_lnk(repo.repo_name),
268 "atom": atom_lnk(repo.repo_name),
269 }
269 }
270 if admin:
270 if admin:
271 row.update({
271 row.update({
272 "action": repo_actions(repo.repo_name),
272 "action": repo_actions(repo.repo_name),
273 })
273 })
274 repos_data.append(row)
274 repos_data.append(row)
275
275
276 return repos_data
276 return repos_data
277
277
278 def _get_defaults(self, repo_name):
278 def _get_defaults(self, repo_name):
279 """
279 """
280 Gets information about repository, and returns a dict for
280 Gets information about repository, and returns a dict for
281 usage in forms
281 usage in forms
282
282
283 :param repo_name:
283 :param repo_name:
284 """
284 """
285
285
286 repo_info = Repository.get_by_repo_name(repo_name)
286 repo_info = Repository.get_by_repo_name(repo_name)
287
287
288 if repo_info is None:
288 if repo_info is None:
289 return None
289 return None
290
290
291 defaults = repo_info.get_dict()
291 defaults = repo_info.get_dict()
292 defaults['repo_name'] = repo_info.just_name
292 defaults['repo_name'] = repo_info.just_name
293
293
294 groups = repo_info.groups_with_parents
294 groups = repo_info.groups_with_parents
295 parent_group = groups[-1] if groups else None
295 parent_group = groups[-1] if groups else None
296
296
297 # we use -1 as this is how in HTML, we mark an empty group
297 # we use -1 as this is how in HTML, we mark an empty group
298 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
298 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
299
299
300 keys_to_process = (
300 keys_to_process = (
301 {'k': 'repo_type', 'strip': False},
301 {'k': 'repo_type', 'strip': False},
302 {'k': 'repo_enable_downloads', 'strip': True},
302 {'k': 'repo_enable_downloads', 'strip': True},
303 {'k': 'repo_description', 'strip': True},
303 {'k': 'repo_description', 'strip': True},
304 {'k': 'repo_enable_locking', 'strip': True},
304 {'k': 'repo_enable_locking', 'strip': True},
305 {'k': 'repo_landing_rev', 'strip': True},
305 {'k': 'repo_landing_rev', 'strip': True},
306 {'k': 'clone_uri', 'strip': False},
306 {'k': 'clone_uri', 'strip': False},
307 {'k': 'repo_private', 'strip': True},
307 {'k': 'repo_private', 'strip': True},
308 {'k': 'repo_enable_statistics', 'strip': True}
308 {'k': 'repo_enable_statistics', 'strip': True}
309 )
309 )
310
310
311 for item in keys_to_process:
311 for item in keys_to_process:
312 attr = item['k']
312 attr = item['k']
313 if item['strip']:
313 if item['strip']:
314 attr = remove_prefix(item['k'], 'repo_')
314 attr = remove_prefix(item['k'], 'repo_')
315
315
316 val = defaults[attr]
316 val = defaults[attr]
317 if item['k'] == 'repo_landing_rev':
317 if item['k'] == 'repo_landing_rev':
318 val = ':'.join(defaults[attr])
318 val = ':'.join(defaults[attr])
319 defaults[item['k']] = val
319 defaults[item['k']] = val
320 if item['k'] == 'clone_uri':
320 if item['k'] == 'clone_uri':
321 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
321 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
322
322
323 # fill owner
323 # fill owner
324 if repo_info.user:
324 if repo_info.user:
325 defaults.update({'user': repo_info.user.username})
325 defaults.update({'user': repo_info.user.username})
326 else:
326 else:
327 replacement_user = User.get_first_super_admin().username
327 replacement_user = User.get_first_super_admin().username
328 defaults.update({'user': replacement_user})
328 defaults.update({'user': replacement_user})
329
329
330 return defaults
330 return defaults
331
331
332 def update(self, repo, **kwargs):
332 def update(self, repo, **kwargs):
333 try:
333 try:
334 cur_repo = self._get_repo(repo)
334 cur_repo = self._get_repo(repo)
335 source_repo_name = cur_repo.repo_name
335 source_repo_name = cur_repo.repo_name
336 if 'user' in kwargs:
336 if 'user' in kwargs:
337 cur_repo.user = User.get_by_username(kwargs['user'])
337 cur_repo.user = User.get_by_username(kwargs['user'])
338
338
339 if 'repo_group' in kwargs:
339 if 'repo_group' in kwargs:
340 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
340 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
341 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
341 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
342
342
343 update_keys = [
343 update_keys = [
344 (1, 'repo_description'),
344 (1, 'repo_description'),
345 (1, 'repo_landing_rev'),
345 (1, 'repo_landing_rev'),
346 (1, 'repo_private'),
346 (1, 'repo_private'),
347 (1, 'repo_enable_downloads'),
347 (1, 'repo_enable_downloads'),
348 (1, 'repo_enable_locking'),
348 (1, 'repo_enable_locking'),
349 (1, 'repo_enable_statistics'),
349 (1, 'repo_enable_statistics'),
350 (0, 'clone_uri'),
350 (0, 'clone_uri'),
351 (0, 'fork_id')
351 (0, 'fork_id')
352 ]
352 ]
353 for strip, k in update_keys:
353 for strip, k in update_keys:
354 if k in kwargs:
354 if k in kwargs:
355 val = kwargs[k]
355 val = kwargs[k]
356 if strip:
356 if strip:
357 k = remove_prefix(k, 'repo_')
357 k = remove_prefix(k, 'repo_')
358
358
359 setattr(cur_repo, k, val)
359 setattr(cur_repo, k, val)
360
360
361 new_name = cur_repo.get_new_name(kwargs['repo_name'])
361 new_name = cur_repo.get_new_name(kwargs['repo_name'])
362 cur_repo.repo_name = new_name
362 cur_repo.repo_name = new_name
363
363
364 # if private flag is set, reset default permission to NONE
364 # if private flag is set, reset default permission to NONE
365 if kwargs.get('repo_private'):
365 if kwargs.get('repo_private'):
366 EMPTY_PERM = 'repository.none'
366 EMPTY_PERM = 'repository.none'
367 RepoModel().grant_user_permission(
367 RepoModel().grant_user_permission(
368 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
368 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
369 )
369 )
370
370
371 # handle extra fields
371 # handle extra fields
372 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
372 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
373 kwargs):
373 kwargs):
374 k = RepositoryField.un_prefix_key(field)
374 k = RepositoryField.un_prefix_key(field)
375 ex_field = RepositoryField.get_by_key_name(
375 ex_field = RepositoryField.get_by_key_name(
376 key=k, repo=cur_repo)
376 key=k, repo=cur_repo)
377 if ex_field:
377 if ex_field:
378 ex_field.field_value = kwargs[field]
378 ex_field.field_value = kwargs[field]
379 self.sa.add(ex_field)
379 self.sa.add(ex_field)
380 cur_repo.updated_on = datetime.datetime.now()
380 cur_repo.updated_on = datetime.datetime.now()
381 self.sa.add(cur_repo)
381 self.sa.add(cur_repo)
382
382
383 if source_repo_name != new_name:
383 if source_repo_name != new_name:
384 # rename repository
384 # rename repository
385 self._rename_filesystem_repo(
385 self._rename_filesystem_repo(
386 old=source_repo_name, new=new_name)
386 old=source_repo_name, new=new_name)
387
387
388 return cur_repo
388 return cur_repo
389 except Exception:
389 except Exception:
390 log.error(traceback.format_exc())
390 log.error(traceback.format_exc())
391 raise
391 raise
392
392
393 def _create_repo(self, repo_name, repo_type, description, owner,
393 def _create_repo(self, repo_name, repo_type, description, owner,
394 private=False, clone_uri=None, repo_group=None,
394 private=False, clone_uri=None, repo_group=None,
395 landing_rev='rev:tip', fork_of=None,
395 landing_rev='rev:tip', fork_of=None,
396 copy_fork_permissions=False, enable_statistics=False,
396 copy_fork_permissions=False, enable_statistics=False,
397 enable_locking=False, enable_downloads=False,
397 enable_locking=False, enable_downloads=False,
398 copy_group_permissions=False,
398 copy_group_permissions=False,
399 state=Repository.STATE_PENDING):
399 state=Repository.STATE_PENDING):
400 """
400 """
401 Create repository inside database with PENDING state, this should be
401 Create repository inside database with PENDING state, this should be
402 only executed by create() repo. With exception of importing existing
402 only executed by create() repo. With exception of importing existing
403 repos
403 repos
404 """
404 """
405 from rhodecode.model.scm import ScmModel
405 from rhodecode.model.scm import ScmModel
406
406
407 owner = self._get_user(owner)
407 owner = self._get_user(owner)
408 fork_of = self._get_repo(fork_of)
408 fork_of = self._get_repo(fork_of)
409 repo_group = self._get_repo_group(safe_int(repo_group))
409 repo_group = self._get_repo_group(safe_int(repo_group))
410
410
411 try:
411 try:
412 repo_name = safe_unicode(repo_name)
412 repo_name = safe_unicode(repo_name)
413 description = safe_unicode(description)
413 description = safe_unicode(description)
414 # repo name is just a name of repository
414 # repo name is just a name of repository
415 # while repo_name_full is a full qualified name that is combined
415 # while repo_name_full is a full qualified name that is combined
416 # with name and path of group
416 # with name and path of group
417 repo_name_full = repo_name
417 repo_name_full = repo_name
418 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
418 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
419
419
420 new_repo = Repository()
420 new_repo = Repository()
421 new_repo.repo_state = state
421 new_repo.repo_state = state
422 new_repo.enable_statistics = False
422 new_repo.enable_statistics = False
423 new_repo.repo_name = repo_name_full
423 new_repo.repo_name = repo_name_full
424 new_repo.repo_type = repo_type
424 new_repo.repo_type = repo_type
425 new_repo.user = owner
425 new_repo.user = owner
426 new_repo.group = repo_group
426 new_repo.group = repo_group
427 new_repo.description = description or repo_name
427 new_repo.description = description or repo_name
428 new_repo.private = private
428 new_repo.private = private
429 new_repo.clone_uri = clone_uri
429 new_repo.clone_uri = clone_uri
430 new_repo.landing_rev = landing_rev
430 new_repo.landing_rev = landing_rev
431
431
432 new_repo.enable_statistics = enable_statistics
432 new_repo.enable_statistics = enable_statistics
433 new_repo.enable_locking = enable_locking
433 new_repo.enable_locking = enable_locking
434 new_repo.enable_downloads = enable_downloads
434 new_repo.enable_downloads = enable_downloads
435
435
436 if repo_group:
436 if repo_group:
437 new_repo.enable_locking = repo_group.enable_locking
437 new_repo.enable_locking = repo_group.enable_locking
438
438
439 if fork_of:
439 if fork_of:
440 parent_repo = fork_of
440 parent_repo = fork_of
441 new_repo.fork = parent_repo
441 new_repo.fork = parent_repo
442
442
443 events.trigger(events.RepoPreCreateEvent(new_repo))
443 events.trigger(events.RepoPreCreateEvent(new_repo))
444
444
445 self.sa.add(new_repo)
445 self.sa.add(new_repo)
446
446
447 EMPTY_PERM = 'repository.none'
447 EMPTY_PERM = 'repository.none'
448 if fork_of and copy_fork_permissions:
448 if fork_of and copy_fork_permissions:
449 repo = fork_of
449 repo = fork_of
450 user_perms = UserRepoToPerm.query() \
450 user_perms = UserRepoToPerm.query() \
451 .filter(UserRepoToPerm.repository == repo).all()
451 .filter(UserRepoToPerm.repository == repo).all()
452 group_perms = UserGroupRepoToPerm.query() \
452 group_perms = UserGroupRepoToPerm.query() \
453 .filter(UserGroupRepoToPerm.repository == repo).all()
453 .filter(UserGroupRepoToPerm.repository == repo).all()
454
454
455 for perm in user_perms:
455 for perm in user_perms:
456 UserRepoToPerm.create(
456 UserRepoToPerm.create(
457 perm.user, new_repo, perm.permission)
457 perm.user, new_repo, perm.permission)
458
458
459 for perm in group_perms:
459 for perm in group_perms:
460 UserGroupRepoToPerm.create(
460 UserGroupRepoToPerm.create(
461 perm.users_group, new_repo, perm.permission)
461 perm.users_group, new_repo, perm.permission)
462 # in case we copy permissions and also set this repo to private
462 # in case we copy permissions and also set this repo to private
463 # override the default user permission to make it a private
463 # override the default user permission to make it a private
464 # repo
464 # repo
465 if private:
465 if private:
466 RepoModel(self.sa).grant_user_permission(
466 RepoModel(self.sa).grant_user_permission(
467 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
467 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
468
468
469 elif repo_group and copy_group_permissions:
469 elif repo_group and copy_group_permissions:
470 user_perms = UserRepoGroupToPerm.query() \
470 user_perms = UserRepoGroupToPerm.query() \
471 .filter(UserRepoGroupToPerm.group == repo_group).all()
471 .filter(UserRepoGroupToPerm.group == repo_group).all()
472
472
473 group_perms = UserGroupRepoGroupToPerm.query() \
473 group_perms = UserGroupRepoGroupToPerm.query() \
474 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
474 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
475
475
476 for perm in user_perms:
476 for perm in user_perms:
477 perm_name = perm.permission.permission_name.replace(
477 perm_name = perm.permission.permission_name.replace(
478 'group.', 'repository.')
478 'group.', 'repository.')
479 perm_obj = Permission.get_by_key(perm_name)
479 perm_obj = Permission.get_by_key(perm_name)
480 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
480 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
481
481
482 for perm in group_perms:
482 for perm in group_perms:
483 perm_name = perm.permission.permission_name.replace(
483 perm_name = perm.permission.permission_name.replace(
484 'group.', 'repository.')
484 'group.', 'repository.')
485 perm_obj = Permission.get_by_key(perm_name)
485 perm_obj = Permission.get_by_key(perm_name)
486 UserGroupRepoToPerm.create(
486 UserGroupRepoToPerm.create(
487 perm.users_group, new_repo, perm_obj)
487 perm.users_group, new_repo, perm_obj)
488
488
489 if private:
489 if private:
490 RepoModel(self.sa).grant_user_permission(
490 RepoModel(self.sa).grant_user_permission(
491 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
491 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
492
492
493 else:
493 else:
494 perm_obj = self._create_default_perms(new_repo, private)
494 perm_obj = self._create_default_perms(new_repo, private)
495 self.sa.add(perm_obj)
495 self.sa.add(perm_obj)
496
496
497 # now automatically start following this repository as owner
497 # now automatically start following this repository as owner
498 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
498 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
499 owner.user_id)
499 owner.user_id)
500
500
501 # we need to flush here, in order to check if database won't
501 # we need to flush here, in order to check if database won't
502 # throw any exceptions, create filesystem dirs at the very end
502 # throw any exceptions, create filesystem dirs at the very end
503 self.sa.flush()
503 self.sa.flush()
504 events.trigger(events.RepoCreateEvent(new_repo))
504 events.trigger(events.RepoCreateEvent(new_repo))
505 return new_repo
505 return new_repo
506
506
507 except Exception:
507 except Exception:
508 log.error(traceback.format_exc())
508 log.error(traceback.format_exc())
509 raise
509 raise
510
510
511 def create(self, form_data, cur_user):
511 def create(self, form_data, cur_user):
512 """
512 """
513 Create repository using celery tasks
513 Create repository using celery tasks
514
514
515 :param form_data:
515 :param form_data:
516 :param cur_user:
516 :param cur_user:
517 """
517 """
518 from rhodecode.lib.celerylib import tasks, run_task
518 from rhodecode.lib.celerylib import tasks, run_task
519 return run_task(tasks.create_repo, form_data, cur_user)
519 return run_task(tasks.create_repo, form_data, cur_user)
520
520
521 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
521 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
522 perm_deletions=None, check_perms=True,
522 perm_deletions=None, check_perms=True,
523 cur_user=None):
523 cur_user=None):
524 if not perm_additions:
524 if not perm_additions:
525 perm_additions = []
525 perm_additions = []
526 if not perm_updates:
526 if not perm_updates:
527 perm_updates = []
527 perm_updates = []
528 if not perm_deletions:
528 if not perm_deletions:
529 perm_deletions = []
529 perm_deletions = []
530
530
531 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
531 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
532
532
533 changes = {
533 changes = {
534 'added': [],
534 'added': [],
535 'updated': [],
535 'updated': [],
536 'deleted': []
536 'deleted': []
537 }
537 }
538 # update permissions
538 # update permissions
539 for member_id, perm, member_type in perm_updates:
539 for member_id, perm, member_type in perm_updates:
540 member_id = int(member_id)
540 member_id = int(member_id)
541 if member_type == 'user':
541 if member_type == 'user':
542 member_name = User.get(member_id).username
542 member_name = User.get(member_id).username
543 # this updates also current one if found
543 # this updates also current one if found
544 self.grant_user_permission(
544 self.grant_user_permission(
545 repo=repo, user=member_id, perm=perm)
545 repo=repo, user=member_id, perm=perm)
546 else: # set for user group
546 else: # set for user group
547 # check if we have permissions to alter this usergroup
547 # check if we have permissions to alter this usergroup
548 member_name = UserGroup.get(member_id).users_group_name
548 member_name = UserGroup.get(member_id).users_group_name
549 if not check_perms or HasUserGroupPermissionAny(
549 if not check_perms or HasUserGroupPermissionAny(
550 *req_perms)(member_name, user=cur_user):
550 *req_perms)(member_name, user=cur_user):
551 self.grant_user_group_permission(
551 self.grant_user_group_permission(
552 repo=repo, group_name=member_id, perm=perm)
552 repo=repo, group_name=member_id, perm=perm)
553
553
554 changes['updated'].append({'type': member_type, 'id': member_id,
554 changes['updated'].append({'type': member_type, 'id': member_id,
555 'name': member_name, 'new_perm': perm})
555 'name': member_name, 'new_perm': perm})
556
556
557 # set new permissions
557 # set new permissions
558 for member_id, perm, member_type in perm_additions:
558 for member_id, perm, member_type in perm_additions:
559 member_id = int(member_id)
559 member_id = int(member_id)
560 if member_type == 'user':
560 if member_type == 'user':
561 member_name = User.get(member_id).username
561 member_name = User.get(member_id).username
562 self.grant_user_permission(
562 self.grant_user_permission(
563 repo=repo, user=member_id, perm=perm)
563 repo=repo, user=member_id, perm=perm)
564 else: # set for user group
564 else: # set for user group
565 # check if we have permissions to alter this usergroup
565 # check if we have permissions to alter this usergroup
566 member_name = UserGroup.get(member_id).users_group_name
566 member_name = UserGroup.get(member_id).users_group_name
567 if not check_perms or HasUserGroupPermissionAny(
567 if not check_perms or HasUserGroupPermissionAny(
568 *req_perms)(member_name, user=cur_user):
568 *req_perms)(member_name, user=cur_user):
569 self.grant_user_group_permission(
569 self.grant_user_group_permission(
570 repo=repo, group_name=member_id, perm=perm)
570 repo=repo, group_name=member_id, perm=perm)
571 changes['added'].append({'type': member_type, 'id': member_id,
571 changes['added'].append({'type': member_type, 'id': member_id,
572 'name': member_name, 'new_perm': perm})
572 'name': member_name, 'new_perm': perm})
573 # delete permissions
573 # delete permissions
574 for member_id, perm, member_type in perm_deletions:
574 for member_id, perm, member_type in perm_deletions:
575 member_id = int(member_id)
575 member_id = int(member_id)
576 if member_type == 'user':
576 if member_type == 'user':
577 member_name = User.get(member_id).username
577 member_name = User.get(member_id).username
578 self.revoke_user_permission(repo=repo, user=member_id)
578 self.revoke_user_permission(repo=repo, user=member_id)
579 else: # set for user group
579 else: # set for user group
580 # check if we have permissions to alter this usergroup
580 # check if we have permissions to alter this usergroup
581 member_name = UserGroup.get(member_id).users_group_name
581 member_name = UserGroup.get(member_id).users_group_name
582 if not check_perms or HasUserGroupPermissionAny(
582 if not check_perms or HasUserGroupPermissionAny(
583 *req_perms)(member_name, user=cur_user):
583 *req_perms)(member_name, user=cur_user):
584 self.revoke_user_group_permission(
584 self.revoke_user_group_permission(
585 repo=repo, group_name=member_id)
585 repo=repo, group_name=member_id)
586
586
587 changes['deleted'].append({'type': member_type, 'id': member_id,
587 changes['deleted'].append({'type': member_type, 'id': member_id,
588 'name': member_name, 'new_perm': perm})
588 'name': member_name, 'new_perm': perm})
589 return changes
589 return changes
590
590
591 def create_fork(self, form_data, cur_user):
591 def create_fork(self, form_data, cur_user):
592 """
592 """
593 Simple wrapper into executing celery task for fork creation
593 Simple wrapper into executing celery task for fork creation
594
594
595 :param form_data:
595 :param form_data:
596 :param cur_user:
596 :param cur_user:
597 """
597 """
598 from rhodecode.lib.celerylib import tasks, run_task
598 from rhodecode.lib.celerylib import tasks, run_task
599 return run_task(tasks.create_repo_fork, form_data, cur_user)
599 return run_task(tasks.create_repo_fork, form_data, cur_user)
600
600
601 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
601 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
602 """
602 """
603 Delete given repository, forks parameter defines what do do with
603 Delete given repository, forks parameter defines what do do with
604 attached forks. Throws AttachedForksError if deleted repo has attached
604 attached forks. Throws AttachedForksError if deleted repo has attached
605 forks
605 forks
606
606
607 :param repo:
607 :param repo:
608 :param forks: str 'delete' or 'detach'
608 :param forks: str 'delete' or 'detach'
609 :param fs_remove: remove(archive) repo from filesystem
609 :param fs_remove: remove(archive) repo from filesystem
610 """
610 """
611 if not cur_user:
611 if not cur_user:
612 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
612 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
613 repo = self._get_repo(repo)
613 repo = self._get_repo(repo)
614 if repo:
614 if repo:
615 if forks == 'detach':
615 if forks == 'detach':
616 for r in repo.forks:
616 for r in repo.forks:
617 r.fork = None
617 r.fork = None
618 self.sa.add(r)
618 self.sa.add(r)
619 elif forks == 'delete':
619 elif forks == 'delete':
620 for r in repo.forks:
620 for r in repo.forks:
621 self.delete(r, forks='delete')
621 self.delete(r, forks='delete')
622 elif [f for f in repo.forks]:
622 elif [f for f in repo.forks]:
623 raise AttachedForksError()
623 raise AttachedForksError()
624
624
625 old_repo_dict = repo.get_dict()
625 old_repo_dict = repo.get_dict()
626 events.trigger(events.RepoPreDeleteEvent(repo))
626 events.trigger(events.RepoPreDeleteEvent(repo))
627 try:
627 try:
628 self.sa.delete(repo)
628 self.sa.delete(repo)
629 if fs_remove:
629 if fs_remove:
630 self._delete_filesystem_repo(repo)
630 self._delete_filesystem_repo(repo)
631 else:
631 else:
632 log.debug('skipping removal from filesystem')
632 log.debug('skipping removal from filesystem')
633 old_repo_dict.update({
633 old_repo_dict.update({
634 'deleted_by': cur_user,
634 'deleted_by': cur_user,
635 'deleted_on': time.time(),
635 'deleted_on': time.time(),
636 })
636 })
637 log_delete_repository(**old_repo_dict)
637 log_delete_repository(**old_repo_dict)
638 events.trigger(events.RepoDeleteEvent(repo))
638 events.trigger(events.RepoDeleteEvent(repo))
639 except Exception:
639 except Exception:
640 log.error(traceback.format_exc())
640 log.error(traceback.format_exc())
641 raise
641 raise
642
642
643 def grant_user_permission(self, repo, user, perm):
643 def grant_user_permission(self, repo, user, perm):
644 """
644 """
645 Grant permission for user on given repository, or update existing one
645 Grant permission for user on given repository, or update existing one
646 if found
646 if found
647
647
648 :param repo: Instance of Repository, repository_id, or repository name
648 :param repo: Instance of Repository, repository_id, or repository name
649 :param user: Instance of User, user_id or username
649 :param user: Instance of User, user_id or username
650 :param perm: Instance of Permission, or permission_name
650 :param perm: Instance of Permission, or permission_name
651 """
651 """
652 user = self._get_user(user)
652 user = self._get_user(user)
653 repo = self._get_repo(repo)
653 repo = self._get_repo(repo)
654 permission = self._get_perm(perm)
654 permission = self._get_perm(perm)
655
655
656 # check if we have that permission already
656 # check if we have that permission already
657 obj = self.sa.query(UserRepoToPerm) \
657 obj = self.sa.query(UserRepoToPerm) \
658 .filter(UserRepoToPerm.user == user) \
658 .filter(UserRepoToPerm.user == user) \
659 .filter(UserRepoToPerm.repository == repo) \
659 .filter(UserRepoToPerm.repository == repo) \
660 .scalar()
660 .scalar()
661 if obj is None:
661 if obj is None:
662 # create new !
662 # create new !
663 obj = UserRepoToPerm()
663 obj = UserRepoToPerm()
664 obj.repository = repo
664 obj.repository = repo
665 obj.user = user
665 obj.user = user
666 obj.permission = permission
666 obj.permission = permission
667 self.sa.add(obj)
667 self.sa.add(obj)
668 log.debug('Granted perm %s to %s on %s', perm, user, repo)
668 log.debug('Granted perm %s to %s on %s', perm, user, repo)
669 action_logger_generic(
669 action_logger_generic(
670 'granted permission: {} to user: {} on repo: {}'.format(
670 'granted permission: {} to user: {} on repo: {}'.format(
671 perm, user, repo), namespace='security.repo')
671 perm, user, repo), namespace='security.repo')
672 return obj
672 return obj
673
673
674 def revoke_user_permission(self, repo, user):
674 def revoke_user_permission(self, repo, user):
675 """
675 """
676 Revoke permission for user on given repository
676 Revoke permission for user on given repository
677
677
678 :param repo: Instance of Repository, repository_id, or repository name
678 :param repo: Instance of Repository, repository_id, or repository name
679 :param user: Instance of User, user_id or username
679 :param user: Instance of User, user_id or username
680 """
680 """
681
681
682 user = self._get_user(user)
682 user = self._get_user(user)
683 repo = self._get_repo(repo)
683 repo = self._get_repo(repo)
684
684
685 obj = self.sa.query(UserRepoToPerm) \
685 obj = self.sa.query(UserRepoToPerm) \
686 .filter(UserRepoToPerm.repository == repo) \
686 .filter(UserRepoToPerm.repository == repo) \
687 .filter(UserRepoToPerm.user == user) \
687 .filter(UserRepoToPerm.user == user) \
688 .scalar()
688 .scalar()
689 if obj:
689 if obj:
690 self.sa.delete(obj)
690 self.sa.delete(obj)
691 log.debug('Revoked perm on %s on %s', repo, user)
691 log.debug('Revoked perm on %s on %s', repo, user)
692 action_logger_generic(
692 action_logger_generic(
693 'revoked permission from user: {} on repo: {}'.format(
693 'revoked permission from user: {} on repo: {}'.format(
694 user, repo), namespace='security.repo')
694 user, repo), namespace='security.repo')
695
695
696 def grant_user_group_permission(self, repo, group_name, perm):
696 def grant_user_group_permission(self, repo, group_name, perm):
697 """
697 """
698 Grant permission for user group on given repository, or update
698 Grant permission for user group on given repository, or update
699 existing one if found
699 existing one if found
700
700
701 :param repo: Instance of Repository, repository_id, or repository name
701 :param repo: Instance of Repository, repository_id, or repository name
702 :param group_name: Instance of UserGroup, users_group_id,
702 :param group_name: Instance of UserGroup, users_group_id,
703 or user group name
703 or user group name
704 :param perm: Instance of Permission, or permission_name
704 :param perm: Instance of Permission, or permission_name
705 """
705 """
706 repo = self._get_repo(repo)
706 repo = self._get_repo(repo)
707 group_name = self._get_user_group(group_name)
707 group_name = self._get_user_group(group_name)
708 permission = self._get_perm(perm)
708 permission = self._get_perm(perm)
709
709
710 # check if we have that permission already
710 # check if we have that permission already
711 obj = self.sa.query(UserGroupRepoToPerm) \
711 obj = self.sa.query(UserGroupRepoToPerm) \
712 .filter(UserGroupRepoToPerm.users_group == group_name) \
712 .filter(UserGroupRepoToPerm.users_group == group_name) \
713 .filter(UserGroupRepoToPerm.repository == repo) \
713 .filter(UserGroupRepoToPerm.repository == repo) \
714 .scalar()
714 .scalar()
715
715
716 if obj is None:
716 if obj is None:
717 # create new
717 # create new
718 obj = UserGroupRepoToPerm()
718 obj = UserGroupRepoToPerm()
719
719
720 obj.repository = repo
720 obj.repository = repo
721 obj.users_group = group_name
721 obj.users_group = group_name
722 obj.permission = permission
722 obj.permission = permission
723 self.sa.add(obj)
723 self.sa.add(obj)
724 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
724 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
725 action_logger_generic(
725 action_logger_generic(
726 'granted permission: {} to usergroup: {} on repo: {}'.format(
726 'granted permission: {} to usergroup: {} on repo: {}'.format(
727 perm, group_name, repo), namespace='security.repo')
727 perm, group_name, repo), namespace='security.repo')
728
728
729 return obj
729 return obj
730
730
731 def revoke_user_group_permission(self, repo, group_name):
731 def revoke_user_group_permission(self, repo, group_name):
732 """
732 """
733 Revoke permission for user group on given repository
733 Revoke permission for user group on given repository
734
734
735 :param repo: Instance of Repository, repository_id, or repository name
735 :param repo: Instance of Repository, repository_id, or repository name
736 :param group_name: Instance of UserGroup, users_group_id,
736 :param group_name: Instance of UserGroup, users_group_id,
737 or user group name
737 or user group name
738 """
738 """
739 repo = self._get_repo(repo)
739 repo = self._get_repo(repo)
740 group_name = self._get_user_group(group_name)
740 group_name = self._get_user_group(group_name)
741
741
742 obj = self.sa.query(UserGroupRepoToPerm) \
742 obj = self.sa.query(UserGroupRepoToPerm) \
743 .filter(UserGroupRepoToPerm.repository == repo) \
743 .filter(UserGroupRepoToPerm.repository == repo) \
744 .filter(UserGroupRepoToPerm.users_group == group_name) \
744 .filter(UserGroupRepoToPerm.users_group == group_name) \
745 .scalar()
745 .scalar()
746 if obj:
746 if obj:
747 self.sa.delete(obj)
747 self.sa.delete(obj)
748 log.debug('Revoked perm to %s on %s', repo, group_name)
748 log.debug('Revoked perm to %s on %s', repo, group_name)
749 action_logger_generic(
749 action_logger_generic(
750 'revoked permission from usergroup: {} on repo: {}'.format(
750 'revoked permission from usergroup: {} on repo: {}'.format(
751 group_name, repo), namespace='security.repo')
751 group_name, repo), namespace='security.repo')
752
752
753 def delete_stats(self, repo_name):
753 def delete_stats(self, repo_name):
754 """
754 """
755 removes stats for given repo
755 removes stats for given repo
756
756
757 :param repo_name:
757 :param repo_name:
758 """
758 """
759 repo = self._get_repo(repo_name)
759 repo = self._get_repo(repo_name)
760 try:
760 try:
761 obj = self.sa.query(Statistics) \
761 obj = self.sa.query(Statistics) \
762 .filter(Statistics.repository == repo).scalar()
762 .filter(Statistics.repository == repo).scalar()
763 if obj:
763 if obj:
764 self.sa.delete(obj)
764 self.sa.delete(obj)
765 except Exception:
765 except Exception:
766 log.error(traceback.format_exc())
766 log.error(traceback.format_exc())
767 raise
767 raise
768
768
769 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
769 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
770 field_type='str', field_desc=''):
770 field_type='str', field_desc=''):
771
771
772 repo = self._get_repo(repo_name)
772 repo = self._get_repo(repo_name)
773
773
774 new_field = RepositoryField()
774 new_field = RepositoryField()
775 new_field.repository = repo
775 new_field.repository = repo
776 new_field.field_key = field_key
776 new_field.field_key = field_key
777 new_field.field_type = field_type # python type
777 new_field.field_type = field_type # python type
778 new_field.field_value = field_value
778 new_field.field_value = field_value
779 new_field.field_desc = field_desc
779 new_field.field_desc = field_desc
780 new_field.field_label = field_label
780 new_field.field_label = field_label
781 self.sa.add(new_field)
781 self.sa.add(new_field)
782 return new_field
782 return new_field
783
783
784 def delete_repo_field(self, repo_name, field_key):
784 def delete_repo_field(self, repo_name, field_key):
785 repo = self._get_repo(repo_name)
785 repo = self._get_repo(repo_name)
786 field = RepositoryField.get_by_key_name(field_key, repo)
786 field = RepositoryField.get_by_key_name(field_key, repo)
787 if field:
787 if field:
788 self.sa.delete(field)
788 self.sa.delete(field)
789
789
790 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
790 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
791 clone_uri=None, repo_store_location=None,
791 clone_uri=None, repo_store_location=None,
792 use_global_config=False):
792 use_global_config=False):
793 """
793 """
794 makes repository on filesystem. It's group aware means it'll create
794 makes repository on filesystem. It's group aware means it'll create
795 a repository within a group, and alter the paths accordingly of
795 a repository within a group, and alter the paths accordingly of
796 group location
796 group location
797
797
798 :param repo_name:
798 :param repo_name:
799 :param alias:
799 :param alias:
800 :param parent:
800 :param parent:
801 :param clone_uri:
801 :param clone_uri:
802 :param repo_store_location:
802 :param repo_store_location:
803 """
803 """
804 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
804 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
805 from rhodecode.model.scm import ScmModel
805 from rhodecode.model.scm import ScmModel
806
806
807 if Repository.NAME_SEP in repo_name:
807 if Repository.NAME_SEP in repo_name:
808 raise ValueError(
808 raise ValueError(
809 'repo_name must not contain groups got `%s`' % repo_name)
809 'repo_name must not contain groups got `%s`' % repo_name)
810
810
811 if isinstance(repo_group, RepoGroup):
811 if isinstance(repo_group, RepoGroup):
812 new_parent_path = os.sep.join(repo_group.full_path_splitted)
812 new_parent_path = os.sep.join(repo_group.full_path_splitted)
813 else:
813 else:
814 new_parent_path = repo_group or ''
814 new_parent_path = repo_group or ''
815
815
816 if repo_store_location:
816 if repo_store_location:
817 _paths = [repo_store_location]
817 _paths = [repo_store_location]
818 else:
818 else:
819 _paths = [self.repos_path, new_parent_path, repo_name]
819 _paths = [self.repos_path, new_parent_path, repo_name]
820 # we need to make it str for mercurial
820 # we need to make it str for mercurial
821 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
821 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
822
822
823 # check if this path is not a repository
823 # check if this path is not a repository
824 if is_valid_repo(repo_path, self.repos_path):
824 if is_valid_repo(repo_path, self.repos_path):
825 raise Exception('This path %s is a valid repository' % repo_path)
825 raise Exception('This path %s is a valid repository' % repo_path)
826
826
827 # check if this path is a group
827 # check if this path is a group
828 if is_valid_repo_group(repo_path, self.repos_path):
828 if is_valid_repo_group(repo_path, self.repos_path):
829 raise Exception('This path %s is a valid group' % repo_path)
829 raise Exception('This path %s is a valid group' % repo_path)
830
830
831 log.info('creating repo %s in %s from url: `%s`',
831 log.info('creating repo %s in %s from url: `%s`',
832 repo_name, safe_unicode(repo_path),
832 repo_name, safe_unicode(repo_path),
833 obfuscate_url_pw(clone_uri))
833 obfuscate_url_pw(clone_uri))
834
834
835 backend = get_backend(repo_type)
835 backend = get_backend(repo_type)
836
836
837 config_repo = None if use_global_config else repo_name
837 config_repo = None if use_global_config else repo_name
838 if config_repo and new_parent_path:
838 if config_repo and new_parent_path:
839 config_repo = Repository.NAME_SEP.join(
839 config_repo = Repository.NAME_SEP.join(
840 (new_parent_path, config_repo))
840 (new_parent_path, config_repo))
841 config = make_db_config(clear_session=False, repo=config_repo)
841 config = make_db_config(clear_session=False, repo=config_repo)
842 config.set('extensions', 'largefiles', '')
842 config.set('extensions', 'largefiles', '')
843
843
844 # patch and reset hooks section of UI config to not run any
844 # patch and reset hooks section of UI config to not run any
845 # hooks on creating remote repo
845 # hooks on creating remote repo
846 config.clear_section('hooks')
846 config.clear_section('hooks')
847
847
848 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
848 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
849 if repo_type == 'git':
849 if repo_type == 'git':
850 repo = backend(
850 repo = backend(
851 repo_path, config=config, create=True, src_url=clone_uri,
851 repo_path, config=config, create=True, src_url=clone_uri,
852 bare=True)
852 bare=True)
853 else:
853 else:
854 repo = backend(
854 repo = backend(
855 repo_path, config=config, create=True, src_url=clone_uri)
855 repo_path, config=config, create=True, src_url=clone_uri)
856
856
857 ScmModel().install_hooks(repo, repo_type=repo_type)
857 ScmModel().install_hooks(repo, repo_type=repo_type)
858
858
859 log.debug('Created repo %s with %s backend',
859 log.debug('Created repo %s with %s backend',
860 safe_unicode(repo_name), safe_unicode(repo_type))
860 safe_unicode(repo_name), safe_unicode(repo_type))
861 return repo
861 return repo
862
862
863 def _rename_filesystem_repo(self, old, new):
863 def _rename_filesystem_repo(self, old, new):
864 """
864 """
865 renames repository on filesystem
865 renames repository on filesystem
866
866
867 :param old: old name
867 :param old: old name
868 :param new: new name
868 :param new: new name
869 """
869 """
870 log.info('renaming repo from %s to %s', old, new)
870 log.info('renaming repo from %s to %s', old, new)
871
871
872 old_path = os.path.join(self.repos_path, old)
872 old_path = os.path.join(self.repos_path, old)
873 new_path = os.path.join(self.repos_path, new)
873 new_path = os.path.join(self.repos_path, new)
874 if os.path.isdir(new_path):
874 if os.path.isdir(new_path):
875 raise Exception(
875 raise Exception(
876 'Was trying to rename to already existing dir %s' % new_path
876 'Was trying to rename to already existing dir %s' % new_path
877 )
877 )
878 shutil.move(old_path, new_path)
878 shutil.move(old_path, new_path)
879
879
880 def _delete_filesystem_repo(self, repo):
880 def _delete_filesystem_repo(self, repo):
881 """
881 """
882 removes repo from filesystem, the removal is acctually made by
882 removes repo from filesystem, the removal is acctually made by
883 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
883 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
884 repository is no longer valid for rhodecode, can be undeleted later on
884 repository is no longer valid for rhodecode, can be undeleted later on
885 by reverting the renames on this repository
885 by reverting the renames on this repository
886
886
887 :param repo: repo object
887 :param repo: repo object
888 """
888 """
889 rm_path = os.path.join(self.repos_path, repo.repo_name)
889 rm_path = os.path.join(self.repos_path, repo.repo_name)
890 repo_group = repo.group
890 repo_group = repo.group
891 log.info("Removing repository %s", rm_path)
891 log.info("Removing repository %s", rm_path)
892 # disable hg/git internal that it doesn't get detected as repo
892 # disable hg/git internal that it doesn't get detected as repo
893 alias = repo.repo_type
893 alias = repo.repo_type
894
894
895 config = make_db_config(clear_session=False)
895 config = make_db_config(clear_session=False)
896 config.set('extensions', 'largefiles', '')
896 config.set('extensions', 'largefiles', '')
897 bare = getattr(repo.scm_instance(config=config), 'bare', False)
897 bare = getattr(repo.scm_instance(config=config), 'bare', False)
898
898
899 # skip this for bare git repos
899 # skip this for bare git repos
900 if not bare:
900 if not bare:
901 # disable VCS repo
901 # disable VCS repo
902 vcs_path = os.path.join(rm_path, '.%s' % alias)
902 vcs_path = os.path.join(rm_path, '.%s' % alias)
903 if os.path.exists(vcs_path):
903 if os.path.exists(vcs_path):
904 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
904 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
905
905
906 _now = datetime.datetime.now()
906 _now = datetime.datetime.now()
907 _ms = str(_now.microsecond).rjust(6, '0')
907 _ms = str(_now.microsecond).rjust(6, '0')
908 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
908 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
909 repo.just_name)
909 repo.just_name)
910 if repo_group:
910 if repo_group:
911 # if repository is in group, prefix the removal path with the group
911 # if repository is in group, prefix the removal path with the group
912 args = repo_group.full_path_splitted + [_d]
912 args = repo_group.full_path_splitted + [_d]
913 _d = os.path.join(*args)
913 _d = os.path.join(*args)
914
914
915 if os.path.isdir(rm_path):
915 if os.path.isdir(rm_path):
916 shutil.move(rm_path, os.path.join(self.repos_path, _d))
916 shutil.move(rm_path, os.path.join(self.repos_path, _d))
917
917
918
918
919 class ReadmeFinder:
919 class ReadmeFinder:
920 """
920 """
921 Utility which knows how to find a readme for a specific commit.
921 Utility which knows how to find a readme for a specific commit.
922
922
923 The main idea is that this is a configurable algorithm. When creating an
923 The main idea is that this is a configurable algorithm. When creating an
924 instance you can define parameters, currently only the `default_renderer`.
924 instance you can define parameters, currently only the `default_renderer`.
925 Based on this configuration the method :meth:`search` behaves slightly
925 Based on this configuration the method :meth:`search` behaves slightly
926 different.
926 different.
927 """
927 """
928
928
929 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
929 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
930 path_re = re.compile(r'^docs?', re.IGNORECASE)
930 path_re = re.compile(r'^docs?', re.IGNORECASE)
931
931
932 default_priorities = {
932 default_priorities = {
933 None: 0,
933 None: 0,
934 '.text': 2,
934 '.text': 2,
935 '.txt': 3,
935 '.txt': 3,
936 '.rst': 1,
936 '.rst': 1,
937 '.rest': 2,
937 '.rest': 2,
938 '.md': 1,
938 '.md': 1,
939 '.mkdn': 2,
939 '.mkdn': 2,
940 '.mdown': 3,
940 '.mdown': 3,
941 '.markdown': 4,
941 '.markdown': 4,
942 }
942 }
943
943
944 path_priority = {
944 path_priority = {
945 'doc': 0,
945 'doc': 0,
946 'docs': 1,
946 'docs': 1,
947 }
947 }
948
948
949 FALLBACK_PRIORITY = 99
949 FALLBACK_PRIORITY = 99
950
950
951 RENDERER_TO_EXTENSION = {
951 RENDERER_TO_EXTENSION = {
952 'rst': ['.rst', '.rest'],
952 'rst': ['.rst', '.rest'],
953 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
953 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
954 }
954 }
955
955
956 def __init__(self, default_renderer=None):
956 def __init__(self, default_renderer=None):
957 self._default_renderer = default_renderer
957 self._default_renderer = default_renderer
958 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
958 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
959 default_renderer, [])
959 default_renderer, [])
960
960
961 def search(self, commit, path='/'):
961 def search(self, commit, path='/'):
962 """
962 """
963 Find a readme in the given `commit`.
963 Find a readme in the given `commit`.
964 """
964 """
965 nodes = commit.get_nodes(path)
965 nodes = commit.get_nodes(path)
966 matches = self._match_readmes(nodes)
966 matches = self._match_readmes(nodes)
967 matches = self._sort_according_to_priority(matches)
967 matches = self._sort_according_to_priority(matches)
968 if matches:
968 if matches:
969 return matches[0].node
969 return matches[0].node
970
970
971 paths = self._match_paths(nodes)
971 paths = self._match_paths(nodes)
972 paths = self._sort_paths_according_to_priority(paths)
972 paths = self._sort_paths_according_to_priority(paths)
973 for path in paths:
973 for path in paths:
974 match = self.search(commit, path=path)
974 match = self.search(commit, path=path)
975 if match:
975 if match:
976 return match
976 return match
977
977
978 return None
978 return None
979
979
980 def _match_readmes(self, nodes):
980 def _match_readmes(self, nodes):
981 for node in nodes:
981 for node in nodes:
982 if not node.is_file():
982 if not node.is_file():
983 continue
983 continue
984 path = node.path.rsplit('/', 1)[-1]
984 path = node.path.rsplit('/', 1)[-1]
985 match = self.readme_re.match(path)
985 match = self.readme_re.match(path)
986 if match:
986 if match:
987 extension = match.group(1)
987 extension = match.group(1)
988 yield ReadmeMatch(node, match, self._priority(extension))
988 yield ReadmeMatch(node, match, self._priority(extension))
989
989
990 def _match_paths(self, nodes):
990 def _match_paths(self, nodes):
991 for node in nodes:
991 for node in nodes:
992 if not node.is_dir():
992 if not node.is_dir():
993 continue
993 continue
994 match = self.path_re.match(node.path)
994 match = self.path_re.match(node.path)
995 if match:
995 if match:
996 yield node.path
996 yield node.path
997
997
998 def _priority(self, extension):
998 def _priority(self, extension):
999 renderer_priority = (
999 renderer_priority = (
1000 0 if extension in self._renderer_extensions else 1)
1000 0 if extension in self._renderer_extensions else 1)
1001 extension_priority = self.default_priorities.get(
1001 extension_priority = self.default_priorities.get(
1002 extension, self.FALLBACK_PRIORITY)
1002 extension, self.FALLBACK_PRIORITY)
1003 return (renderer_priority, extension_priority)
1003 return (renderer_priority, extension_priority)
1004
1004
1005 def _sort_according_to_priority(self, matches):
1005 def _sort_according_to_priority(self, matches):
1006
1006
1007 def priority_and_path(match):
1007 def priority_and_path(match):
1008 return (match.priority, match.path)
1008 return (match.priority, match.path)
1009
1009
1010 return sorted(matches, key=priority_and_path)
1010 return sorted(matches, key=priority_and_path)
1011
1011
1012 def _sort_paths_according_to_priority(self, paths):
1012 def _sort_paths_according_to_priority(self, paths):
1013
1013
1014 def priority_and_path(path):
1014 def priority_and_path(path):
1015 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1015 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1016
1016
1017 return sorted(paths, key=priority_and_path)
1017 return sorted(paths, key=priority_and_path)
1018
1018
1019
1019
1020 class ReadmeMatch:
1020 class ReadmeMatch:
1021
1021
1022 def __init__(self, node, match, priority):
1022 def __init__(self, node, match, priority):
1023 self.node = node
1023 self.node = node
1024 self._match = match
1024 self._match = match
1025 self.priority = priority
1025 self.priority = priority
1026
1026
1027 @property
1027 @property
1028 def path(self):
1028 def path(self):
1029 return self.node.path
1029 return self.node.path
1030
1030
1031 def __repr__(self):
1031 def __repr__(self):
1032 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
1032 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,764 +1,764 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 repo group model for RhodeCode
23 repo group model for RhodeCode
24 """
24 """
25
25
26 import os
26 import os
27 import datetime
27 import datetime
28 import itertools
28 import itertools
29 import logging
29 import logging
30 import shutil
30 import shutil
31 import traceback
31 import traceback
32 import string
32 import string
33
33
34 from zope.cachedescriptors.property import Lazy as LazyProperty
34 from zope.cachedescriptors.property import Lazy as LazyProperty
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.model import BaseModel
37 from rhodecode.model import BaseModel
38 from rhodecode.model.db import (_hash_key,
38 from rhodecode.model.db import (_hash_key,
39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 UserGroup, Repository)
40 UserGroup, Repository)
41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
43 from rhodecode.lib.utils2 import action_logger_generic, datetime_to_time
43 from rhodecode.lib.utils2 import action_logger_generic, datetime_to_time
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class RepoGroupModel(BaseModel):
48 class RepoGroupModel(BaseModel):
49
49
50 cls = RepoGroup
50 cls = RepoGroup
51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
52 PERSONAL_GROUP_PATTERN = '${username}' # default
52 PERSONAL_GROUP_PATTERN = '${username}' # default
53
53
54 def _get_user_group(self, users_group):
54 def _get_user_group(self, users_group):
55 return self._get_instance(UserGroup, users_group,
55 return self._get_instance(UserGroup, users_group,
56 callback=UserGroup.get_by_group_name)
56 callback=UserGroup.get_by_group_name)
57
57
58 def _get_repo_group(self, repo_group):
58 def _get_repo_group(self, repo_group):
59 return self._get_instance(RepoGroup, repo_group,
59 return self._get_instance(RepoGroup, repo_group,
60 callback=RepoGroup.get_by_group_name)
60 callback=RepoGroup.get_by_group_name)
61
61
62 @LazyProperty
62 @LazyProperty
63 def repos_path(self):
63 def repos_path(self):
64 """
64 """
65 Gets the repositories root path from database
65 Gets the repositories root path from database
66 """
66 """
67
67
68 settings_model = VcsSettingsModel(sa=self.sa)
68 settings_model = VcsSettingsModel(sa=self.sa)
69 return settings_model.get_repos_location()
69 return settings_model.get_repos_location()
70
70
71 def get_by_group_name(self, repo_group_name, cache=None):
71 def get_by_group_name(self, repo_group_name, cache=None):
72 repo = self.sa.query(RepoGroup) \
72 repo = self.sa.query(RepoGroup) \
73 .filter(RepoGroup.group_name == repo_group_name)
73 .filter(RepoGroup.group_name == repo_group_name)
74
74
75 if cache:
75 if cache:
76 name_key = _hash_key(repo_group_name)
76 name_key = _hash_key(repo_group_name)
77 repo = repo.options(
77 repo = repo.options(
78 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
78 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
79 return repo.scalar()
79 return repo.scalar()
80
80
81 def get_default_create_personal_repo_group(self):
81 def get_default_create_personal_repo_group(self):
82 value = SettingsModel().get_setting_by_name(
82 value = SettingsModel().get_setting_by_name(
83 'create_personal_repo_group')
83 'create_personal_repo_group')
84 return value.app_settings_value if value else None or False
84 return value.app_settings_value if value else None or False
85
85
86 def get_personal_group_name_pattern(self):
86 def get_personal_group_name_pattern(self):
87 value = SettingsModel().get_setting_by_name(
87 value = SettingsModel().get_setting_by_name(
88 'personal_repo_group_pattern')
88 'personal_repo_group_pattern')
89 val = value.app_settings_value if value else None
89 val = value.app_settings_value if value else None
90 group_template = val or self.PERSONAL_GROUP_PATTERN
90 group_template = val or self.PERSONAL_GROUP_PATTERN
91
91
92 group_template = group_template.lstrip('/')
92 group_template = group_template.lstrip('/')
93 return group_template
93 return group_template
94
94
95 def get_personal_group_name(self, user):
95 def get_personal_group_name(self, user):
96 template = self.get_personal_group_name_pattern()
96 template = self.get_personal_group_name_pattern()
97 return string.Template(template).safe_substitute(
97 return string.Template(template).safe_substitute(
98 username=user.username,
98 username=user.username,
99 user_id=user.user_id,
99 user_id=user.user_id,
100 )
100 )
101
101
102 def create_personal_repo_group(self, user, commit_early=True):
102 def create_personal_repo_group(self, user, commit_early=True):
103 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
103 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
104 personal_repo_group_name = self.get_personal_group_name(user)
104 personal_repo_group_name = self.get_personal_group_name(user)
105
105
106 # create a new one
106 # create a new one
107 RepoGroupModel().create(
107 RepoGroupModel().create(
108 group_name=personal_repo_group_name,
108 group_name=personal_repo_group_name,
109 group_description=desc,
109 group_description=desc,
110 owner=user.username,
110 owner=user.username,
111 personal=True,
111 personal=True,
112 commit_early=commit_early)
112 commit_early=commit_early)
113
113
114 def _create_default_perms(self, new_group):
114 def _create_default_perms(self, new_group):
115 # create default permission
115 # create default permission
116 default_perm = 'group.read'
116 default_perm = 'group.read'
117 def_user = User.get_default_user()
117 def_user = User.get_default_user()
118 for p in def_user.user_perms:
118 for p in def_user.user_perms:
119 if p.permission.permission_name.startswith('group.'):
119 if p.permission.permission_name.startswith('group.'):
120 default_perm = p.permission.permission_name
120 default_perm = p.permission.permission_name
121 break
121 break
122
122
123 repo_group_to_perm = UserRepoGroupToPerm()
123 repo_group_to_perm = UserRepoGroupToPerm()
124 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
124 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
125
125
126 repo_group_to_perm.group = new_group
126 repo_group_to_perm.group = new_group
127 repo_group_to_perm.user_id = def_user.user_id
127 repo_group_to_perm.user_id = def_user.user_id
128 return repo_group_to_perm
128 return repo_group_to_perm
129
129
130 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
130 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
131 get_object=False):
131 get_object=False):
132 """
132 """
133 Get's the group name and a parent group name from given group name.
133 Get's the group name and a parent group name from given group name.
134 If repo_in_path is set to truth, we asume the full path also includes
134 If repo_in_path is set to truth, we asume the full path also includes
135 repo name, in such case we clean the last element.
135 repo name, in such case we clean the last element.
136
136
137 :param group_name_full:
137 :param group_name_full:
138 """
138 """
139 split_paths = 1
139 split_paths = 1
140 if repo_in_path:
140 if repo_in_path:
141 split_paths = 2
141 split_paths = 2
142 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
142 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
143
143
144 if repo_in_path and len(_parts) > 1:
144 if repo_in_path and len(_parts) > 1:
145 # such case last element is the repo_name
145 # such case last element is the repo_name
146 _parts.pop(-1)
146 _parts.pop(-1)
147 group_name_cleaned = _parts[-1] # just the group name
147 group_name_cleaned = _parts[-1] # just the group name
148 parent_repo_group_name = None
148 parent_repo_group_name = None
149
149
150 if len(_parts) > 1:
150 if len(_parts) > 1:
151 parent_repo_group_name = _parts[0]
151 parent_repo_group_name = _parts[0]
152
152
153 parent_group = None
153 parent_group = None
154 if parent_repo_group_name:
154 if parent_repo_group_name:
155 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
155 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
156
156
157 if get_object:
157 if get_object:
158 return group_name_cleaned, parent_repo_group_name, parent_group
158 return group_name_cleaned, parent_repo_group_name, parent_group
159
159
160 return group_name_cleaned, parent_repo_group_name
160 return group_name_cleaned, parent_repo_group_name
161
161
162 def check_exist_filesystem(self, group_name, exc_on_failure=True):
162 def check_exist_filesystem(self, group_name, exc_on_failure=True):
163 create_path = os.path.join(self.repos_path, group_name)
163 create_path = os.path.join(self.repos_path, group_name)
164 log.debug('creating new group in %s', create_path)
164 log.debug('creating new group in %s', create_path)
165
165
166 if os.path.isdir(create_path):
166 if os.path.isdir(create_path):
167 if exc_on_failure:
167 if exc_on_failure:
168 abs_create_path = os.path.abspath(create_path)
168 abs_create_path = os.path.abspath(create_path)
169 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
169 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
170 return False
170 return False
171 return True
171 return True
172
172
173 def _create_group(self, group_name):
173 def _create_group(self, group_name):
174 """
174 """
175 makes repository group on filesystem
175 makes repository group on filesystem
176
176
177 :param repo_name:
177 :param repo_name:
178 :param parent_id:
178 :param parent_id:
179 """
179 """
180
180
181 self.check_exist_filesystem(group_name)
181 self.check_exist_filesystem(group_name)
182 create_path = os.path.join(self.repos_path, group_name)
182 create_path = os.path.join(self.repos_path, group_name)
183 log.debug('creating new group in %s', create_path)
183 log.debug('creating new group in %s', create_path)
184 os.makedirs(create_path, mode=0755)
184 os.makedirs(create_path, mode=0755)
185 log.debug('created group in %s', create_path)
185 log.debug('created group in %s', create_path)
186
186
187 def _rename_group(self, old, new):
187 def _rename_group(self, old, new):
188 """
188 """
189 Renames a group on filesystem
189 Renames a group on filesystem
190
190
191 :param group_name:
191 :param group_name:
192 """
192 """
193
193
194 if old == new:
194 if old == new:
195 log.debug('skipping group rename')
195 log.debug('skipping group rename')
196 return
196 return
197
197
198 log.debug('renaming repository group from %s to %s', old, new)
198 log.debug('renaming repository group from %s to %s', old, new)
199
199
200 old_path = os.path.join(self.repos_path, old)
200 old_path = os.path.join(self.repos_path, old)
201 new_path = os.path.join(self.repos_path, new)
201 new_path = os.path.join(self.repos_path, new)
202
202
203 log.debug('renaming repos paths from %s to %s', old_path, new_path)
203 log.debug('renaming repos paths from %s to %s', old_path, new_path)
204
204
205 if os.path.isdir(new_path):
205 if os.path.isdir(new_path):
206 raise Exception('Was trying to rename to already '
206 raise Exception('Was trying to rename to already '
207 'existing dir %s' % new_path)
207 'existing dir %s' % new_path)
208 shutil.move(old_path, new_path)
208 shutil.move(old_path, new_path)
209
209
210 def _delete_filesystem_group(self, group, force_delete=False):
210 def _delete_filesystem_group(self, group, force_delete=False):
211 """
211 """
212 Deletes a group from a filesystem
212 Deletes a group from a filesystem
213
213
214 :param group: instance of group from database
214 :param group: instance of group from database
215 :param force_delete: use shutil rmtree to remove all objects
215 :param force_delete: use shutil rmtree to remove all objects
216 """
216 """
217 paths = group.full_path.split(RepoGroup.url_sep())
217 paths = group.full_path.split(RepoGroup.url_sep())
218 paths = os.sep.join(paths)
218 paths = os.sep.join(paths)
219
219
220 rm_path = os.path.join(self.repos_path, paths)
220 rm_path = os.path.join(self.repos_path, paths)
221 log.info("Removing group %s", rm_path)
221 log.info("Removing group %s", rm_path)
222 # delete only if that path really exists
222 # delete only if that path really exists
223 if os.path.isdir(rm_path):
223 if os.path.isdir(rm_path):
224 if force_delete:
224 if force_delete:
225 shutil.rmtree(rm_path)
225 shutil.rmtree(rm_path)
226 else:
226 else:
227 # archive that group`
227 # archive that group`
228 _now = datetime.datetime.now()
228 _now = datetime.datetime.now()
229 _ms = str(_now.microsecond).rjust(6, '0')
229 _ms = str(_now.microsecond).rjust(6, '0')
230 _d = 'rm__%s_GROUP_%s' % (
230 _d = 'rm__%s_GROUP_%s' % (
231 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
231 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
232 shutil.move(rm_path, os.path.join(self.repos_path, _d))
232 shutil.move(rm_path, os.path.join(self.repos_path, _d))
233
233
234 def create(self, group_name, group_description, owner, just_db=False,
234 def create(self, group_name, group_description, owner, just_db=False,
235 copy_permissions=False, personal=None, commit_early=True):
235 copy_permissions=False, personal=None, commit_early=True):
236
236
237 (group_name_cleaned,
237 (group_name_cleaned,
238 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
238 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
239
239
240 parent_group = None
240 parent_group = None
241 if parent_group_name:
241 if parent_group_name:
242 parent_group = self._get_repo_group(parent_group_name)
242 parent_group = self._get_repo_group(parent_group_name)
243 if not parent_group:
243 if not parent_group:
244 # we tried to create a nested group, but the parent is not
244 # we tried to create a nested group, but the parent is not
245 # existing
245 # existing
246 raise ValueError(
246 raise ValueError(
247 'Parent group `%s` given in `%s` group name '
247 'Parent group `%s` given in `%s` group name '
248 'is not yet existing.' % (parent_group_name, group_name))
248 'is not yet existing.' % (parent_group_name, group_name))
249
249
250 # because we are doing a cleanup, we need to check if such directory
250 # because we are doing a cleanup, we need to check if such directory
251 # already exists. If we don't do that we can accidentally delete
251 # already exists. If we don't do that we can accidentally delete
252 # existing directory via cleanup that can cause data issues, since
252 # existing directory via cleanup that can cause data issues, since
253 # delete does a folder rename to special syntax later cleanup
253 # delete does a folder rename to special syntax later cleanup
254 # functions can delete this
254 # functions can delete this
255 cleanup_group = self.check_exist_filesystem(group_name,
255 cleanup_group = self.check_exist_filesystem(group_name,
256 exc_on_failure=False)
256 exc_on_failure=False)
257 user = self._get_user(owner)
257 user = self._get_user(owner)
258 if not user:
258 if not user:
259 raise ValueError('Owner %s not found as rhodecode user', owner)
259 raise ValueError('Owner %s not found as rhodecode user', owner)
260
260
261 try:
261 try:
262 new_repo_group = RepoGroup()
262 new_repo_group = RepoGroup()
263 new_repo_group.user = user
263 new_repo_group.user = user
264 new_repo_group.group_description = group_description or group_name
264 new_repo_group.group_description = group_description or group_name
265 new_repo_group.parent_group = parent_group
265 new_repo_group.parent_group = parent_group
266 new_repo_group.group_name = group_name
266 new_repo_group.group_name = group_name
267 new_repo_group.personal = personal
267 new_repo_group.personal = personal
268
268
269 self.sa.add(new_repo_group)
269 self.sa.add(new_repo_group)
270
270
271 # create an ADMIN permission for owner except if we're super admin,
271 # create an ADMIN permission for owner except if we're super admin,
272 # later owner should go into the owner field of groups
272 # later owner should go into the owner field of groups
273 if not user.is_admin:
273 if not user.is_admin:
274 self.grant_user_permission(repo_group=new_repo_group,
274 self.grant_user_permission(repo_group=new_repo_group,
275 user=owner, perm='group.admin')
275 user=owner, perm='group.admin')
276
276
277 if parent_group and copy_permissions:
277 if parent_group and copy_permissions:
278 # copy permissions from parent
278 # copy permissions from parent
279 user_perms = UserRepoGroupToPerm.query() \
279 user_perms = UserRepoGroupToPerm.query() \
280 .filter(UserRepoGroupToPerm.group == parent_group).all()
280 .filter(UserRepoGroupToPerm.group == parent_group).all()
281
281
282 group_perms = UserGroupRepoGroupToPerm.query() \
282 group_perms = UserGroupRepoGroupToPerm.query() \
283 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
283 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
284
284
285 for perm in user_perms:
285 for perm in user_perms:
286 # don't copy over the permission for user who is creating
286 # don't copy over the permission for user who is creating
287 # this group, if he is not super admin he get's admin
287 # this group, if he is not super admin he get's admin
288 # permission set above
288 # permission set above
289 if perm.user != user or user.is_admin:
289 if perm.user != user or user.is_admin:
290 UserRepoGroupToPerm.create(
290 UserRepoGroupToPerm.create(
291 perm.user, new_repo_group, perm.permission)
291 perm.user, new_repo_group, perm.permission)
292
292
293 for perm in group_perms:
293 for perm in group_perms:
294 UserGroupRepoGroupToPerm.create(
294 UserGroupRepoGroupToPerm.create(
295 perm.users_group, new_repo_group, perm.permission)
295 perm.users_group, new_repo_group, perm.permission)
296 else:
296 else:
297 perm_obj = self._create_default_perms(new_repo_group)
297 perm_obj = self._create_default_perms(new_repo_group)
298 self.sa.add(perm_obj)
298 self.sa.add(perm_obj)
299
299
300 # now commit the changes, earlier so we are sure everything is in
300 # now commit the changes, earlier so we are sure everything is in
301 # the database.
301 # the database.
302 if commit_early:
302 if commit_early:
303 self.sa.commit()
303 self.sa.commit()
304 if not just_db:
304 if not just_db:
305 self._create_group(new_repo_group.group_name)
305 self._create_group(new_repo_group.group_name)
306
306
307 # trigger the post hook
307 # trigger the post hook
308 from rhodecode.lib.hooks_base import log_create_repository_group
308 from rhodecode.lib.hooks_base import log_create_repository_group
309 repo_group = RepoGroup.get_by_group_name(group_name)
309 repo_group = RepoGroup.get_by_group_name(group_name)
310 log_create_repository_group(
310 log_create_repository_group(
311 created_by=user.username, **repo_group.get_dict())
311 created_by=user.username, **repo_group.get_dict())
312
312
313 # Trigger create event.
313 # Trigger create event.
314 events.trigger(events.RepoGroupCreateEvent(repo_group))
314 events.trigger(events.RepoGroupCreateEvent(repo_group))
315
315
316 return new_repo_group
316 return new_repo_group
317 except Exception:
317 except Exception:
318 self.sa.rollback()
318 self.sa.rollback()
319 log.exception('Exception occurred when creating repository group, '
319 log.exception('Exception occurred when creating repository group, '
320 'doing cleanup...')
320 'doing cleanup...')
321 # rollback things manually !
321 # rollback things manually !
322 repo_group = RepoGroup.get_by_group_name(group_name)
322 repo_group = RepoGroup.get_by_group_name(group_name)
323 if repo_group:
323 if repo_group:
324 RepoGroup.delete(repo_group.group_id)
324 RepoGroup.delete(repo_group.group_id)
325 self.sa.commit()
325 self.sa.commit()
326 if cleanup_group:
326 if cleanup_group:
327 RepoGroupModel()._delete_filesystem_group(repo_group)
327 RepoGroupModel()._delete_filesystem_group(repo_group)
328 raise
328 raise
329
329
330 def update_permissions(
330 def update_permissions(
331 self, repo_group, perm_additions=None, perm_updates=None,
331 self, repo_group, perm_additions=None, perm_updates=None,
332 perm_deletions=None, recursive=None, check_perms=True,
332 perm_deletions=None, recursive=None, check_perms=True,
333 cur_user=None):
333 cur_user=None):
334 from rhodecode.model.repo import RepoModel
334 from rhodecode.model.repo import RepoModel
335 from rhodecode.lib.auth import HasUserGroupPermissionAny
335 from rhodecode.lib.auth import HasUserGroupPermissionAny
336
336
337 if not perm_additions:
337 if not perm_additions:
338 perm_additions = []
338 perm_additions = []
339 if not perm_updates:
339 if not perm_updates:
340 perm_updates = []
340 perm_updates = []
341 if not perm_deletions:
341 if not perm_deletions:
342 perm_deletions = []
342 perm_deletions = []
343
343
344 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
344 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
345
345
346 changes = {
346 changes = {
347 'added': [],
347 'added': [],
348 'updated': [],
348 'updated': [],
349 'deleted': []
349 'deleted': []
350 }
350 }
351
351
352 def _set_perm_user(obj, user, perm):
352 def _set_perm_user(obj, user, perm):
353 if isinstance(obj, RepoGroup):
353 if isinstance(obj, RepoGroup):
354 self.grant_user_permission(
354 self.grant_user_permission(
355 repo_group=obj, user=user, perm=perm)
355 repo_group=obj, user=user, perm=perm)
356 elif isinstance(obj, Repository):
356 elif isinstance(obj, Repository):
357 # private repos will not allow to change the default
357 # private repos will not allow to change the default
358 # permissions using recursive mode
358 # permissions using recursive mode
359 if obj.private and user == User.DEFAULT_USER:
359 if obj.private and user == User.DEFAULT_USER:
360 return
360 return
361
361
362 # we set group permission but we have to switch to repo
362 # we set group permission but we have to switch to repo
363 # permission
363 # permission
364 perm = perm.replace('group.', 'repository.')
364 perm = perm.replace('group.', 'repository.')
365 RepoModel().grant_user_permission(
365 RepoModel().grant_user_permission(
366 repo=obj, user=user, perm=perm)
366 repo=obj, user=user, perm=perm)
367
367
368 def _set_perm_group(obj, users_group, perm):
368 def _set_perm_group(obj, users_group, perm):
369 if isinstance(obj, RepoGroup):
369 if isinstance(obj, RepoGroup):
370 self.grant_user_group_permission(
370 self.grant_user_group_permission(
371 repo_group=obj, group_name=users_group, perm=perm)
371 repo_group=obj, group_name=users_group, perm=perm)
372 elif isinstance(obj, Repository):
372 elif isinstance(obj, Repository):
373 # we set group permission but we have to switch to repo
373 # we set group permission but we have to switch to repo
374 # permission
374 # permission
375 perm = perm.replace('group.', 'repository.')
375 perm = perm.replace('group.', 'repository.')
376 RepoModel().grant_user_group_permission(
376 RepoModel().grant_user_group_permission(
377 repo=obj, group_name=users_group, perm=perm)
377 repo=obj, group_name=users_group, perm=perm)
378
378
379 def _revoke_perm_user(obj, user):
379 def _revoke_perm_user(obj, user):
380 if isinstance(obj, RepoGroup):
380 if isinstance(obj, RepoGroup):
381 self.revoke_user_permission(repo_group=obj, user=user)
381 self.revoke_user_permission(repo_group=obj, user=user)
382 elif isinstance(obj, Repository):
382 elif isinstance(obj, Repository):
383 RepoModel().revoke_user_permission(repo=obj, user=user)
383 RepoModel().revoke_user_permission(repo=obj, user=user)
384
384
385 def _revoke_perm_group(obj, user_group):
385 def _revoke_perm_group(obj, user_group):
386 if isinstance(obj, RepoGroup):
386 if isinstance(obj, RepoGroup):
387 self.revoke_user_group_permission(
387 self.revoke_user_group_permission(
388 repo_group=obj, group_name=user_group)
388 repo_group=obj, group_name=user_group)
389 elif isinstance(obj, Repository):
389 elif isinstance(obj, Repository):
390 RepoModel().revoke_user_group_permission(
390 RepoModel().revoke_user_group_permission(
391 repo=obj, group_name=user_group)
391 repo=obj, group_name=user_group)
392
392
393 # start updates
393 # start updates
394 log.debug('Now updating permissions for %s in recursive mode:%s',
394 log.debug('Now updating permissions for %s in recursive mode:%s',
395 repo_group, recursive)
395 repo_group, recursive)
396
396
397 # initialize check function, we'll call that multiple times
397 # initialize check function, we'll call that multiple times
398 has_group_perm = HasUserGroupPermissionAny(*req_perms)
398 has_group_perm = HasUserGroupPermissionAny(*req_perms)
399
399
400 for obj in repo_group.recursive_groups_and_repos():
400 for obj in repo_group.recursive_groups_and_repos():
401 # iterated obj is an instance of a repos group or repository in
401 # iterated obj is an instance of a repos group or repository in
402 # that group, recursive option can be: none, repos, groups, all
402 # that group, recursive option can be: none, repos, groups, all
403 if recursive == 'all':
403 if recursive == 'all':
404 obj = obj
404 obj = obj
405 elif recursive == 'repos':
405 elif recursive == 'repos':
406 # skip groups, other than this one
406 # skip groups, other than this one
407 if isinstance(obj, RepoGroup) and not obj == repo_group:
407 if isinstance(obj, RepoGroup) and not obj == repo_group:
408 continue
408 continue
409 elif recursive == 'groups':
409 elif recursive == 'groups':
410 # skip repos
410 # skip repos
411 if isinstance(obj, Repository):
411 if isinstance(obj, Repository):
412 continue
412 continue
413 else: # recursive == 'none':
413 else: # recursive == 'none':
414 # DEFAULT option - don't apply to iterated objects
414 # DEFAULT option - don't apply to iterated objects
415 # also we do a break at the end of this loop. if we are not
415 # also we do a break at the end of this loop. if we are not
416 # in recursive mode
416 # in recursive mode
417 obj = repo_group
417 obj = repo_group
418
418
419 change_obj = obj.get_api_data()
419 change_obj = obj.get_api_data()
420
420
421 # update permissions
421 # update permissions
422 for member_id, perm, member_type in perm_updates:
422 for member_id, perm, member_type in perm_updates:
423 member_id = int(member_id)
423 member_id = int(member_id)
424 if member_type == 'user':
424 if member_type == 'user':
425 member_name = User.get(member_id).username
425 member_name = User.get(member_id).username
426 # this updates also current one if found
426 # this updates also current one if found
427 _set_perm_user(obj, user=member_id, perm=perm)
427 _set_perm_user(obj, user=member_id, perm=perm)
428 else: # set for user group
428 else: # set for user group
429 member_name = UserGroup.get(member_id).users_group_name
429 member_name = UserGroup.get(member_id).users_group_name
430 if not check_perms or has_group_perm(member_name,
430 if not check_perms or has_group_perm(member_name,
431 user=cur_user):
431 user=cur_user):
432 _set_perm_group(obj, users_group=member_id, perm=perm)
432 _set_perm_group(obj, users_group=member_id, perm=perm)
433
433
434 changes['updated'].append(
434 changes['updated'].append(
435 {'change_obj': change_obj, 'type': member_type,
435 {'change_obj': change_obj, 'type': member_type,
436 'id': member_id, 'name': member_name, 'new_perm': perm})
436 'id': member_id, 'name': member_name, 'new_perm': perm})
437
437
438 # set new permissions
438 # set new permissions
439 for member_id, perm, member_type in perm_additions:
439 for member_id, perm, member_type in perm_additions:
440 member_id = int(member_id)
440 member_id = int(member_id)
441 if member_type == 'user':
441 if member_type == 'user':
442 member_name = User.get(member_id).username
442 member_name = User.get(member_id).username
443 _set_perm_user(obj, user=member_id, perm=perm)
443 _set_perm_user(obj, user=member_id, perm=perm)
444 else: # set for user group
444 else: # set for user group
445 # check if we have permissions to alter this usergroup
445 # check if we have permissions to alter this usergroup
446 member_name = UserGroup.get(member_id).users_group_name
446 member_name = UserGroup.get(member_id).users_group_name
447 if not check_perms or has_group_perm(member_name,
447 if not check_perms or has_group_perm(member_name,
448 user=cur_user):
448 user=cur_user):
449 _set_perm_group(obj, users_group=member_id, perm=perm)
449 _set_perm_group(obj, users_group=member_id, perm=perm)
450
450
451 changes['added'].append(
451 changes['added'].append(
452 {'change_obj': change_obj, 'type': member_type,
452 {'change_obj': change_obj, 'type': member_type,
453 'id': member_id, 'name': member_name, 'new_perm': perm})
453 'id': member_id, 'name': member_name, 'new_perm': perm})
454
454
455 # delete permissions
455 # delete permissions
456 for member_id, perm, member_type in perm_deletions:
456 for member_id, perm, member_type in perm_deletions:
457 member_id = int(member_id)
457 member_id = int(member_id)
458 if member_type == 'user':
458 if member_type == 'user':
459 member_name = User.get(member_id).username
459 member_name = User.get(member_id).username
460 _revoke_perm_user(obj, user=member_id)
460 _revoke_perm_user(obj, user=member_id)
461 else: # set for user group
461 else: # set for user group
462 # check if we have permissions to alter this usergroup
462 # check if we have permissions to alter this usergroup
463 member_name = UserGroup.get(member_id).users_group_name
463 member_name = UserGroup.get(member_id).users_group_name
464 if not check_perms or has_group_perm(member_name,
464 if not check_perms or has_group_perm(member_name,
465 user=cur_user):
465 user=cur_user):
466 _revoke_perm_group(obj, user_group=member_id)
466 _revoke_perm_group(obj, user_group=member_id)
467
467
468 changes['deleted'].append(
468 changes['deleted'].append(
469 {'change_obj': change_obj, 'type': member_type,
469 {'change_obj': change_obj, 'type': member_type,
470 'id': member_id, 'name': member_name, 'new_perm': perm})
470 'id': member_id, 'name': member_name, 'new_perm': perm})
471
471
472 # if it's not recursive call for all,repos,groups
472 # if it's not recursive call for all,repos,groups
473 # break the loop and don't proceed with other changes
473 # break the loop and don't proceed with other changes
474 if recursive not in ['all', 'repos', 'groups']:
474 if recursive not in ['all', 'repos', 'groups']:
475 break
475 break
476
476
477 return changes
477 return changes
478
478
479 def update(self, repo_group, form_data):
479 def update(self, repo_group, form_data):
480 try:
480 try:
481 repo_group = self._get_repo_group(repo_group)
481 repo_group = self._get_repo_group(repo_group)
482 old_path = repo_group.full_path
482 old_path = repo_group.full_path
483
483
484 # change properties
484 # change properties
485 if 'group_description' in form_data:
485 if 'group_description' in form_data:
486 repo_group.group_description = form_data['group_description']
486 repo_group.group_description = form_data['group_description']
487
487
488 if 'enable_locking' in form_data:
488 if 'enable_locking' in form_data:
489 repo_group.enable_locking = form_data['enable_locking']
489 repo_group.enable_locking = form_data['enable_locking']
490
490
491 if 'group_parent_id' in form_data:
491 if 'group_parent_id' in form_data:
492 parent_group = (
492 parent_group = (
493 self._get_repo_group(form_data['group_parent_id']))
493 self._get_repo_group(form_data['group_parent_id']))
494 repo_group.group_parent_id = (
494 repo_group.group_parent_id = (
495 parent_group.group_id if parent_group else None)
495 parent_group.group_id if parent_group else None)
496 repo_group.parent_group = parent_group
496 repo_group.parent_group = parent_group
497
497
498 # mikhail: to update the full_path, we have to explicitly
498 # mikhail: to update the full_path, we have to explicitly
499 # update group_name
499 # update group_name
500 group_name = form_data.get('group_name', repo_group.name)
500 group_name = form_data.get('group_name', repo_group.name)
501 repo_group.group_name = repo_group.get_new_name(group_name)
501 repo_group.group_name = repo_group.get_new_name(group_name)
502
502
503 new_path = repo_group.full_path
503 new_path = repo_group.full_path
504
504
505 if 'user' in form_data:
505 if 'user' in form_data:
506 repo_group.user = User.get_by_username(form_data['user'])
506 repo_group.user = User.get_by_username(form_data['user'])
507 repo_group.updated_on = datetime.datetime.now()
507 repo_group.updated_on = datetime.datetime.now()
508 self.sa.add(repo_group)
508 self.sa.add(repo_group)
509
509
510 # iterate over all members of this groups and do fixes
510 # iterate over all members of this groups and do fixes
511 # set locking if given
511 # set locking if given
512 # if obj is a repoGroup also fix the name of the group according
512 # if obj is a repoGroup also fix the name of the group according
513 # to the parent
513 # to the parent
514 # if obj is a Repo fix it's name
514 # if obj is a Repo fix it's name
515 # this can be potentially heavy operation
515 # this can be potentially heavy operation
516 for obj in repo_group.recursive_groups_and_repos():
516 for obj in repo_group.recursive_groups_and_repos():
517 # set the value from it's parent
517 # set the value from it's parent
518 obj.enable_locking = repo_group.enable_locking
518 obj.enable_locking = repo_group.enable_locking
519 if isinstance(obj, RepoGroup):
519 if isinstance(obj, RepoGroup):
520 new_name = obj.get_new_name(obj.name)
520 new_name = obj.get_new_name(obj.name)
521 log.debug('Fixing group %s to new name %s',
521 log.debug('Fixing group %s to new name %s',
522 obj.group_name, new_name)
522 obj.group_name, new_name)
523 obj.group_name = new_name
523 obj.group_name = new_name
524 obj.updated_on = datetime.datetime.now()
524 obj.updated_on = datetime.datetime.now()
525 elif isinstance(obj, Repository):
525 elif isinstance(obj, Repository):
526 # we need to get all repositories from this new group and
526 # we need to get all repositories from this new group and
527 # rename them accordingly to new group path
527 # rename them accordingly to new group path
528 new_name = obj.get_new_name(obj.just_name)
528 new_name = obj.get_new_name(obj.just_name)
529 log.debug('Fixing repo %s to new name %s',
529 log.debug('Fixing repo %s to new name %s',
530 obj.repo_name, new_name)
530 obj.repo_name, new_name)
531 obj.repo_name = new_name
531 obj.repo_name = new_name
532 obj.updated_on = datetime.datetime.now()
532 obj.updated_on = datetime.datetime.now()
533 self.sa.add(obj)
533 self.sa.add(obj)
534
534
535 self._rename_group(old_path, new_path)
535 self._rename_group(old_path, new_path)
536
536
537 # Trigger update event.
537 # Trigger update event.
538 events.trigger(events.RepoGroupUpdateEvent(repo_group))
538 events.trigger(events.RepoGroupUpdateEvent(repo_group))
539
539
540 return repo_group
540 return repo_group
541 except Exception:
541 except Exception:
542 log.error(traceback.format_exc())
542 log.error(traceback.format_exc())
543 raise
543 raise
544
544
545 def delete(self, repo_group, force_delete=False, fs_remove=True):
545 def delete(self, repo_group, force_delete=False, fs_remove=True):
546 repo_group = self._get_repo_group(repo_group)
546 repo_group = self._get_repo_group(repo_group)
547 if not repo_group:
547 if not repo_group:
548 return False
548 return False
549 try:
549 try:
550 self.sa.delete(repo_group)
550 self.sa.delete(repo_group)
551 if fs_remove:
551 if fs_remove:
552 self._delete_filesystem_group(repo_group, force_delete)
552 self._delete_filesystem_group(repo_group, force_delete)
553 else:
553 else:
554 log.debug('skipping removal from filesystem')
554 log.debug('skipping removal from filesystem')
555
555
556 # Trigger delete event.
556 # Trigger delete event.
557 events.trigger(events.RepoGroupDeleteEvent(repo_group))
557 events.trigger(events.RepoGroupDeleteEvent(repo_group))
558 return True
558 return True
559
559
560 except Exception:
560 except Exception:
561 log.error('Error removing repo_group %s', repo_group)
561 log.error('Error removing repo_group %s', repo_group)
562 raise
562 raise
563
563
564 def grant_user_permission(self, repo_group, user, perm):
564 def grant_user_permission(self, repo_group, user, perm):
565 """
565 """
566 Grant permission for user on given repository group, or update
566 Grant permission for user on given repository group, or update
567 existing one if found
567 existing one if found
568
568
569 :param repo_group: Instance of RepoGroup, repositories_group_id,
569 :param repo_group: Instance of RepoGroup, repositories_group_id,
570 or repositories_group name
570 or repositories_group name
571 :param user: Instance of User, user_id or username
571 :param user: Instance of User, user_id or username
572 :param perm: Instance of Permission, or permission_name
572 :param perm: Instance of Permission, or permission_name
573 """
573 """
574
574
575 repo_group = self._get_repo_group(repo_group)
575 repo_group = self._get_repo_group(repo_group)
576 user = self._get_user(user)
576 user = self._get_user(user)
577 permission = self._get_perm(perm)
577 permission = self._get_perm(perm)
578
578
579 # check if we have that permission already
579 # check if we have that permission already
580 obj = self.sa.query(UserRepoGroupToPerm)\
580 obj = self.sa.query(UserRepoGroupToPerm)\
581 .filter(UserRepoGroupToPerm.user == user)\
581 .filter(UserRepoGroupToPerm.user == user)\
582 .filter(UserRepoGroupToPerm.group == repo_group)\
582 .filter(UserRepoGroupToPerm.group == repo_group)\
583 .scalar()
583 .scalar()
584 if obj is None:
584 if obj is None:
585 # create new !
585 # create new !
586 obj = UserRepoGroupToPerm()
586 obj = UserRepoGroupToPerm()
587 obj.group = repo_group
587 obj.group = repo_group
588 obj.user = user
588 obj.user = user
589 obj.permission = permission
589 obj.permission = permission
590 self.sa.add(obj)
590 self.sa.add(obj)
591 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
591 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
592 action_logger_generic(
592 action_logger_generic(
593 'granted permission: {} to user: {} on repogroup: {}'.format(
593 'granted permission: {} to user: {} on repogroup: {}'.format(
594 perm, user, repo_group), namespace='security.repogroup')
594 perm, user, repo_group), namespace='security.repogroup')
595 return obj
595 return obj
596
596
597 def revoke_user_permission(self, repo_group, user):
597 def revoke_user_permission(self, repo_group, user):
598 """
598 """
599 Revoke permission for user on given repository group
599 Revoke permission for user on given repository group
600
600
601 :param repo_group: Instance of RepoGroup, repositories_group_id,
601 :param repo_group: Instance of RepoGroup, repositories_group_id,
602 or repositories_group name
602 or repositories_group name
603 :param user: Instance of User, user_id or username
603 :param user: Instance of User, user_id or username
604 """
604 """
605
605
606 repo_group = self._get_repo_group(repo_group)
606 repo_group = self._get_repo_group(repo_group)
607 user = self._get_user(user)
607 user = self._get_user(user)
608
608
609 obj = self.sa.query(UserRepoGroupToPerm)\
609 obj = self.sa.query(UserRepoGroupToPerm)\
610 .filter(UserRepoGroupToPerm.user == user)\
610 .filter(UserRepoGroupToPerm.user == user)\
611 .filter(UserRepoGroupToPerm.group == repo_group)\
611 .filter(UserRepoGroupToPerm.group == repo_group)\
612 .scalar()
612 .scalar()
613 if obj:
613 if obj:
614 self.sa.delete(obj)
614 self.sa.delete(obj)
615 log.debug('Revoked perm on %s on %s', repo_group, user)
615 log.debug('Revoked perm on %s on %s', repo_group, user)
616 action_logger_generic(
616 action_logger_generic(
617 'revoked permission from user: {} on repogroup: {}'.format(
617 'revoked permission from user: {} on repogroup: {}'.format(
618 user, repo_group), namespace='security.repogroup')
618 user, repo_group), namespace='security.repogroup')
619
619
620 def grant_user_group_permission(self, repo_group, group_name, perm):
620 def grant_user_group_permission(self, repo_group, group_name, perm):
621 """
621 """
622 Grant permission for user group on given repository group, or update
622 Grant permission for user group on given repository group, or update
623 existing one if found
623 existing one if found
624
624
625 :param repo_group: Instance of RepoGroup, repositories_group_id,
625 :param repo_group: Instance of RepoGroup, repositories_group_id,
626 or repositories_group name
626 or repositories_group name
627 :param group_name: Instance of UserGroup, users_group_id,
627 :param group_name: Instance of UserGroup, users_group_id,
628 or user group name
628 or user group name
629 :param perm: Instance of Permission, or permission_name
629 :param perm: Instance of Permission, or permission_name
630 """
630 """
631 repo_group = self._get_repo_group(repo_group)
631 repo_group = self._get_repo_group(repo_group)
632 group_name = self._get_user_group(group_name)
632 group_name = self._get_user_group(group_name)
633 permission = self._get_perm(perm)
633 permission = self._get_perm(perm)
634
634
635 # check if we have that permission already
635 # check if we have that permission already
636 obj = self.sa.query(UserGroupRepoGroupToPerm)\
636 obj = self.sa.query(UserGroupRepoGroupToPerm)\
637 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
637 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
638 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
638 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
639 .scalar()
639 .scalar()
640
640
641 if obj is None:
641 if obj is None:
642 # create new
642 # create new
643 obj = UserGroupRepoGroupToPerm()
643 obj = UserGroupRepoGroupToPerm()
644
644
645 obj.group = repo_group
645 obj.group = repo_group
646 obj.users_group = group_name
646 obj.users_group = group_name
647 obj.permission = permission
647 obj.permission = permission
648 self.sa.add(obj)
648 self.sa.add(obj)
649 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
649 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
650 action_logger_generic(
650 action_logger_generic(
651 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
651 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
652 perm, group_name, repo_group), namespace='security.repogroup')
652 perm, group_name, repo_group), namespace='security.repogroup')
653 return obj
653 return obj
654
654
655 def revoke_user_group_permission(self, repo_group, group_name):
655 def revoke_user_group_permission(self, repo_group, group_name):
656 """
656 """
657 Revoke permission for user group on given repository group
657 Revoke permission for user group on given repository group
658
658
659 :param repo_group: Instance of RepoGroup, repositories_group_id,
659 :param repo_group: Instance of RepoGroup, repositories_group_id,
660 or repositories_group name
660 or repositories_group name
661 :param group_name: Instance of UserGroup, users_group_id,
661 :param group_name: Instance of UserGroup, users_group_id,
662 or user group name
662 or user group name
663 """
663 """
664 repo_group = self._get_repo_group(repo_group)
664 repo_group = self._get_repo_group(repo_group)
665 group_name = self._get_user_group(group_name)
665 group_name = self._get_user_group(group_name)
666
666
667 obj = self.sa.query(UserGroupRepoGroupToPerm)\
667 obj = self.sa.query(UserGroupRepoGroupToPerm)\
668 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
668 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
669 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
669 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
670 .scalar()
670 .scalar()
671 if obj:
671 if obj:
672 self.sa.delete(obj)
672 self.sa.delete(obj)
673 log.debug('Revoked perm to %s on %s', repo_group, group_name)
673 log.debug('Revoked perm to %s on %s', repo_group, group_name)
674 action_logger_generic(
674 action_logger_generic(
675 'revoked permission from usergroup: {} on repogroup: {}'.format(
675 'revoked permission from usergroup: {} on repogroup: {}'.format(
676 group_name, repo_group), namespace='security.repogroup')
676 group_name, repo_group), namespace='security.repogroup')
677
677
678 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
678 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
679 super_user_actions=False):
679 super_user_actions=False):
680
680
681 from pyramid.threadlocal import get_current_request
681 from pyramid.threadlocal import get_current_request
682 _render = get_current_request().get_partial_renderer(
682 _render = get_current_request().get_partial_renderer(
683 'data_table/_dt_elements.mako')
683 'rhodecode:templates/data_table/_dt_elements.mako')
684 c = _render.get_call_context()
684 c = _render.get_call_context()
685 h = _render.get_helpers()
685 h = _render.get_helpers()
686
686
687 def quick_menu(repo_group_name):
687 def quick_menu(repo_group_name):
688 return _render('quick_repo_group_menu', repo_group_name)
688 return _render('quick_repo_group_menu', repo_group_name)
689
689
690 def repo_group_lnk(repo_group_name):
690 def repo_group_lnk(repo_group_name):
691 return _render('repo_group_name', repo_group_name)
691 return _render('repo_group_name', repo_group_name)
692
692
693 def last_change(last_change):
693 def last_change(last_change):
694 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
694 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
695 last_change = last_change + datetime.timedelta(seconds=
695 last_change = last_change + datetime.timedelta(seconds=
696 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
696 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
697 return _render("last_change", last_change)
697 return _render("last_change", last_change)
698
698
699 def desc(desc, personal):
699 def desc(desc, personal):
700 return _render(
700 return _render(
701 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
701 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
702
702
703 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
703 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
704 return _render(
704 return _render(
705 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
705 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
706
706
707 def repo_group_name(repo_group_name, children_groups):
707 def repo_group_name(repo_group_name, children_groups):
708 return _render("repo_group_name", repo_group_name, children_groups)
708 return _render("repo_group_name", repo_group_name, children_groups)
709
709
710 def user_profile(username):
710 def user_profile(username):
711 return _render('user_profile', username)
711 return _render('user_profile', username)
712
712
713 repo_group_data = []
713 repo_group_data = []
714 for group in repo_group_list:
714 for group in repo_group_list:
715
715
716 row = {
716 row = {
717 "menu": quick_menu(group.group_name),
717 "menu": quick_menu(group.group_name),
718 "name": repo_group_lnk(group.group_name),
718 "name": repo_group_lnk(group.group_name),
719 "name_raw": group.group_name,
719 "name_raw": group.group_name,
720 "last_change": last_change(group.last_db_change),
720 "last_change": last_change(group.last_db_change),
721 "last_change_raw": datetime_to_time(group.last_db_change),
721 "last_change_raw": datetime_to_time(group.last_db_change),
722 "desc": desc(group.description_safe, group.personal),
722 "desc": desc(group.description_safe, group.personal),
723 "top_level_repos": 0,
723 "top_level_repos": 0,
724 "owner": user_profile(group.user.username)
724 "owner": user_profile(group.user.username)
725 }
725 }
726 if admin:
726 if admin:
727 repo_count = group.repositories.count()
727 repo_count = group.repositories.count()
728 children_groups = map(
728 children_groups = map(
729 h.safe_unicode,
729 h.safe_unicode,
730 itertools.chain((g.name for g in group.parents),
730 itertools.chain((g.name for g in group.parents),
731 (x.name for x in [group])))
731 (x.name for x in [group])))
732 row.update({
732 row.update({
733 "action": repo_group_actions(
733 "action": repo_group_actions(
734 group.group_id, group.group_name, repo_count),
734 group.group_id, group.group_name, repo_count),
735 "top_level_repos": repo_count,
735 "top_level_repos": repo_count,
736 "name": repo_group_name(group.group_name, children_groups),
736 "name": repo_group_name(group.group_name, children_groups),
737
737
738 })
738 })
739 repo_group_data.append(row)
739 repo_group_data.append(row)
740
740
741 return repo_group_data
741 return repo_group_data
742
742
743 def _get_defaults(self, repo_group_name):
743 def _get_defaults(self, repo_group_name):
744 repo_group = RepoGroup.get_by_group_name(repo_group_name)
744 repo_group = RepoGroup.get_by_group_name(repo_group_name)
745
745
746 if repo_group is None:
746 if repo_group is None:
747 return None
747 return None
748
748
749 defaults = repo_group.get_dict()
749 defaults = repo_group.get_dict()
750 defaults['repo_group_name'] = repo_group.name
750 defaults['repo_group_name'] = repo_group.name
751 defaults['repo_group_description'] = repo_group.group_description
751 defaults['repo_group_description'] = repo_group.group_description
752 defaults['repo_group_enable_locking'] = repo_group.enable_locking
752 defaults['repo_group_enable_locking'] = repo_group.enable_locking
753
753
754 # we use -1 as this is how in HTML, we mark an empty group
754 # we use -1 as this is how in HTML, we mark an empty group
755 defaults['repo_group'] = defaults['group_parent_id'] or -1
755 defaults['repo_group'] = defaults['group_parent_id'] or -1
756
756
757 # fill owner
757 # fill owner
758 if repo_group.user:
758 if repo_group.user:
759 defaults.update({'user': repo_group.user.username})
759 defaults.update({'user': repo_group.user.username})
760 else:
760 else:
761 replacement_user = User.get_first_super_admin().username
761 replacement_user = User.get_first_super_admin().username
762 defaults.update({'user': replacement_user})
762 defaults.update({'user': replacement_user})
763
763
764 return defaults
764 return defaults
General Comments 0
You need to be logged in to leave comments. Login now