Show More
@@ -0,0 +1,27 b'' | |||||
|
1 | import logging | |||
|
2 | ||||
|
3 | from sqlalchemy import Column, MetaData, Boolean | |||
|
4 | ||||
|
5 | from rhodecode.lib.dbmigrate.versions import _reset_base | |||
|
6 | ||||
|
7 | log = logging.getLogger(__name__) | |||
|
8 | ||||
|
9 | ||||
|
10 | def upgrade(migrate_engine): | |||
|
11 | """ | |||
|
12 | Upgrade operations go here. | |||
|
13 | Don't create your own engine; bind migrate_engine to your metadata | |||
|
14 | """ | |||
|
15 | _reset_base(migrate_engine) | |||
|
16 | from rhodecode.lib.dbmigrate.schema import db_4_5_0_0 as db | |||
|
17 | ||||
|
18 | # Add personal column to RepoGroup table. | |||
|
19 | rg_table = db.RepoGroup.__table__ | |||
|
20 | rg_col = Column( | |||
|
21 | 'personal', Boolean(), nullable=True, unique=None, default=None) | |||
|
22 | rg_col.create(table=rg_table) | |||
|
23 | ||||
|
24 | ||||
|
25 | def downgrade(migrate_engine): | |||
|
26 | meta = MetaData() | |||
|
27 | meta.bind = migrate_engine |
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}' | |||||
51 | EXTENSIONS = {} |
|
51 | EXTENSIONS = {} | |
52 |
|
52 | |||
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
53 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) | |
54 |
__dbversion__ = 6 |
|
54 | __dbversion__ = 63 # defines current db version for migrations | |
55 | __platform__ = platform.system() |
|
55 | __platform__ = platform.system() | |
56 | __license__ = 'AGPLv3, and Commercial License' |
|
56 | __license__ = 'AGPLv3, and Commercial License' | |
57 | __author__ = 'RhodeCode GmbH' |
|
57 | __author__ = 'RhodeCode GmbH' |
@@ -25,7 +25,7 b' from rhodecode.api.utils import (' | |||||
25 | Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) |
|
25 | Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) | |
26 | from rhodecode.lib.auth import AuthUser, PasswordGenerator |
|
26 | from rhodecode.lib.auth import AuthUser, PasswordGenerator | |
27 | from rhodecode.lib.exceptions import DefaultUserException |
|
27 | from rhodecode.lib.exceptions import DefaultUserException | |
28 | from rhodecode.lib.utils2 import safe_int |
|
28 | from rhodecode.lib.utils2 import safe_int, str2bool | |
29 | from rhodecode.model.db import Session, User, Repository |
|
29 | from rhodecode.model.db import Session, User, Repository | |
30 | from rhodecode.model.user import UserModel |
|
30 | from rhodecode.model.user import UserModel | |
31 |
|
31 | |||
@@ -155,7 +155,8 b' def create_user(request, apiuser, userna' | |||||
155 | active=Optional(True), admin=Optional(False), |
|
155 | active=Optional(True), admin=Optional(False), | |
156 | extern_name=Optional('rhodecode'), |
|
156 | extern_name=Optional('rhodecode'), | |
157 | extern_type=Optional('rhodecode'), |
|
157 | extern_type=Optional('rhodecode'), | |
158 |
force_password_change=Optional(False) |
|
158 | force_password_change=Optional(False), | |
|
159 | create_personal_repo_group=Optional(None)): | |||
159 | """ |
|
160 | """ | |
160 | Creates a new user and returns the new user object. |
|
161 | Creates a new user and returns the new user object. | |
161 |
|
162 | |||
@@ -188,7 +189,8 b' def create_user(request, apiuser, userna' | |||||
188 | :param force_password_change: Force the new user to change password |
|
189 | :param force_password_change: Force the new user to change password | |
189 | on next login. |
|
190 | on next login. | |
190 | :type force_password_change: Optional(``True`` | ``False``) |
|
191 | :type force_password_change: Optional(``True`` | ``False``) | |
191 |
|
192 | :param create_personal_repo_group: Create personal repo group for this user | ||
|
193 | :type create_personal_repo_group: Optional(``True`` | ``False``) | |||
192 | Example output: |
|
194 | Example output: | |
193 |
|
195 | |||
194 | .. code-block:: bash |
|
196 | .. code-block:: bash | |
@@ -230,6 +232,9 b' def create_user(request, apiuser, userna' | |||||
230 | Optional.extract(extern_name) != 'rhodecode'): |
|
232 | Optional.extract(extern_name) != 'rhodecode'): | |
231 | # generate temporary password if user is external |
|
233 | # generate temporary password if user is external | |
232 | password = PasswordGenerator().gen_password(length=16) |
|
234 | password = PasswordGenerator().gen_password(length=16) | |
|
235 | create_repo_group = Optional.extract(create_personal_repo_group) | |||
|
236 | if isinstance(create_repo_group, basestring): | |||
|
237 | create_repo_group = str2bool(create_repo_group) | |||
233 |
|
238 | |||
234 | try: |
|
239 | try: | |
235 | user = UserModel().create_or_update( |
|
240 | user = UserModel().create_or_update( | |
@@ -243,6 +248,7 b' def create_user(request, apiuser, userna' | |||||
243 | extern_type=Optional.extract(extern_type), |
|
248 | extern_type=Optional.extract(extern_type), | |
244 | extern_name=Optional.extract(extern_name), |
|
249 | extern_name=Optional.extract(extern_name), | |
245 | force_password_change=Optional.extract(force_password_change), |
|
250 | force_password_change=Optional.extract(force_password_change), | |
|
251 | create_repo_group=create_repo_group | |||
246 | ) |
|
252 | ) | |
247 | Session().commit() |
|
253 | Session().commit() | |
248 | return { |
|
254 | return { |
@@ -160,6 +160,7 b' class ReposController(BaseRepoController' | |||||
160 | self.__load_defaults() |
|
160 | self.__load_defaults() | |
161 | form_result = {} |
|
161 | form_result = {} | |
162 | task_id = None |
|
162 | task_id = None | |
|
163 | c.personal_repo_group = c.rhodecode_user.personal_repo_group | |||
163 | try: |
|
164 | try: | |
164 | # CanWriteToGroup validators checks permissions of this POST |
|
165 | # CanWriteToGroup validators checks permissions of this POST | |
165 | form_result = RepoForm(repo_groups=c.repo_groups_choices, |
|
166 | form_result = RepoForm(repo_groups=c.repo_groups_choices, | |
@@ -173,8 +174,6 b' class ReposController(BaseRepoController' | |||||
173 | if isinstance(task, BaseAsyncResult): |
|
174 | if isinstance(task, BaseAsyncResult): | |
174 | task_id = task.task_id |
|
175 | task_id = task.task_id | |
175 | except formencode.Invalid as errors: |
|
176 | except formencode.Invalid as errors: | |
176 | c.personal_repo_group = RepoGroup.get_by_group_name( |
|
|||
177 | c.rhodecode_user.username) |
|
|||
178 | return htmlfill.render( |
|
177 | return htmlfill.render( | |
179 | render('admin/repos/repo_add.html'), |
|
178 | render('admin/repos/repo_add.html'), | |
180 | defaults=errors.value, |
|
179 | defaults=errors.value, | |
@@ -215,7 +214,7 b' class ReposController(BaseRepoController' | |||||
215 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) |
|
214 | c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) | |
216 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) |
|
215 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) | |
217 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
216 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() | |
218 |
c.personal_repo_group = |
|
217 | c.personal_repo_group = c.rhodecode_user.personal_repo_group | |
219 | c.new_repo = repo_name_slug(new_repo) |
|
218 | c.new_repo = repo_name_slug(new_repo) | |
220 |
|
219 | |||
221 | ## apply the defaults from defaults page |
|
220 | ## apply the defaults from defaults page | |
@@ -299,9 +298,8 b' class ReposController(BaseRepoController' | |||||
299 | repo_model = RepoModel() |
|
298 | repo_model = RepoModel() | |
300 | changed_name = repo_name |
|
299 | changed_name = repo_name | |
301 |
|
300 | |||
|
301 | c.personal_repo_group = c.rhodecode_user.personal_repo_group | |||
302 | # override the choices with extracted revisions ! |
|
302 | # override the choices with extracted revisions ! | |
303 | c.personal_repo_group = RepoGroup.get_by_group_name( |
|
|||
304 | c.rhodecode_user.username) |
|
|||
305 | repo = Repository.get_by_repo_name(repo_name) |
|
303 | repo = Repository.get_by_repo_name(repo_name) | |
306 | old_data = { |
|
304 | old_data = { | |
307 | 'repo_name': repo_name, |
|
305 | 'repo_name': repo_name, | |
@@ -399,8 +397,7 b' class ReposController(BaseRepoController' | |||||
399 |
|
397 | |||
400 | c.repo_fields = RepositoryField.query()\ |
|
398 | c.repo_fields = RepositoryField.query()\ | |
401 | .filter(RepositoryField.repository == c.repo_info).all() |
|
399 | .filter(RepositoryField.repository == c.repo_info).all() | |
402 |
c.personal_repo_group = |
|
400 | c.personal_repo_group = c.rhodecode_user.personal_repo_group | |
403 | c.rhodecode_user.username) |
|
|||
404 | c.active = 'settings' |
|
401 | c.active = 'settings' | |
405 | return htmlfill.render( |
|
402 | return htmlfill.render( | |
406 | render('admin/repos/repo_edit.html'), |
|
403 | render('admin/repos/repo_edit.html'), |
@@ -55,6 +55,7 b' from rhodecode.model.db import RhodeCode' | |||||
55 | from rhodecode.model.forms import ApplicationSettingsForm, \ |
|
55 | from rhodecode.model.forms import ApplicationSettingsForm, \ | |
56 | ApplicationUiSettingsForm, ApplicationVisualisationForm, \ |
|
56 | ApplicationUiSettingsForm, ApplicationVisualisationForm, \ | |
57 | LabsSettingsForm, IssueTrackerPatternsForm |
|
57 | LabsSettingsForm, IssueTrackerPatternsForm | |
|
58 | from rhodecode.model.repo_group import RepoGroupModel | |||
58 |
|
59 | |||
59 | from rhodecode.model.scm import ScmModel |
|
60 | from rhodecode.model.scm import ScmModel | |
60 | from rhodecode.model.notification import EmailNotificationModel |
|
61 | from rhodecode.model.notification import EmailNotificationModel | |
@@ -245,6 +246,8 b' class SettingsController(BaseController)' | |||||
245 | """POST /admin/settings/global: All items in the collection""" |
|
246 | """POST /admin/settings/global: All items in the collection""" | |
246 | # url('admin_settings_global') |
|
247 | # url('admin_settings_global') | |
247 | c.active = 'global' |
|
248 | c.active = 'global' | |
|
249 | c.personal_repo_group_default_pattern = RepoGroupModel()\ | |||
|
250 | .get_personal_group_name_pattern() | |||
248 | application_form = ApplicationSettingsForm()() |
|
251 | application_form = ApplicationSettingsForm()() | |
249 | try: |
|
252 | try: | |
250 | form_result = application_form.to_python(dict(request.POST)) |
|
253 | form_result = application_form.to_python(dict(request.POST)) | |
@@ -259,16 +262,18 b' class SettingsController(BaseController)' | |||||
259 |
|
262 | |||
260 | try: |
|
263 | try: | |
261 | settings = [ |
|
264 | settings = [ | |
262 | ('title', 'rhodecode_title'), |
|
265 | ('title', 'rhodecode_title', 'unicode'), | |
263 | ('realm', 'rhodecode_realm'), |
|
266 | ('realm', 'rhodecode_realm', 'unicode'), | |
264 | ('pre_code', 'rhodecode_pre_code'), |
|
267 | ('pre_code', 'rhodecode_pre_code', 'unicode'), | |
265 | ('post_code', 'rhodecode_post_code'), |
|
268 | ('post_code', 'rhodecode_post_code', 'unicode'), | |
266 | ('captcha_public_key', 'rhodecode_captcha_public_key'), |
|
269 | ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'), | |
267 | ('captcha_private_key', 'rhodecode_captcha_private_key'), |
|
270 | ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'), | |
|
271 | ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'), | |||
|
272 | ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'), | |||
268 | ] |
|
273 | ] | |
269 | for setting, form_key in settings: |
|
274 | for setting, form_key, type_ in settings: | |
270 | sett = SettingsModel().create_or_update_setting( |
|
275 | sett = SettingsModel().create_or_update_setting( | |
271 | setting, form_result[form_key]) |
|
276 | setting, form_result[form_key], type_) | |
272 | Session().add(sett) |
|
277 | Session().add(sett) | |
273 |
|
278 | |||
274 | Session().commit() |
|
279 | Session().commit() | |
@@ -287,6 +292,8 b' class SettingsController(BaseController)' | |||||
287 | """GET /admin/settings/global: All items in the collection""" |
|
292 | """GET /admin/settings/global: All items in the collection""" | |
288 | # url('admin_settings_global') |
|
293 | # url('admin_settings_global') | |
289 | c.active = 'global' |
|
294 | c.active = 'global' | |
|
295 | c.personal_repo_group_default_pattern = RepoGroupModel()\ | |||
|
296 | .get_personal_group_name_pattern() | |||
290 |
|
297 | |||
291 | return htmlfill.render( |
|
298 | return htmlfill.render( | |
292 | render('admin/settings/settings.html'), |
|
299 | render('admin/settings/settings.html'), |
@@ -45,12 +45,13 b' from rhodecode.model.db import (' | |||||
45 | PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) |
|
45 | PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) | |
46 | from rhodecode.model.forms import ( |
|
46 | from rhodecode.model.forms import ( | |
47 | UserForm, UserPermissionsForm, UserIndividualPermissionsForm) |
|
47 | UserForm, UserPermissionsForm, UserIndividualPermissionsForm) | |
|
48 | from rhodecode.model.repo_group import RepoGroupModel | |||
48 | from rhodecode.model.user import UserModel |
|
49 | from rhodecode.model.user import UserModel | |
49 | from rhodecode.model.meta import Session |
|
50 | from rhodecode.model.meta import Session | |
50 | from rhodecode.model.permission import PermissionModel |
|
51 | from rhodecode.model.permission import PermissionModel | |
51 | from rhodecode.lib.utils import action_logger |
|
52 | from rhodecode.lib.utils import action_logger | |
52 | from rhodecode.lib.ext_json import json |
|
53 | from rhodecode.lib.ext_json import json | |
53 | from rhodecode.lib.utils2 import datetime_to_time, safe_int |
|
54 | from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict | |
54 |
|
55 | |||
55 | log = logging.getLogger(__name__) |
|
56 | log = logging.getLogger(__name__) | |
56 |
|
57 | |||
@@ -120,6 +121,16 b' class UsersController(BaseController):' | |||||
120 | c.data = json.dumps(users_data) |
|
121 | c.data = json.dumps(users_data) | |
121 | return render('admin/users/users.html') |
|
122 | return render('admin/users/users.html') | |
122 |
|
123 | |||
|
124 | def _get_personal_repo_group_template_vars(self): | |||
|
125 | DummyUser = AttributeDict({ | |||
|
126 | 'username': '${username}', | |||
|
127 | 'user_id': '${user_id}', | |||
|
128 | }) | |||
|
129 | c.default_create_repo_group = RepoGroupModel() \ | |||
|
130 | .get_default_create_personal_repo_group() | |||
|
131 | c.personal_repo_group_name = RepoGroupModel() \ | |||
|
132 | .get_personal_group_name(DummyUser) | |||
|
133 | ||||
123 | @HasPermissionAllDecorator('hg.admin') |
|
134 | @HasPermissionAllDecorator('hg.admin') | |
124 | @auth.CSRFRequired() |
|
135 | @auth.CSRFRequired() | |
125 | def create(self): |
|
136 | def create(self): | |
@@ -143,6 +154,7 b' class UsersController(BaseController):' | |||||
143 | % {'user_link': user_link}), category='success') |
|
154 | % {'user_link': user_link}), category='success') | |
144 | Session().commit() |
|
155 | Session().commit() | |
145 | except formencode.Invalid as errors: |
|
156 | except formencode.Invalid as errors: | |
|
157 | self._get_personal_repo_group_template_vars() | |||
146 | return htmlfill.render( |
|
158 | return htmlfill.render( | |
147 | render('admin/users/user_add.html'), |
|
159 | render('admin/users/user_add.html'), | |
148 | defaults=errors.value, |
|
160 | defaults=errors.value, | |
@@ -163,6 +175,7 b' class UsersController(BaseController):' | |||||
163 | """GET /users/new: Form to create a new item""" |
|
175 | """GET /users/new: Form to create a new item""" | |
164 | # url('new_user') |
|
176 | # url('new_user') | |
165 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name |
|
177 | c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name | |
|
178 | self._get_personal_repo_group_template_vars() | |||
166 | return render('admin/users/user_add.html') |
|
179 | return render('admin/users/user_add.html') | |
167 |
|
180 | |||
168 | @HasPermissionAllDecorator('hg.admin') |
|
181 | @HasPermissionAllDecorator('hg.admin') | |
@@ -339,22 +352,41 b' class UsersController(BaseController):' | |||||
339 |
|
352 | |||
340 | user_id = safe_int(user_id) |
|
353 | user_id = safe_int(user_id) | |
341 | c.user = User.get_or_404(user_id) |
|
354 | c.user = User.get_or_404(user_id) | |
|
355 | personal_repo_group = RepoGroup.get_user_personal_repo_group( | |||
|
356 | c.user.user_id) | |||
|
357 | if personal_repo_group: | |||
|
358 | return redirect(url('edit_user_advanced', user_id=user_id)) | |||
342 |
|
359 | |||
|
360 | personal_repo_group_name = RepoGroupModel().get_personal_group_name( | |||
|
361 | c.user) | |||
|
362 | named_personal_group = RepoGroup.get_by_group_name( | |||
|
363 | personal_repo_group_name) | |||
343 | try: |
|
364 | try: | |
344 | desc = RepoGroupModel.PERSONAL_GROUP_DESC % { |
|
|||
345 | 'username': c.user.username} |
|
|||
346 | if not RepoGroup.get_by_group_name(c.user.username): |
|
|||
347 | RepoGroupModel().create(group_name=c.user.username, |
|
|||
348 | group_description=desc, |
|
|||
349 | owner=c.user.username) |
|
|||
350 |
|
365 | |||
351 | msg = _('Created repository group `%s`' % (c.user.username,)) |
|
366 | if named_personal_group and named_personal_group.user_id == c.user.user_id: | |
|
367 | # migrate the same named group, and mark it as personal | |||
|
368 | named_personal_group.personal = True | |||
|
369 | Session().add(named_personal_group) | |||
|
370 | Session().commit() | |||
|
371 | msg = _('Linked repository group `%s` as personal' % ( | |||
|
372 | personal_repo_group_name,)) | |||
352 | h.flash(msg, category='success') |
|
373 | h.flash(msg, category='success') | |
|
374 | elif not named_personal_group: | |||
|
375 | RepoGroupModel().create_personal_repo_group(c.user) | |||
|
376 | ||||
|
377 | msg = _('Created repository group `%s`' % ( | |||
|
378 | personal_repo_group_name,)) | |||
|
379 | h.flash(msg, category='success') | |||
|
380 | else: | |||
|
381 | msg = _('Repository group `%s` is already taken' % ( | |||
|
382 | personal_repo_group_name,)) | |||
|
383 | h.flash(msg, category='warning') | |||
353 | except Exception: |
|
384 | except Exception: | |
354 | log.exception("Exception during repository group creation") |
|
385 | log.exception("Exception during repository group creation") | |
355 | msg = _( |
|
386 | msg = _( | |
356 | 'An error occurred during repository group creation for user') |
|
387 | 'An error occurred during repository group creation for user') | |
357 | h.flash(msg, category='error') |
|
388 | h.flash(msg, category='error') | |
|
389 | Session().rollback() | |||
358 |
|
390 | |||
359 | return redirect(url('edit_user_advanced', user_id=user_id)) |
|
391 | return redirect(url('edit_user_advanced', user_id=user_id)) | |
360 |
|
392 | |||
@@ -397,7 +429,9 b' class UsersController(BaseController):' | |||||
397 |
|
429 | |||
398 | c.active = 'advanced' |
|
430 | c.active = 'advanced' | |
399 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) |
|
431 | c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) | |
400 | c.personal_repo_group = RepoGroup.get_by_group_name(user.username) |
|
432 | c.personal_repo_group = c.perm_user.personal_repo_group | |
|
433 | c.personal_repo_group_name = RepoGroupModel()\ | |||
|
434 | .get_personal_group_name(user) | |||
401 | c.first_admin = User.get_first_super_admin() |
|
435 | c.first_admin = User.get_first_super_admin() | |
402 | defaults = user.get_dict() |
|
436 | defaults = user.get_dict() | |
403 |
|
437 |
@@ -60,8 +60,7 b' class ForksController(BaseRepoController' | |||||
60 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) |
|
60 | c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups) | |
61 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() |
|
61 | choices, c.landing_revs = ScmModel().get_repo_landing_revs() | |
62 | c.landing_revs_choices = choices |
|
62 | c.landing_revs_choices = choices | |
63 |
c.personal_repo_group = |
|
63 | c.personal_repo_group = c.rhodecode_user.personal_repo_group | |
64 | c.rhodecode_user.username) |
|
|||
65 |
|
64 | |||
66 | def __load_data(self, repo_name=None): |
|
65 | def __load_data(self, repo_name=None): | |
67 | """ |
|
66 | """ |
@@ -48,6 +48,7 b' from rhodecode.events.base import Rhodec' | |||||
48 |
|
48 | |||
49 | from rhodecode.events.user import ( # noqa |
|
49 | from rhodecode.events.user import ( # noqa | |
50 | UserPreCreate, |
|
50 | UserPreCreate, | |
|
51 | UserPostCreate, | |||
51 | UserPreUpdate, |
|
52 | UserPreUpdate, | |
52 | UserRegistered |
|
53 | UserRegistered | |
53 | ) |
|
54 | ) |
@@ -51,6 +51,19 b' class UserPreCreate(RhodecodeEvent):' | |||||
51 | self.user_data = user_data |
|
51 | self.user_data = user_data | |
52 |
|
52 | |||
53 |
|
53 | |||
|
54 | @implementer(IUserPreCreate) | |||
|
55 | class UserPostCreate(RhodecodeEvent): | |||
|
56 | """ | |||
|
57 | An instance of this class is emitted as an :term:`event` after a new user | |||
|
58 | object is created. | |||
|
59 | """ | |||
|
60 | name = 'user-post-create' | |||
|
61 | display_name = lazy_ugettext('user post create') | |||
|
62 | ||||
|
63 | def __init__(self, user_data): | |||
|
64 | self.user_data = user_data | |||
|
65 | ||||
|
66 | ||||
54 | @implementer(IUserPreUpdate) |
|
67 | @implementer(IUserPreUpdate) | |
55 | class UserPreUpdate(RhodecodeEvent): |
|
68 | class UserPreUpdate(RhodecodeEvent): | |
56 | """ |
|
69 | """ |
@@ -49,7 +49,7 b' from rhodecode.model.meta import Session' | |||||
49 | from rhodecode.model.user import UserModel |
|
49 | from rhodecode.model.user import UserModel | |
50 | from rhodecode.model.db import ( |
|
50 | from rhodecode.model.db import ( | |
51 | User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember, |
|
51 | User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember, | |
52 | UserIpMap, UserApiKeys) |
|
52 | UserIpMap, UserApiKeys, RepoGroup) | |
53 | from rhodecode.lib import caches |
|
53 | from rhodecode.lib import caches | |
54 | from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5 |
|
54 | from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5 | |
55 | from rhodecode.lib.utils import ( |
|
55 | from rhodecode.lib.utils import ( | |
@@ -983,6 +983,9 b' class AuthUser(object):' | |||||
983 | inherit = self.inherit_default_permissions |
|
983 | inherit = self.inherit_default_permissions | |
984 | return AuthUser.check_ip_allowed(self.user_id, self.ip_addr, |
|
984 | return AuthUser.check_ip_allowed(self.user_id, self.ip_addr, | |
985 | inherit_from_default=inherit) |
|
985 | inherit_from_default=inherit) | |
|
986 | @property | |||
|
987 | def personal_repo_group(self): | |||
|
988 | return RepoGroup.get_user_personal_repo_group(self.user_id) | |||
986 |
|
989 | |||
987 | @classmethod |
|
990 | @classmethod | |
988 | def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default): |
|
991 | def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default): |
@@ -96,7 +96,7 b' def __get_lem(extra_mapping=None):' | |||||
96 |
|
96 | |||
97 | def str2bool(_str): |
|
97 | def str2bool(_str): | |
98 | """ |
|
98 | """ | |
99 | returs True/False value from given string, it tries to translate the |
|
99 | returns True/False value from given string, it tries to translate the | |
100 | string into boolean |
|
100 | string into boolean | |
101 |
|
101 | |||
102 | :param _str: string value to translate into boolean |
|
102 | :param _str: string value to translate into boolean |
@@ -2028,6 +2028,7 b' class RepoGroup(Base, BaseModel):' | |||||
2028 | enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) |
|
2028 | enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) | |
2029 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) |
|
2029 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) | |
2030 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
2030 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
2031 | personal = Column('personal', Boolean(), nullable=True, unique=None, default=None) | |||
2031 |
|
2032 | |||
2032 | repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') |
|
2033 | repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') | |
2033 | users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') |
|
2034 | users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') | |
@@ -2083,6 +2084,13 b' class RepoGroup(Base, BaseModel):' | |||||
2083 | return gr.scalar() |
|
2084 | return gr.scalar() | |
2084 |
|
2085 | |||
2085 | @classmethod |
|
2086 | @classmethod | |
|
2087 | def get_user_personal_repo_group(cls, user_id): | |||
|
2088 | user = User.get(user_id) | |||
|
2089 | return cls.query()\ | |||
|
2090 | .filter(cls.personal == true())\ | |||
|
2091 | .filter(cls.user == user).scalar() | |||
|
2092 | ||||
|
2093 | @classmethod | |||
2086 | def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None), |
|
2094 | def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None), | |
2087 | case_insensitive=True): |
|
2095 | case_insensitive=True): | |
2088 | q = RepoGroup.query() |
|
2096 | q = RepoGroup.query() |
@@ -341,6 +341,8 b' def ApplicationSettingsForm():' | |||||
341 | rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False) |
|
341 | rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False) | |
342 | rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False) |
|
342 | rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False) | |
343 | rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False) |
|
343 | rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False) | |
|
344 | rhodecode_create_personal_repo_group = v.StringBoolean(if_missing=False) | |||
|
345 | rhodecode_personal_repo_group_pattern = v.UnicodeString(strip=True, min=1, not_empty=False) | |||
344 |
|
346 | |||
345 | return _ApplicationSettingsForm |
|
347 | return _ApplicationSettingsForm | |
346 |
|
348 |
@@ -23,13 +23,13 b'' | |||||
23 | repo group model for RhodeCode |
|
23 | repo group model for RhodeCode | |
24 | """ |
|
24 | """ | |
25 |
|
25 | |||
26 |
|
26 | import os | ||
27 | import datetime |
|
27 | import datetime | |
28 | import itertools |
|
28 | import itertools | |
29 | import logging |
|
29 | import logging | |
30 | import os |
|
|||
31 | import shutil |
|
30 | import shutil | |
32 | import traceback |
|
31 | import traceback | |
|
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 | |||
@@ -38,7 +38,7 b' from rhodecode.model import BaseModel' | |||||
38 | from rhodecode.model.db import ( |
|
38 | from rhodecode.model.db import ( | |
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 |
|
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 |
|
43 | from rhodecode.lib.utils2 import action_logger_generic | |
44 |
|
44 | |||
@@ -49,6 +49,7 b' class RepoGroupModel(BaseModel):' | |||||
49 |
|
49 | |||
50 | cls = RepoGroup |
|
50 | cls = RepoGroup | |
51 | PERSONAL_GROUP_DESC = '[personal] repo group: owner `%(username)s`' |
|
51 | PERSONAL_GROUP_DESC = '[personal] repo group: owner `%(username)s`' | |
|
52 | PERSONAL_GROUP_PATTERN = '${username}' # default | |||
52 |
|
53 | |||
53 | def _get_user_group(self, users_group): |
|
54 | def _get_user_group(self, users_group): | |
54 | return self._get_instance(UserGroup, users_group, |
|
55 | return self._get_instance(UserGroup, users_group, | |
@@ -76,6 +77,39 b' class RepoGroupModel(BaseModel):' | |||||
76 | "sql_cache_short", "get_repo_group_%s" % repo_group_name)) |
|
77 | "sql_cache_short", "get_repo_group_%s" % repo_group_name)) | |
77 | return repo.scalar() |
|
78 | return repo.scalar() | |
78 |
|
79 | |||
|
80 | def get_default_create_personal_repo_group(self): | |||
|
81 | value = SettingsModel().get_setting_by_name( | |||
|
82 | 'create_personal_repo_group') | |||
|
83 | return value.app_settings_value if value else None or False | |||
|
84 | ||||
|
85 | def get_personal_group_name_pattern(self): | |||
|
86 | value = SettingsModel().get_setting_by_name( | |||
|
87 | 'personal_repo_group_pattern') | |||
|
88 | val = value.app_settings_value if value else None | |||
|
89 | group_template = val or self.PERSONAL_GROUP_PATTERN | |||
|
90 | ||||
|
91 | group_template = group_template.lstrip('/') | |||
|
92 | return group_template | |||
|
93 | ||||
|
94 | def get_personal_group_name(self, user): | |||
|
95 | template = self.get_personal_group_name_pattern() | |||
|
96 | return string.Template(template).safe_substitute( | |||
|
97 | username=user.username, | |||
|
98 | user_id=user.user_id, | |||
|
99 | ) | |||
|
100 | ||||
|
101 | def create_personal_repo_group(self, user, commit_early=True): | |||
|
102 | desc = self.PERSONAL_GROUP_DESC % {'username': user.username} | |||
|
103 | personal_repo_group_name = self.get_personal_group_name(user) | |||
|
104 | ||||
|
105 | # create a new one | |||
|
106 | RepoGroupModel().create( | |||
|
107 | group_name=personal_repo_group_name, | |||
|
108 | group_description=desc, | |||
|
109 | owner=user.username, | |||
|
110 | personal=True, | |||
|
111 | commit_early=commit_early) | |||
|
112 | ||||
79 | def _create_default_perms(self, new_group): |
|
113 | def _create_default_perms(self, new_group): | |
80 | # create default permission |
|
114 | # create default permission | |
81 | default_perm = 'group.read' |
|
115 | default_perm = 'group.read' | |
@@ -191,7 +225,7 b' class RepoGroupModel(BaseModel):' | |||||
191 | shutil.move(rm_path, os.path.join(self.repos_path, _d)) |
|
225 | shutil.move(rm_path, os.path.join(self.repos_path, _d)) | |
192 |
|
226 | |||
193 | def create(self, group_name, group_description, owner, just_db=False, |
|
227 | def create(self, group_name, group_description, owner, just_db=False, | |
194 | copy_permissions=False, commit_early=True): |
|
228 | copy_permissions=False, personal=None, commit_early=True): | |
195 |
|
229 | |||
196 | (group_name_cleaned, |
|
230 | (group_name_cleaned, | |
197 | parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name) |
|
231 | parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name) | |
@@ -199,11 +233,18 b' class RepoGroupModel(BaseModel):' | |||||
199 | parent_group = None |
|
233 | parent_group = None | |
200 | if parent_group_name: |
|
234 | if parent_group_name: | |
201 | parent_group = self._get_repo_group(parent_group_name) |
|
235 | parent_group = self._get_repo_group(parent_group_name) | |
|
236 | if not parent_group: | |||
|
237 | # we tried to create a nested group, but the parent is not | |||
|
238 | # existing | |||
|
239 | raise ValueError( | |||
|
240 | 'Parent group `%s` given in `%s` group name ' | |||
|
241 | 'is not yet existing.' % (parent_group_name, group_name)) | |||
202 |
|
242 | |||
203 | # becase we are doing a cleanup, we need to check if such directory |
|
243 | # because we are doing a cleanup, we need to check if such directory | |
204 |
# already exists. If we don't do that we can accidentally delete |
|
244 | # already exists. If we don't do that we can accidentally delete | |
205 |
# directory via cleanup that can cause data issues, since |
|
245 | # existing directory via cleanup that can cause data issues, since | |
206 |
# folder rename to special syntax later cleanup |
|
246 | # delete does a folder rename to special syntax later cleanup | |
|
247 | # functions can delete this | |||
207 | cleanup_group = self.check_exist_filesystem(group_name, |
|
248 | cleanup_group = self.check_exist_filesystem(group_name, | |
208 | exc_on_failure=False) |
|
249 | exc_on_failure=False) | |
209 | try: |
|
250 | try: | |
@@ -213,6 +254,7 b' class RepoGroupModel(BaseModel):' | |||||
213 | new_repo_group.group_description = group_description or group_name |
|
254 | new_repo_group.group_description = group_description or group_name | |
214 | new_repo_group.parent_group = parent_group |
|
255 | new_repo_group.parent_group = parent_group | |
215 | new_repo_group.group_name = group_name |
|
256 | new_repo_group.group_name = group_name | |
|
257 | new_repo_group.personal = personal | |||
216 |
|
258 | |||
217 | self.sa.add(new_repo_group) |
|
259 | self.sa.add(new_repo_group) | |
218 |
|
260 |
@@ -35,7 +35,7 b' from sqlalchemy.sql.expression import tr' | |||||
35 | from rhodecode import events |
|
35 | from rhodecode import events | |
36 | from rhodecode.lib.utils2 import ( |
|
36 | from rhodecode.lib.utils2 import ( | |
37 | safe_unicode, get_current_rhodecode_user, action_logger_generic, |
|
37 | safe_unicode, get_current_rhodecode_user, action_logger_generic, | |
38 | AttributeDict) |
|
38 | AttributeDict, str2bool) | |
39 | from rhodecode.lib.caching_query import FromCache |
|
39 | from rhodecode.lib.caching_query import FromCache | |
40 | from rhodecode.model import BaseModel |
|
40 | from rhodecode.model import BaseModel | |
41 | from rhodecode.model.auth_token import AuthTokenModel |
|
41 | from rhodecode.model.auth_token import AuthTokenModel | |
@@ -104,12 +104,13 b' class UserModel(BaseModel):' | |||||
104 | 'cur_user': cur_user |
|
104 | 'cur_user': cur_user | |
105 | } |
|
105 | } | |
106 |
|
106 | |||
|
107 | if 'create_repo_group' in form_data: | |||
|
108 | user_data['create_repo_group'] = str2bool( | |||
|
109 | form_data.get('create_repo_group')) | |||
|
110 | ||||
107 | try: |
|
111 | try: | |
108 | if form_data.get('create_repo_group'): |
|
|||
109 | user_data['create_repo_group'] = True |
|
|||
110 | if form_data.get('password_change'): |
|
112 | if form_data.get('password_change'): | |
111 | user_data['force_password_change'] = True |
|
113 | user_data['force_password_change'] = True | |
112 |
|
||||
113 | return UserModel().create_or_update(**user_data) |
|
114 | return UserModel().create_or_update(**user_data) | |
114 | except Exception: |
|
115 | except Exception: | |
115 | log.error(traceback.format_exc()) |
|
116 | log.error(traceback.format_exc()) | |
@@ -177,7 +178,7 b' class UserModel(BaseModel):' | |||||
177 | self, username, password, email, firstname='', lastname='', |
|
178 | self, username, password, email, firstname='', lastname='', | |
178 | active=True, admin=False, extern_type=None, extern_name=None, |
|
179 | active=True, admin=False, extern_type=None, extern_name=None, | |
179 | cur_user=None, plugin=None, force_password_change=False, |
|
180 | cur_user=None, plugin=None, force_password_change=False, | |
180 |
allow_to_create_user=True, create_repo_group= |
|
181 | allow_to_create_user=True, create_repo_group=None, | |
181 | updating_user_id=None, language=None, strict_creation_check=True): |
|
182 | updating_user_id=None, language=None, strict_creation_check=True): | |
182 | """ |
|
183 | """ | |
183 | Creates a new instance if not found, or updates current one |
|
184 | Creates a new instance if not found, or updates current one | |
@@ -222,8 +223,8 b' class UserModel(BaseModel):' | |||||
222 | # in case it's a plugin we don't care |
|
223 | # in case it's a plugin we don't care | |
223 | if not plugin: |
|
224 | if not plugin: | |
224 |
|
225 | |||
225 |
# first check if we gave crypted password back, and if it |
|
226 | # first check if we gave crypted password back, and if it | |
226 | # it's not password change |
|
227 | # matches it's not password change | |
227 | if new_user.password == password: |
|
228 | if new_user.password == password: | |
228 | return False |
|
229 | return False | |
229 |
|
230 | |||
@@ -233,6 +234,12 b' class UserModel(BaseModel):' | |||||
233 |
|
234 | |||
234 | return False |
|
235 | return False | |
235 |
|
236 | |||
|
237 | # read settings on default personal repo group creation | |||
|
238 | if create_repo_group is None: | |||
|
239 | default_create_repo_group = RepoGroupModel()\ | |||
|
240 | .get_default_create_personal_repo_group() | |||
|
241 | create_repo_group = default_create_repo_group | |||
|
242 | ||||
236 | user_data = { |
|
243 | user_data = { | |
237 | 'username': username, |
|
244 | 'username': username, | |
238 | 'password': password, |
|
245 | 'password': password, | |
@@ -319,17 +326,16 b' class UserModel(BaseModel):' | |||||
319 | self.sa.add(new_user) |
|
326 | self.sa.add(new_user) | |
320 |
|
327 | |||
321 | if not edit and create_repo_group: |
|
328 | if not edit and create_repo_group: | |
322 | # create new group same as username, and make this user an owner |
|
329 | RepoGroupModel().create_personal_repo_group( | |
323 | desc = RepoGroupModel.PERSONAL_GROUP_DESC % {'username': username} |
|
330 | new_user, commit_early=False) | |
324 | RepoGroupModel().create(group_name=username, |
|
331 | ||
325 | group_description=desc, |
|
|||
326 | owner=username, commit_early=False) |
|
|||
327 | if not edit: |
|
332 | if not edit: | |
328 | # add the RSS token |
|
333 | # add the RSS token | |
329 | AuthTokenModel().create(username, |
|
334 | AuthTokenModel().create(username, | |
330 | description='Generated feed token', |
|
335 | description='Generated feed token', | |
331 | role=AuthTokenModel.cls.ROLE_FEED) |
|
336 | role=AuthTokenModel.cls.ROLE_FEED) | |
332 | log_create_user(created_by=cur_user, **new_user.get_dict()) |
|
337 | log_create_user(created_by=cur_user, **new_user.get_dict()) | |
|
338 | events.trigger(events.UserPostCreate(user_data)) | |||
333 | return new_user |
|
339 | return new_user | |
334 | except (DatabaseError,): |
|
340 | except (DatabaseError,): | |
335 | log.error(traceback.format_exc()) |
|
341 | log.error(traceback.format_exc()) |
@@ -30,6 +30,35 b'' | |||||
30 | </div> |
|
30 | </div> | |
31 | </div> |
|
31 | </div> | |
32 |
|
32 | |||
|
33 | ||||
|
34 | <div class="panel panel-default"> | |||
|
35 | <div class="panel-heading" id="personal-group-options"> | |||
|
36 | <h3 class="panel-title">${_('Personal Repository Group')} <a class="permalink" href="#personal-group-options"> ¶</a></h3> | |||
|
37 | </div> | |||
|
38 | <div class="panel-body"> | |||
|
39 | <div class="checkbox"> | |||
|
40 | ${h.checkbox('rhodecode_create_personal_repo_group','True')} | |||
|
41 | <label for="rhodecode_create_personal_repo_group">${_('Create Personal Repository Group')}</label> | |||
|
42 | </div> | |||
|
43 | <span class="help-block"> | |||
|
44 | ${_('Always create Personal Repository Groups for new users.')} <br/> | |||
|
45 | ${_('When creating new users from add user form or API you can still turn this off via a checkbox or flag')} | |||
|
46 | </span> | |||
|
47 | ||||
|
48 | <div class="label"> | |||
|
49 | <label for="rhodecode_personal_repo_group_pattern">${_('Personal Repo Group Pattern')}</label> | |||
|
50 | </div> | |||
|
51 | <div class="field input"> | |||
|
52 | ${h.text('rhodecode_personal_repo_group_pattern',size=60, placeholder=c.personal_repo_group_default_pattern)} | |||
|
53 | </div> | |||
|
54 | <span class="help-block"> | |||
|
55 | ${_('Pattern used to create Personal Repository Groups. Prefix can be other existing repository group path[s], eg. /u/${username}')} <br/> | |||
|
56 | ${_('Available variables are currently ${username} and ${user_id}')} | |||
|
57 | </span> | |||
|
58 | </div> | |||
|
59 | </div> | |||
|
60 | ||||
|
61 | ||||
33 | <div class="panel panel-default"> |
|
62 | <div class="panel panel-default"> | |
34 | <div class="panel-heading" id="captcha-options"> |
|
63 | <div class="panel-heading" id="captcha-options"> | |
35 | <h3 class="panel-title">${_('Registration Captcha')} <a class="permalink" href="#captcha-options"> ¶</a></h3> |
|
64 | <h3 class="panel-title">${_('Registration Captcha')} <a class="permalink" href="#captcha-options"> ¶</a></h3> |
@@ -113,11 +113,14 b'' | |||||
113 |
|
113 | |||
114 | <div class="field"> |
|
114 | <div class="field"> | |
115 | <div class="label label-checkbox"> |
|
115 | <div class="label label-checkbox"> | |
116 | <label for="create_repo_group">${_('Add repository group')}:</label> |
|
116 | <label for="create_repo_group">${_('Add personal repository group')}:</label> | |
117 | </div> |
|
117 | </div> | |
118 | <div class="checkboxes"> |
|
118 | <div class="checkboxes"> | |
119 | ${h.checkbox('create_repo_group',value=True)} |
|
119 | ${h.checkbox('create_repo_group',value=True, checked=c.default_create_repo_group)} | |
120 | <span class="help-block">${_('Add repository group with the same name as username. \nUser will be automatically set as this group owner.')}</span> |
|
120 | <span class="help-block"> | |
|
121 | ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}<br/> | |||
|
122 | ${_('User will be automatically set as this group owner.')} | |||
|
123 | </span> | |||
121 | </div> |
|
124 | </div> | |
122 | </div> |
|
125 | </div> | |
123 |
|
126 |
@@ -61,7 +61,11 b'' | |||||
61 | %if c.personal_repo_group: |
|
61 | %if c.personal_repo_group: | |
62 | <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, url('repo_group_home', group_name=c.personal_repo_group.group_name))}</div> |
|
62 | <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, url('repo_group_home', group_name=c.personal_repo_group.group_name))}</div> | |
63 | %else: |
|
63 | %else: | |
64 | <div class="panel-body-title-text">${_('This user currently does not have a personal repository group')}</div> |
|
64 | <div class="panel-body-title-text"> | |
|
65 | ${_('This user currently does not have a personal repository group')} | |||
|
66 | <br/> | |||
|
67 | ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}} | |||
|
68 | </div> | |||
65 | %endif |
|
69 | %endif | |
66 | <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}> |
|
70 | <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}> | |
67 | <i class="icon-folder-close"></i> |
|
71 | <i class="icon-folder-close"></i> |
@@ -338,6 +338,9 b'' | |||||
338 | <div class=""> |
|
338 | <div class=""> | |
339 | <ol class="links"> |
|
339 | <ol class="links"> | |
340 | <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li> |
|
340 | <li>${h.link_to(_(u'My account'),h.url('my_account'))}</li> | |
|
341 | % if c.rhodecode_user.personal_repo_group: | |||
|
342 | <li>${h.link_to(_(u'My personal group'), h.url('repo_group_home', group_name=c.rhodecode_user.personal_repo_group.group_name))}</li> | |||
|
343 | % endif | |||
341 | <li class="logout"> |
|
344 | <li class="logout"> | |
342 | ${h.secure_form(h.route_path('logout'))} |
|
345 | ${h.secure_form(h.route_path('logout'))} | |
343 | ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")} |
|
346 | ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")} |
General Comments 0
You need to be logged in to leave comments.
Login now