##// END OF EJS Templates
admin: moved admin pyramid into apps.
marcink -
r1503:91b1464c default
parent child Browse files
Show More
@@ -1,57 +1,57 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 from rhodecode.admin.navigation import NavigationRegistry
22 from rhodecode.apps.admin.navigation import NavigationRegistry
23 23 from rhodecode.config.routing import ADMIN_PREFIX
24 24 from rhodecode.lib.utils2 import str2bool
25 25
26 26
27 27 def includeme(config):
28 28 settings = config.get_settings()
29 29
30 30 # Create admin navigation registry and add it to the pyramid registry.
31 31 labs_active = str2bool(settings.get('labs_settings_active', False))
32 32 navigation_registry = NavigationRegistry(labs_active=labs_active)
33 33 config.registry.registerUtility(navigation_registry)
34 34
35 35 config.add_route(
36 36 name='admin_settings_open_source',
37 37 pattern=ADMIN_PREFIX + '/settings/open_source')
38 38 config.add_route(
39 39 name='admin_settings_vcs_svn_generate_cfg',
40 40 pattern=ADMIN_PREFIX + '/settings/vcs/svn_generate_cfg')
41 41
42 42 config.add_route(
43 43 name='admin_settings_system',
44 44 pattern=ADMIN_PREFIX + '/settings/system')
45 45 config.add_route(
46 46 name='admin_settings_system_update',
47 47 pattern=ADMIN_PREFIX + '/settings/system/updates')
48 48
49 49 config.add_route(
50 50 name='admin_settings_sessions',
51 51 pattern=ADMIN_PREFIX + '/settings/sessions')
52 52 config.add_route(
53 53 name='admin_settings_sessions_cleanup',
54 54 pattern=ADMIN_PREFIX + '/settings/sessions/cleanup')
55 55
56 56 # Scan module for configuration decorators.
57 57 config.scan()
1 NO CONTENT: file renamed from rhodecode/admin/interfaces.py to rhodecode/apps/admin/interfaces.py
@@ -1,140 +1,140 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 from pylons import url
26 26 from zope.interface import implementer
27 27
28 from rhodecode.admin.interfaces import IAdminNavigationRegistry
28 from rhodecode.apps.admin.interfaces import IAdminNavigationRegistry
29 29 from rhodecode.lib.utils import get_registry
30 30 from rhodecode.translation import _
31 31
32 32
33 33 log = logging.getLogger(__name__)
34 34
35 35 NavListEntry = collections.namedtuple('NavListEntry', ['key', 'name', 'url'])
36 36
37 37
38 38 class NavEntry(object):
39 39 """
40 40 Represents an entry in the admin navigation.
41 41
42 42 :param key: Unique identifier used to store reference in an OrderedDict.
43 43 :param name: Display name, usually a translation string.
44 44 :param view_name: Name of the view, used generate the URL.
45 45 :param pyramid: Indicator to use pyramid for URL generation. This should
46 46 be removed as soon as we are fully migrated to pyramid.
47 47 """
48 48
49 49 def __init__(self, key, name, view_name, pyramid=False):
50 50 self.key = key
51 51 self.name = name
52 52 self.view_name = view_name
53 53 self.pyramid = pyramid
54 54
55 55 def generate_url(self, request):
56 56 if self.pyramid:
57 57 if hasattr(request, 'route_path'):
58 58 return request.route_path(self.view_name)
59 59 else:
60 60 # TODO: johbo: Remove this after migrating to pyramid.
61 61 # We need the pyramid request here to generate URLs to pyramid
62 62 # views from within pylons views.
63 63 from pyramid.threadlocal import get_current_request
64 64 pyramid_request = get_current_request()
65 65 return pyramid_request.route_path(self.view_name)
66 66 else:
67 67 return url(self.view_name)
68 68
69 69 def get_localized_name(self, request):
70 70 if hasattr(request, 'translate'):
71 71 return request.translate(self.name)
72 72 else:
73 73 # TODO(marcink): Remove this after migrating to pyramid
74 74 from pyramid.threadlocal import get_current_request
75 75 pyramid_request = get_current_request()
76 76 return pyramid_request.translate(self.name)
77 77
78 78
79 79 @implementer(IAdminNavigationRegistry)
80 80 class NavigationRegistry(object):
81 81
82 82 _base_entries = [
83 83 NavEntry('global', _('Global'), 'admin_settings_global'),
84 84 NavEntry('vcs', _('VCS'), 'admin_settings_vcs'),
85 85 NavEntry('visual', _('Visual'), 'admin_settings_visual'),
86 86 NavEntry('mapping', _('Remap and Rescan'), 'admin_settings_mapping'),
87 87 NavEntry('issuetracker', _('Issue Tracker'),
88 88 'admin_settings_issuetracker'),
89 89 NavEntry('email', _('Email'), 'admin_settings_email'),
90 90 NavEntry('hooks', _('Hooks'), 'admin_settings_hooks'),
91 91 NavEntry('search', _('Full Text Search'), 'admin_settings_search'),
92 92
93 93 NavEntry('integrations', _('Integrations'),
94 94 'global_integrations_home', pyramid=True),
95 95 NavEntry('system', _('System Info'),
96 96 'admin_settings_system', pyramid=True),
97 97 NavEntry('sessions', _('User Sessions'),
98 98 'admin_settings_sessions', pyramid=True),
99 99 NavEntry('open_source', _('Open Source Licenses'),
100 100 'admin_settings_open_source', pyramid=True),
101 101
102 102 # TODO: marcink: we disable supervisor now until the supervisor stats
103 103 # page is fixed in the nix configuration
104 104 # NavEntry('supervisor', _('Supervisor'), 'admin_settings_supervisor'),
105 105 ]
106 106
107 107 _labs_entry = NavEntry('labs', _('Labs'), 'admin_settings_labs')
108 108
109 109 def __init__(self, labs_active=False):
110 110 self._registered_entries = collections.OrderedDict([
111 111 (item.key, item) for item in self.__class__._base_entries
112 112 ])
113 113
114 114 if labs_active:
115 115 self.add_entry(self._labs_entry)
116 116
117 117 def add_entry(self, entry):
118 118 self._registered_entries[entry.key] = entry
119 119
120 120 def get_navlist(self, request):
121 121 navlist = [NavListEntry(i.key, i.get_localized_name(request),
122 122 i.generate_url(request))
123 123 for i in self._registered_entries.values()]
124 124 return navlist
125 125
126 126
127 127 def navigation_registry(request):
128 128 """
129 129 Helper that returns the admin navigation registry.
130 130 """
131 131 pyramid_registry = get_registry(request)
132 132 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
133 133 return nav_registry
134 134
135 135
136 136 def navigation_list(request):
137 137 """
138 138 Helper that returns the admin navigation as list of NavListEntry objects.
139 139 """
140 140 return navigation_registry(request).get_navlist(request)
1 NO CONTENT: file renamed from rhodecode/admin/views/__init__.py to rhodecode/apps/admin/views/__init__.py
1 NO CONTENT: file renamed from rhodecode/admin/views/base.py to rhodecode/apps/admin/views/base.py
@@ -1,48 +1,48 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import collections
22 22 import logging
23 23
24 24 from pylons import tmpl_context as c
25 25 from pyramid.view import view_config
26 26
27 from rhodecode.admin.views.base import AdminSettingsView
28 from rhodecode.admin.navigation import navigation_list
27 from rhodecode.apps.admin.views.base import AdminSettingsView
28 from rhodecode.apps.admin.navigation import navigation_list
29 29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 30 from rhodecode.lib.utils import read_opensource_licenses
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class OpenSourceLicensesAdminSettingsView(AdminSettingsView):
36 36
37 37 @LoginRequired()
38 38 @HasPermissionAllDecorator('hg.admin')
39 39 @view_config(
40 40 route_name='admin_settings_open_source', request_method='GET',
41 41 renderer='rhodecode:templates/admin/settings/settings.mako')
42 42 def open_source_licenses(self):
43 43 c.active = 'open_source'
44 44 c.navlist = navigation_list(self.request)
45 45 c.opensource_licenses = collections.OrderedDict(
46 46 sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
47 47
48 48 return {}
@@ -1,100 +1,98 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pylons import tmpl_context as c
24 24 from pyramid.view import view_config
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 27 from rhodecode.translation import _
28 28
29 from rhodecode.admin.views.base import AdminSettingsView
29 from rhodecode.apps.admin.views.base import AdminSettingsView
30 from rhodecode.apps.admin.navigation import navigation_list
30 31 from rhodecode.lib.auth import (
31 32 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 33 from rhodecode.lib.utils2 import safe_int
33 34 from rhodecode.lib import system_info
34 35 from rhodecode.lib import user_sessions
35 36
36 37
37 from rhodecode.admin.navigation import navigation_list
38
39
40 38 log = logging.getLogger(__name__)
41 39
42 40
43 41 class AdminSessionSettingsView(AdminSettingsView):
44 42
45 43 @LoginRequired()
46 44 @HasPermissionAllDecorator('hg.admin')
47 45 @view_config(
48 46 route_name='admin_settings_sessions', request_method='GET',
49 47 renderer='rhodecode:templates/admin/settings/settings.mako')
50 48 def settings_sessions(self):
51 49 c.active = 'sessions'
52 50 c.navlist = navigation_list(self.request)
53 51
54 52 c.cleanup_older_days = 60
55 53 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
56 54
57 55 config = system_info.rhodecode_config().get_value()['value']['config']
58 56 c.session_model = user_sessions.get_session_handler(
59 57 config.get('beaker.session.type', 'memory'))(config)
60 58
61 59 c.session_conf = c.session_model.config
62 60 c.session_count = c.session_model.get_count()
63 61 c.session_expired_count = c.session_model.get_expired_count(
64 62 older_than_seconds)
65 63
66 64 return {}
67 65
68 66 @LoginRequired()
69 67 @CSRFRequired()
70 68 @HasPermissionAllDecorator('hg.admin')
71 69 @view_config(
72 70 route_name='admin_settings_sessions_cleanup', request_method='POST')
73 71 def settings_sessions_cleanup(self):
74 72 _ = self.request.translate
75 73 expire_days = safe_int(self.request.params.get('expire_days'))
76 74
77 75 if expire_days is None:
78 76 expire_days = 60
79 77
80 78 older_than_seconds = 60 * 60 * 24 * expire_days
81 79
82 80 config = system_info.rhodecode_config().get_value()['value']['config']
83 81 session_model = user_sessions.get_session_handler(
84 82 config.get('beaker.session.type', 'memory'))(config)
85 83
86 84 try:
87 85 session_model.clean_sessions(
88 86 older_than_seconds=older_than_seconds)
89 87 self.request.session.flash(
90 88 _('Cleaned up old sessions'), queue='success')
91 89 except user_sessions.CleanupCommand as msg:
92 90 self.request.session.flash(msg.message, queue='warning')
93 91 except Exception as e:
94 92 log.exception('Failed session cleanup')
95 93 self.request.session.flash(
96 94 _('Failed to cleanup up old sessions'), queue='error')
97 95
98 96 redirect_to = self.request.resource_path(
99 97 self.context, route_name='admin_settings_sessions')
100 98 return HTTPFound(redirect_to)
@@ -1,60 +1,60 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.view import view_config
24 24
25 25 from rhodecode.svn_support.utils import generate_mod_dav_svn_config
26 26
27 from rhodecode.admin.views.base import AdminSettingsView
27 from rhodecode.apps.admin.views.base import AdminSettingsView
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class SvnConfigAdminSettingsView(AdminSettingsView):
35 35
36 36 @LoginRequired()
37 37 @CSRFRequired()
38 38 @HasPermissionAllDecorator('hg.admin')
39 39 @view_config(
40 40 route_name='admin_settings_vcs_svn_generate_cfg',
41 41 request_method='POST', renderer='json')
42 42 def vcs_svn_generate_config(self):
43 43 _ = self.request.translate
44 44 try:
45 45 generate_mod_dav_svn_config(self.request.registry)
46 46 msg = {
47 47 'message': _('Apache configuration for Subversion generated.'),
48 48 'level': 'success',
49 49 }
50 50 except Exception:
51 51 log.exception(
52 52 'Exception while generating the Apache '
53 53 'configuration for Subversion.')
54 54 msg = {
55 55 'message': _('Failed to generate the Apache configuration for Subversion.'),
56 56 'level': 'error',
57 57 }
58 58
59 59 data = {'message': msg}
60 60 return data
@@ -1,204 +1,203 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import urllib2
23 23 import packaging.version
24 24
25 25 from pylons import tmpl_context as c
26 26 from pyramid.view import view_config
27 27
28 28 import rhodecode
29 from rhodecode.apps.admin.views.base import AdminSettingsView
30 from rhodecode.apps.admin.navigation import navigation_list
29 31 from rhodecode.lib import helpers as h
30 32 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
31 33 from rhodecode.lib.utils2 import str2bool
32 34 from rhodecode.lib import system_info
33 35 from rhodecode.lib.ext_json import json
34
35 from rhodecode.admin.views.base import AdminSettingsView
36 from rhodecode.admin.navigation import navigation_list
37 36 from rhodecode.model.settings import SettingsModel
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 41 class AdminSystemInfoSettingsView(AdminSettingsView):
43 42
44 43 @staticmethod
45 44 def get_update_data(update_url):
46 45 """Return the JSON update data."""
47 46 ver = rhodecode.__version__
48 47 log.debug('Checking for upgrade on `%s` server', update_url)
49 48 opener = urllib2.build_opener()
50 49 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
51 50 response = opener.open(update_url)
52 51 response_data = response.read()
53 52 data = json.loads(response_data)
54 53
55 54 return data
56 55
57 56 def get_update_url(self):
58 57 settings = SettingsModel().get_all_settings()
59 58 return settings.get('rhodecode_update_url')
60 59
61 60 @LoginRequired()
62 61 @HasPermissionAllDecorator('hg.admin')
63 62 @view_config(
64 63 route_name='admin_settings_system', request_method='GET',
65 64 renderer='rhodecode:templates/admin/settings/settings.mako')
66 65 def settings_system_info(self):
67 66 _ = self.request.translate
68 67
69 68 c.active = 'system'
70 69 c.navlist = navigation_list(self.request)
71 70
72 71 # TODO(marcink), figure out how to allow only selected users to do this
73 72 c.allowed_to_snapshot = self._rhodecode_user.admin
74 73
75 74 snapshot = str2bool(self.request.params.get('snapshot'))
76 75
77 76 c.rhodecode_update_url = self.get_update_url()
78 77 server_info = system_info.get_system_info(self.request.environ)
79 78
80 79 for key, val in server_info.items():
81 80 setattr(c, key, val)
82 81
83 82 def val(name, subkey='human_value'):
84 83 return server_info[name][subkey]
85 84
86 85 def state(name):
87 86 return server_info[name]['state']
88 87
89 88 def val2(name):
90 89 val = server_info[name]['human_value']
91 90 state = server_info[name]['state']
92 91 return val, state
93 92
94 93 update_info_msg = _('Note: please make sure this server can '
95 94 'access `${url}` for the update link to work',
96 95 mapping=dict(url=c.rhodecode_update_url))
97 96 c.data_items = [
98 97 # update info
99 98 (_('Update info'), h.literal(
100 99 '<span class="link" id="check_for_update" >%s.</span>' % (
101 100 _('Check for updates')) +
102 101 '<br/> <span >%s.</span>' % (update_info_msg)
103 102 ), ''),
104 103
105 104 # RhodeCode specific
106 105 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
107 106 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
108 107 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
109 108 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
110 109 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
111 110 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
112 111 ('', '', ''), # spacer
113 112
114 113 # Database
115 114 (_('Database'), val('database')['url'], state('database')),
116 115 (_('Database version'), val('database')['version'], state('database')),
117 116 ('', '', ''), # spacer
118 117
119 118 # Platform/Python
120 119 (_('Platform'), val('platform')['name'], state('platform')),
121 120 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
122 121 (_('Python version'), val('python')['version'], state('python')),
123 122 (_('Python path'), val('python')['executable'], state('python')),
124 123 ('', '', ''), # spacer
125 124
126 125 # Systems stats
127 126 (_('CPU'), val('cpu')['text'], state('cpu')),
128 127 (_('Load'), val('load')['text'], state('load')),
129 128 (_('Memory'), val('memory')['text'], state('memory')),
130 129 (_('Uptime'), val('uptime')['text'], state('uptime')),
131 130 ('', '', ''), # spacer
132 131
133 132 # Repo storage
134 133 (_('Storage location'), val('storage')['path'], state('storage')),
135 134 (_('Storage info'), val('storage')['text'], state('storage')),
136 135 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
137 136
138 137 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
139 138 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
140 139
141 140 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
142 141 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
143 142
144 143 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
145 144 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
146 145
147 146 (_('Search info'), val('search')['text'], state('search')),
148 147 (_('Search location'), val('search')['location'], state('search')),
149 148 ('', '', ''), # spacer
150 149
151 150 # VCS specific
152 151 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
153 152 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
154 153 (_('GIT'), val('git'), state('git')),
155 154 (_('HG'), val('hg'), state('hg')),
156 155 (_('SVN'), val('svn'), state('svn')),
157 156
158 157 ]
159 158
160 159 if snapshot:
161 160 if c.allowed_to_snapshot:
162 161 c.data_items.pop(0) # remove server info
163 162 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
164 163 else:
165 164 self.request.session.flash(
166 165 'You are not allowed to do this', queue='warning')
167 166 return {}
168 167
169 168 @LoginRequired()
170 169 @HasPermissionAllDecorator('hg.admin')
171 170 @view_config(
172 171 route_name='admin_settings_system_update', request_method='GET',
173 172 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
174 173 def settings_system_info_check_update(self):
175 174 _ = self.request.translate
176 175
177 176 update_url = self.get_update_url()
178 177
179 178 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
180 179 try:
181 180 data = self.get_update_data(update_url)
182 181 except urllib2.URLError as e:
183 182 log.exception("Exception contacting upgrade server")
184 183 self.request.override_renderer = 'string'
185 184 return _err('Failed to contact upgrade server: %r' % e)
186 185 except ValueError as e:
187 186 log.exception("Bad data sent from update server")
188 187 self.request.override_renderer = 'string'
189 188 return _err('Bad data sent from update server')
190 189
191 190 latest = data['versions'][0]
192 191
193 192 c.update_url = update_url
194 193 c.latest_data = latest
195 194 c.latest_ver = latest['version']
196 195 c.cur_ver = rhodecode.__version__
197 196 c.should_upgrade = False
198 197
199 198 if (packaging.version.Version(c.latest_ver) >
200 199 packaging.version.Version(c.cur_ver)):
201 200 c.should_upgrade = True
202 201 c.important_notices = latest['general']
203 202
204 203 return {}
@@ -1,499 +1,501 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 from collections import OrderedDict
26 26
27 27 from paste.registry import RegistryManager
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pylons.wsgiapp import PylonsApp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 34 from pyramid.httpexceptions import (
35 35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound)
36 36 from pyramid.events import ApplicationCreated
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
39 39 import routes.util
40 40
41 41 import rhodecode
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 45 from rhodecode.config.environment import (
46 46 load_environment, load_pyramid_environment)
47 47 from rhodecode.lib.middleware import csrf
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.error_handling import (
50 50 PylonsErrorHandlingMiddleware)
51 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import (
56 56 scan_repositories_if_enabled, write_metadata_if_needed)
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 # this is used to avoid avoid the route lookup overhead in routesmiddleware
63 63 # for certain routes which won't go to pylons to - eg. static files, debugger
64 64 # it is only needed for the pylons migration and can be removed once complete
65 65 class SkippableRoutesMiddleware(RoutesMiddleware):
66 66 """ Routes middleware that allows you to skip prefixes """
67 67
68 68 def __init__(self, *args, **kw):
69 69 self.skip_prefixes = kw.pop('skip_prefixes', [])
70 70 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
71 71
72 72 def __call__(self, environ, start_response):
73 73 for prefix in self.skip_prefixes:
74 74 if environ['PATH_INFO'].startswith(prefix):
75 75 # added to avoid the case when a missing /_static route falls
76 76 # through to pylons and causes an exception as pylons is
77 77 # expecting wsgiorg.routingargs to be set in the environ
78 78 # by RoutesMiddleware.
79 79 if 'wsgiorg.routing_args' not in environ:
80 80 environ['wsgiorg.routing_args'] = (None, {})
81 81 return self.app(environ, start_response)
82 82
83 83 return super(SkippableRoutesMiddleware, self).__call__(
84 84 environ, start_response)
85 85
86 86
87 87 def make_app(global_conf, static_files=True, **app_conf):
88 88 """Create a Pylons WSGI application and return it
89 89
90 90 ``global_conf``
91 91 The inherited configuration for this application. Normally from
92 92 the [DEFAULT] section of the Paste ini file.
93 93
94 94 ``app_conf``
95 95 The application's local configuration. Normally specified in
96 96 the [app:<name>] section of the Paste ini file (where <name>
97 97 defaults to main).
98 98
99 99 """
100 100 # Apply compatibility patches
101 101 patches.kombu_1_5_1_python_2_7_11()
102 102 patches.inspect_getargspec()
103 103
104 104 # Configure the Pylons environment
105 105 config = load_environment(global_conf, app_conf)
106 106
107 107 # The Pylons WSGI app
108 108 app = PylonsApp(config=config)
109 109 if rhodecode.is_test:
110 110 app = csrf.CSRFDetector(app)
111 111
112 112 expected_origin = config.get('expected_origin')
113 113 if expected_origin:
114 114 # The API can be accessed from other Origins.
115 115 app = csrf.OriginChecker(app, expected_origin,
116 116 skip_urls=[routes.util.url_for('api')])
117 117
118 118 # Establish the Registry for this application
119 119 app = RegistryManager(app)
120 120
121 121 app.config = config
122 122
123 123 return app
124 124
125 125
126 126 def make_pyramid_app(global_config, **settings):
127 127 """
128 128 Constructs the WSGI application based on Pyramid and wraps the Pylons based
129 129 application.
130 130
131 131 Specials:
132 132
133 133 * We migrate from Pylons to Pyramid. While doing this, we keep both
134 134 frameworks functional. This involves moving some WSGI middlewares around
135 135 and providing access to some data internals, so that the old code is
136 136 still functional.
137 137
138 138 * The application can also be integrated like a plugin via the call to
139 139 `includeme`. This is accompanied with the other utility functions which
140 140 are called. Changing this should be done with great care to not break
141 141 cases when these fragments are assembled from another place.
142 142
143 143 """
144 144 # The edition string should be available in pylons too, so we add it here
145 145 # before copying the settings.
146 146 settings.setdefault('rhodecode.edition', 'Community Edition')
147 147
148 148 # As long as our Pylons application does expect "unprepared" settings, make
149 149 # sure that we keep an unmodified copy. This avoids unintentional change of
150 150 # behavior in the old application.
151 151 settings_pylons = settings.copy()
152 152
153 153 sanitize_settings_and_apply_defaults(settings)
154 154 config = Configurator(settings=settings)
155 155 add_pylons_compat_data(config.registry, global_config, settings_pylons)
156 156
157 157 load_pyramid_environment(global_config, settings)
158 158
159 159 includeme_first(config)
160 160 includeme(config)
161 161 pyramid_app = config.make_wsgi_app()
162 162 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
163 163 pyramid_app.config = config
164 164
165 165 # creating the app uses a connection - return it after we are done
166 166 meta.Session.remove()
167 167
168 168 return pyramid_app
169 169
170 170
171 171 def make_not_found_view(config):
172 172 """
173 173 This creates the view which should be registered as not-found-view to
174 174 pyramid. Basically it contains of the old pylons app, converted to a view.
175 175 Additionally it is wrapped by some other middlewares.
176 176 """
177 177 settings = config.registry.settings
178 178 vcs_server_enabled = settings['vcs.server.enable']
179 179
180 180 # Make pylons app from unprepared settings.
181 181 pylons_app = make_app(
182 182 config.registry._pylons_compat_global_config,
183 183 **config.registry._pylons_compat_settings)
184 184 config.registry._pylons_compat_config = pylons_app.config
185 185
186 186 # Appenlight monitoring.
187 187 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
188 188 pylons_app, settings)
189 189
190 190 # The pylons app is executed inside of the pyramid 404 exception handler.
191 191 # Exceptions which are raised inside of it are not handled by pyramid
192 192 # again. Therefore we add a middleware that invokes the error handler in
193 193 # case of an exception or error response. This way we return proper error
194 194 # HTML pages in case of an error.
195 195 reraise = (settings.get('debugtoolbar.enabled', False) or
196 196 rhodecode.disable_error_handler)
197 197 pylons_app = PylonsErrorHandlingMiddleware(
198 198 pylons_app, error_handler, reraise)
199 199
200 200 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
201 201 # view to handle the request. Therefore it is wrapped around the pylons
202 202 # app. It has to be outside of the error handling otherwise error responses
203 203 # from the vcsserver are converted to HTML error pages. This confuses the
204 204 # command line tools and the user won't get a meaningful error message.
205 205 if vcs_server_enabled:
206 206 pylons_app = VCSMiddleware(
207 207 pylons_app, settings, appenlight_client, registry=config.registry)
208 208
209 209 # Convert WSGI app to pyramid view and return it.
210 210 return wsgiapp(pylons_app)
211 211
212 212
213 213 def add_pylons_compat_data(registry, global_config, settings):
214 214 """
215 215 Attach data to the registry to support the Pylons integration.
216 216 """
217 217 registry._pylons_compat_global_config = global_config
218 218 registry._pylons_compat_settings = settings
219 219
220 220
221 221 def error_handler(exception, request):
222 222 import rhodecode
223 223 from rhodecode.lib.utils2 import AttributeDict
224 224
225 225 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
226 226
227 227 base_response = HTTPInternalServerError()
228 228 # prefer original exception for the response since it may have headers set
229 229 if isinstance(exception, HTTPException):
230 230 base_response = exception
231 231
232 232 def is_http_error(response):
233 233 # error which should have traceback
234 234 return response.status_code > 499
235 235
236 236 if is_http_error(base_response):
237 237 log.exception(
238 238 'error occurred handling this request for path: %s', request.path)
239 239
240 240 c = AttributeDict()
241 241 c.error_message = base_response.status
242 242 c.error_explanation = base_response.explanation or str(base_response)
243 243 c.visual = AttributeDict()
244 244
245 245 c.visual.rhodecode_support_url = (
246 246 request.registry.settings.get('rhodecode_support_url') or
247 247 request.route_url('rhodecode_support')
248 248 )
249 249 c.redirect_time = 0
250 250 c.rhodecode_name = rhodecode_title
251 251 if not c.rhodecode_name:
252 252 c.rhodecode_name = 'Rhodecode'
253 253
254 254 c.causes = []
255 255 if hasattr(base_response, 'causes'):
256 256 c.causes = base_response.causes
257 257
258 258 response = render_to_response(
259 259 '/errors/error_document.mako', {'c': c}, request=request,
260 260 response=base_response)
261 261
262 262 return response
263 263
264 264
265 265 def includeme(config):
266 266 settings = config.registry.settings
267 267
268 268 # plugin information
269 269 config.registry.rhodecode_plugins = OrderedDict()
270 270
271 271 config.add_directive(
272 272 'register_rhodecode_plugin', register_rhodecode_plugin)
273 273
274 274 if asbool(settings.get('appenlight', 'false')):
275 275 config.include('appenlight_client.ext.pyramid_tween')
276 276
277 277 # Includes which are required. The application would fail without them.
278 278 config.include('pyramid_mako')
279 279 config.include('pyramid_beaker')
280
280 281 config.include('rhodecode.channelstream')
281 config.include('rhodecode.admin')
282 282 config.include('rhodecode.authentication')
283 283 config.include('rhodecode.integrations')
284 284
285 285 # apps
286 config.include('rhodecode.apps.admin')
286 287 config.include('rhodecode.apps.login')
287 288 config.include('rhodecode.apps.user_profile')
288 289
289 290 config.include('rhodecode.tweens')
290 291 config.include('rhodecode.api')
291 292 config.include('rhodecode.svn_support')
293
292 294 config.add_route(
293 295 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
294 296
295 297 config.add_translation_dirs('rhodecode:i18n/')
296 298 settings['default_locale_name'] = settings.get('lang', 'en')
297 299
298 300 # Add subscribers.
299 301 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
300 302 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
301 303
302 304 # Set the authorization policy.
303 305 authz_policy = ACLAuthorizationPolicy()
304 306 config.set_authorization_policy(authz_policy)
305 307
306 308 # Set the default renderer for HTML templates to mako.
307 309 config.add_mako_renderer('.html')
308 310
309 311 # include RhodeCode plugins
310 312 includes = aslist(settings.get('rhodecode.includes', []))
311 313 for inc in includes:
312 314 config.include(inc)
313 315
314 316 # This is the glue which allows us to migrate in chunks. By registering the
315 317 # pylons based application as the "Not Found" view in Pyramid, we will
316 318 # fallback to the old application each time the new one does not yet know
317 319 # how to handle a request.
318 320 config.add_notfound_view(make_not_found_view(config))
319 321
320 322 if not settings.get('debugtoolbar.enabled', False):
321 323 # if no toolbar, then any exception gets caught and rendered
322 324 config.add_view(error_handler, context=Exception)
323 325
324 326 config.add_view(error_handler, context=HTTPError)
325 327
326 328
327 329 def includeme_first(config):
328 330 # redirect automatic browser favicon.ico requests to correct place
329 331 def favicon_redirect(context, request):
330 332 return HTTPFound(
331 333 request.static_path('rhodecode:public/images/favicon.ico'))
332 334
333 335 config.add_view(favicon_redirect, route_name='favicon')
334 336 config.add_route('favicon', '/favicon.ico')
335 337
336 338 def robots_redirect(context, request):
337 339 return HTTPFound(
338 340 request.static_path('rhodecode:public/robots.txt'))
339 341
340 342 config.add_view(robots_redirect, route_name='robots')
341 343 config.add_route('robots', '/robots.txt')
342 344
343 345 config.add_static_view(
344 346 '_static/deform', 'deform:static')
345 347 config.add_static_view(
346 348 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
347 349
348 350
349 351 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
350 352 """
351 353 Apply outer WSGI middlewares around the application.
352 354
353 355 Part of this has been moved up from the Pylons layer, so that the
354 356 data is also available if old Pylons code is hit through an already ported
355 357 view.
356 358 """
357 359 settings = config.registry.settings
358 360
359 361 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
360 362 pyramid_app = HttpsFixup(pyramid_app, settings)
361 363
362 364 # Add RoutesMiddleware to support the pylons compatibility tween during
363 365 # migration to pyramid.
364 366 pyramid_app = SkippableRoutesMiddleware(
365 367 pyramid_app, config.registry._pylons_compat_config['routes.map'],
366 368 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
367 369
368 370 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
369 371
370 372 if settings['gzip_responses']:
371 373 pyramid_app = make_gzip_middleware(
372 374 pyramid_app, settings, compress_level=1)
373 375
374 376 # this should be the outer most middleware in the wsgi stack since
375 377 # middleware like Routes make database calls
376 378 def pyramid_app_with_cleanup(environ, start_response):
377 379 try:
378 380 return pyramid_app(environ, start_response)
379 381 finally:
380 382 # Dispose current database session and rollback uncommitted
381 383 # transactions.
382 384 meta.Session.remove()
383 385
384 386 # In a single threaded mode server, on non sqlite db we should have
385 387 # '0 Current Checked out connections' at the end of a request,
386 388 # if not, then something, somewhere is leaving a connection open
387 389 pool = meta.Base.metadata.bind.engine.pool
388 390 log.debug('sa pool status: %s', pool.status())
389 391
390 392
391 393 return pyramid_app_with_cleanup
392 394
393 395
394 396 def sanitize_settings_and_apply_defaults(settings):
395 397 """
396 398 Applies settings defaults and does all type conversion.
397 399
398 400 We would move all settings parsing and preparation into this place, so that
399 401 we have only one place left which deals with this part. The remaining parts
400 402 of the application would start to rely fully on well prepared settings.
401 403
402 404 This piece would later be split up per topic to avoid a big fat monster
403 405 function.
404 406 """
405 407
406 408 # Pyramid's mako renderer has to search in the templates folder so that the
407 409 # old templates still work. Ported and new templates are expected to use
408 410 # real asset specifications for the includes.
409 411 mako_directories = settings.setdefault('mako.directories', [
410 412 # Base templates of the original Pylons application
411 413 'rhodecode:templates',
412 414 ])
413 415 log.debug(
414 416 "Using the following Mako template directories: %s",
415 417 mako_directories)
416 418
417 419 # Default includes, possible to change as a user
418 420 pyramid_includes = settings.setdefault('pyramid.includes', [
419 421 'rhodecode.lib.middleware.request_wrapper',
420 422 ])
421 423 log.debug(
422 424 "Using the following pyramid.includes: %s",
423 425 pyramid_includes)
424 426
425 427 # TODO: johbo: Re-think this, usually the call to config.include
426 428 # should allow to pass in a prefix.
427 429 settings.setdefault('rhodecode.api.url', '/_admin/api')
428 430
429 431 # Sanitize generic settings.
430 432 _list_setting(settings, 'default_encoding', 'UTF-8')
431 433 _bool_setting(settings, 'is_test', 'false')
432 434 _bool_setting(settings, 'gzip_responses', 'false')
433 435
434 436 # Call split out functions that sanitize settings for each topic.
435 437 _sanitize_appenlight_settings(settings)
436 438 _sanitize_vcs_settings(settings)
437 439
438 440 return settings
439 441
440 442
441 443 def _sanitize_appenlight_settings(settings):
442 444 _bool_setting(settings, 'appenlight', 'false')
443 445
444 446
445 447 def _sanitize_vcs_settings(settings):
446 448 """
447 449 Applies settings defaults and does type conversion for all VCS related
448 450 settings.
449 451 """
450 452 _string_setting(settings, 'vcs.svn.compatible_version', '')
451 453 _string_setting(settings, 'git_rev_filter', '--all')
452 454 _string_setting(settings, 'vcs.hooks.protocol', 'http')
453 455 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
454 456 _string_setting(settings, 'vcs.server', '')
455 457 _string_setting(settings, 'vcs.server.log_level', 'debug')
456 458 _string_setting(settings, 'vcs.server.protocol', 'http')
457 459 _bool_setting(settings, 'startup.import_repos', 'false')
458 460 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
459 461 _bool_setting(settings, 'vcs.server.enable', 'true')
460 462 _bool_setting(settings, 'vcs.start_server', 'false')
461 463 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
462 464 _int_setting(settings, 'vcs.connection_timeout', 3600)
463 465
464 466 # Support legacy values of vcs.scm_app_implementation. Legacy
465 467 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
466 468 # which is now mapped to 'http'.
467 469 scm_app_impl = settings['vcs.scm_app_implementation']
468 470 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
469 471 settings['vcs.scm_app_implementation'] = 'http'
470 472
471 473
472 474 def _int_setting(settings, name, default):
473 475 settings[name] = int(settings.get(name, default))
474 476
475 477
476 478 def _bool_setting(settings, name, default):
477 479 input = settings.get(name, default)
478 480 if isinstance(input, unicode):
479 481 input = input.encode('utf8')
480 482 settings[name] = asbool(input)
481 483
482 484
483 485 def _list_setting(settings, name, default):
484 486 raw_value = settings.get(name, default)
485 487
486 488 old_separator = ','
487 489 if old_separator in raw_value:
488 490 # If we get a comma separated list, pass it to our own function.
489 491 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
490 492 else:
491 493 # Otherwise we assume it uses pyramids space/newline separation.
492 494 settings[name] = aslist(raw_value)
493 495
494 496
495 497 def _string_setting(settings, name, default, lower=True):
496 498 value = settings.get(name, default)
497 499 if lower:
498 500 value = value.lower()
499 501 settings[name] = value
@@ -1,692 +1,692 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 settings controller for rhodecode admin
24 24 """
25 25
26 26 import collections
27 27 import logging
28 28
29 29 import datetime
30 30 import formencode
31 31 from formencode import htmlfill
32 32 from pylons import request, tmpl_context as c, url, config
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from webob.exc import HTTPBadRequest
37 37
38 38 import rhodecode
39 from rhodecode.admin.navigation import navigation_list
39 from rhodecode.apps.admin.navigation import navigation_list
40 40 from rhodecode.lib import auth
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.celerylib import tasks, run_task
45 45 from rhodecode.lib.utils import repo2db_mapper
46 46 from rhodecode.lib.utils2 import (
47 47 str2bool, safe_unicode, AttributeDict, safe_int)
48 48 from rhodecode.lib.compat import OrderedDict
49 49 from rhodecode.lib.utils import jsonify
50 50
51 51 from rhodecode.model.db import RhodeCodeUi, Repository
52 52 from rhodecode.model.forms import ApplicationSettingsForm, \
53 53 ApplicationUiSettingsForm, ApplicationVisualisationForm, \
54 54 LabsSettingsForm, IssueTrackerPatternsForm
55 55 from rhodecode.model.repo_group import RepoGroupModel
56 56
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.notification import EmailNotificationModel
59 59 from rhodecode.model.meta import Session
60 60 from rhodecode.model.settings import (
61 61 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
62 62 SettingsModel)
63 63
64 64 from rhodecode.model.supervisor import SupervisorModel, SUPERVISOR_MASTER
65 65 from rhodecode.svn_support.config_keys import generate_config
66 66
67 67
68 68 log = logging.getLogger(__name__)
69 69
70 70
71 71 class SettingsController(BaseController):
72 72 """REST Controller styled on the Atom Publishing Protocol"""
73 73 # To properly map this controller, ensure your config/routing.py
74 74 # file has a resource setup:
75 75 # map.resource('setting', 'settings', controller='admin/settings',
76 76 # path_prefix='/admin', name_prefix='admin_')
77 77
78 78 @LoginRequired()
79 79 def __before__(self):
80 80 super(SettingsController, self).__before__()
81 81 c.labs_active = str2bool(
82 82 rhodecode.CONFIG.get('labs_settings_active', 'true'))
83 83 c.navlist = navigation_list(request)
84 84
85 85 def _get_hg_ui_settings(self):
86 86 ret = RhodeCodeUi.query().all()
87 87
88 88 if not ret:
89 89 raise Exception('Could not get application ui settings !')
90 90 settings = {}
91 91 for each in ret:
92 92 k = each.ui_key
93 93 v = each.ui_value
94 94 if k == '/':
95 95 k = 'root_path'
96 96
97 97 if k in ['push_ssl', 'publish']:
98 98 v = str2bool(v)
99 99
100 100 if k.find('.') != -1:
101 101 k = k.replace('.', '_')
102 102
103 103 if each.ui_section in ['hooks', 'extensions']:
104 104 v = each.ui_active
105 105
106 106 settings[each.ui_section + '_' + k] = v
107 107 return settings
108 108
109 109 @HasPermissionAllDecorator('hg.admin')
110 110 @auth.CSRFRequired()
111 111 @jsonify
112 112 def delete_svn_pattern(self):
113 113 if not request.is_xhr:
114 114 raise HTTPBadRequest()
115 115
116 116 delete_pattern_id = request.POST.get('delete_svn_pattern')
117 117 model = VcsSettingsModel()
118 118 try:
119 119 model.delete_global_svn_pattern(delete_pattern_id)
120 120 except SettingNotFound:
121 121 raise HTTPBadRequest()
122 122
123 123 Session().commit()
124 124 return True
125 125
126 126 @HasPermissionAllDecorator('hg.admin')
127 127 @auth.CSRFRequired()
128 128 def settings_vcs_update(self):
129 129 """POST /admin/settings: All items in the collection"""
130 130 # url('admin_settings_vcs')
131 131 c.active = 'vcs'
132 132
133 133 model = VcsSettingsModel()
134 134 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
135 135 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
136 136
137 137 # TODO: Replace with request.registry after migrating to pyramid.
138 138 pyramid_settings = get_current_registry().settings
139 139 c.svn_proxy_generate_config = pyramid_settings[generate_config]
140 140
141 141 application_form = ApplicationUiSettingsForm()()
142 142
143 143 try:
144 144 form_result = application_form.to_python(dict(request.POST))
145 145 except formencode.Invalid as errors:
146 146 h.flash(
147 147 _("Some form inputs contain invalid data."),
148 148 category='error')
149 149 return htmlfill.render(
150 150 render('admin/settings/settings.mako'),
151 151 defaults=errors.value,
152 152 errors=errors.error_dict or {},
153 153 prefix_error=False,
154 154 encoding="UTF-8",
155 155 force_defaults=False
156 156 )
157 157
158 158 try:
159 159 if c.visual.allow_repo_location_change:
160 160 model.update_global_path_setting(
161 161 form_result['paths_root_path'])
162 162
163 163 model.update_global_ssl_setting(form_result['web_push_ssl'])
164 164 model.update_global_hook_settings(form_result)
165 165
166 166 model.create_or_update_global_svn_settings(form_result)
167 167 model.create_or_update_global_hg_settings(form_result)
168 168 model.create_or_update_global_pr_settings(form_result)
169 169 except Exception:
170 170 log.exception("Exception while updating settings")
171 171 h.flash(_('Error occurred during updating '
172 172 'application settings'), category='error')
173 173 else:
174 174 Session().commit()
175 175 h.flash(_('Updated VCS settings'), category='success')
176 176 return redirect(url('admin_settings_vcs'))
177 177
178 178 return htmlfill.render(
179 179 render('admin/settings/settings.mako'),
180 180 defaults=self._form_defaults(),
181 181 encoding="UTF-8",
182 182 force_defaults=False)
183 183
184 184 @HasPermissionAllDecorator('hg.admin')
185 185 def settings_vcs(self):
186 186 """GET /admin/settings: All items in the collection"""
187 187 # url('admin_settings_vcs')
188 188 c.active = 'vcs'
189 189 model = VcsSettingsModel()
190 190 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
191 191 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
192 192
193 193 # TODO: Replace with request.registry after migrating to pyramid.
194 194 pyramid_settings = get_current_registry().settings
195 195 c.svn_proxy_generate_config = pyramid_settings[generate_config]
196 196
197 197 return htmlfill.render(
198 198 render('admin/settings/settings.mako'),
199 199 defaults=self._form_defaults(),
200 200 encoding="UTF-8",
201 201 force_defaults=False)
202 202
203 203 @HasPermissionAllDecorator('hg.admin')
204 204 @auth.CSRFRequired()
205 205 def settings_mapping_update(self):
206 206 """POST /admin/settings/mapping: All items in the collection"""
207 207 # url('admin_settings_mapping')
208 208 c.active = 'mapping'
209 209 rm_obsolete = request.POST.get('destroy', False)
210 210 invalidate_cache = request.POST.get('invalidate', False)
211 211 log.debug(
212 212 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
213 213
214 214 if invalidate_cache:
215 215 log.debug('invalidating all repositories cache')
216 216 for repo in Repository.get_all():
217 217 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
218 218
219 219 filesystem_repos = ScmModel().repo_scan()
220 220 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
221 221 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
222 222 h.flash(_('Repositories successfully '
223 223 'rescanned added: %s ; removed: %s') %
224 224 (_repr(added), _repr(removed)),
225 225 category='success')
226 226 return redirect(url('admin_settings_mapping'))
227 227
228 228 @HasPermissionAllDecorator('hg.admin')
229 229 def settings_mapping(self):
230 230 """GET /admin/settings/mapping: All items in the collection"""
231 231 # url('admin_settings_mapping')
232 232 c.active = 'mapping'
233 233
234 234 return htmlfill.render(
235 235 render('admin/settings/settings.mako'),
236 236 defaults=self._form_defaults(),
237 237 encoding="UTF-8",
238 238 force_defaults=False)
239 239
240 240 @HasPermissionAllDecorator('hg.admin')
241 241 @auth.CSRFRequired()
242 242 def settings_global_update(self):
243 243 """POST /admin/settings/global: All items in the collection"""
244 244 # url('admin_settings_global')
245 245 c.active = 'global'
246 246 c.personal_repo_group_default_pattern = RepoGroupModel()\
247 247 .get_personal_group_name_pattern()
248 248 application_form = ApplicationSettingsForm()()
249 249 try:
250 250 form_result = application_form.to_python(dict(request.POST))
251 251 except formencode.Invalid as errors:
252 252 return htmlfill.render(
253 253 render('admin/settings/settings.mako'),
254 254 defaults=errors.value,
255 255 errors=errors.error_dict or {},
256 256 prefix_error=False,
257 257 encoding="UTF-8",
258 258 force_defaults=False)
259 259
260 260 try:
261 261 settings = [
262 262 ('title', 'rhodecode_title', 'unicode'),
263 263 ('realm', 'rhodecode_realm', 'unicode'),
264 264 ('pre_code', 'rhodecode_pre_code', 'unicode'),
265 265 ('post_code', 'rhodecode_post_code', 'unicode'),
266 266 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
267 267 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
268 268 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
269 269 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
270 270 ]
271 271 for setting, form_key, type_ in settings:
272 272 sett = SettingsModel().create_or_update_setting(
273 273 setting, form_result[form_key], type_)
274 274 Session().add(sett)
275 275
276 276 Session().commit()
277 277 SettingsModel().invalidate_settings_cache()
278 278 h.flash(_('Updated application settings'), category='success')
279 279 except Exception:
280 280 log.exception("Exception while updating application settings")
281 281 h.flash(
282 282 _('Error occurred during updating application settings'),
283 283 category='error')
284 284
285 285 return redirect(url('admin_settings_global'))
286 286
287 287 @HasPermissionAllDecorator('hg.admin')
288 288 def settings_global(self):
289 289 """GET /admin/settings/global: All items in the collection"""
290 290 # url('admin_settings_global')
291 291 c.active = 'global'
292 292 c.personal_repo_group_default_pattern = RepoGroupModel()\
293 293 .get_personal_group_name_pattern()
294 294
295 295 return htmlfill.render(
296 296 render('admin/settings/settings.mako'),
297 297 defaults=self._form_defaults(),
298 298 encoding="UTF-8",
299 299 force_defaults=False)
300 300
301 301 @HasPermissionAllDecorator('hg.admin')
302 302 @auth.CSRFRequired()
303 303 def settings_visual_update(self):
304 304 """POST /admin/settings/visual: All items in the collection"""
305 305 # url('admin_settings_visual')
306 306 c.active = 'visual'
307 307 application_form = ApplicationVisualisationForm()()
308 308 try:
309 309 form_result = application_form.to_python(dict(request.POST))
310 310 except formencode.Invalid as errors:
311 311 return htmlfill.render(
312 312 render('admin/settings/settings.mako'),
313 313 defaults=errors.value,
314 314 errors=errors.error_dict or {},
315 315 prefix_error=False,
316 316 encoding="UTF-8",
317 317 force_defaults=False
318 318 )
319 319
320 320 try:
321 321 settings = [
322 322 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
323 323 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
324 324 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
325 325 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
326 326 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
327 327 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
328 328 ('show_version', 'rhodecode_show_version', 'bool'),
329 329 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
330 330 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
331 331 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
332 332 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
333 333 ('support_url', 'rhodecode_support_url', 'unicode'),
334 334 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
335 335 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
336 336 ]
337 337 for setting, form_key, type_ in settings:
338 338 sett = SettingsModel().create_or_update_setting(
339 339 setting, form_result[form_key], type_)
340 340 Session().add(sett)
341 341
342 342 Session().commit()
343 343 SettingsModel().invalidate_settings_cache()
344 344 h.flash(_('Updated visualisation settings'), category='success')
345 345 except Exception:
346 346 log.exception("Exception updating visualization settings")
347 347 h.flash(_('Error occurred during updating '
348 348 'visualisation settings'),
349 349 category='error')
350 350
351 351 return redirect(url('admin_settings_visual'))
352 352
353 353 @HasPermissionAllDecorator('hg.admin')
354 354 def settings_visual(self):
355 355 """GET /admin/settings/visual: All items in the collection"""
356 356 # url('admin_settings_visual')
357 357 c.active = 'visual'
358 358
359 359 return htmlfill.render(
360 360 render('admin/settings/settings.mako'),
361 361 defaults=self._form_defaults(),
362 362 encoding="UTF-8",
363 363 force_defaults=False)
364 364
365 365 @HasPermissionAllDecorator('hg.admin')
366 366 @auth.CSRFRequired()
367 367 def settings_issuetracker_test(self):
368 368 if request.is_xhr:
369 369 return h.urlify_commit_message(
370 370 request.POST.get('test_text', ''),
371 371 'repo_group/test_repo1')
372 372 else:
373 373 raise HTTPBadRequest()
374 374
375 375 @HasPermissionAllDecorator('hg.admin')
376 376 @auth.CSRFRequired()
377 377 def settings_issuetracker_delete(self):
378 378 uid = request.POST.get('uid')
379 379 IssueTrackerSettingsModel().delete_entries(uid)
380 380 h.flash(_('Removed issue tracker entry'), category='success')
381 381 return redirect(url('admin_settings_issuetracker'))
382 382
383 383 @HasPermissionAllDecorator('hg.admin')
384 384 def settings_issuetracker(self):
385 385 """GET /admin/settings/issue-tracker: All items in the collection"""
386 386 # url('admin_settings_issuetracker')
387 387 c.active = 'issuetracker'
388 388 defaults = SettingsModel().get_all_settings()
389 389
390 390 entry_key = 'rhodecode_issuetracker_pat_'
391 391
392 392 c.issuetracker_entries = {}
393 393 for k, v in defaults.items():
394 394 if k.startswith(entry_key):
395 395 uid = k[len(entry_key):]
396 396 c.issuetracker_entries[uid] = None
397 397
398 398 for uid in c.issuetracker_entries:
399 399 c.issuetracker_entries[uid] = AttributeDict({
400 400 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
401 401 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
402 402 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
403 403 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
404 404 })
405 405
406 406 return render('admin/settings/settings.mako')
407 407
408 408 @HasPermissionAllDecorator('hg.admin')
409 409 @auth.CSRFRequired()
410 410 def settings_issuetracker_save(self):
411 411 settings_model = IssueTrackerSettingsModel()
412 412
413 413 form = IssueTrackerPatternsForm()().to_python(request.POST)
414 414 if form:
415 415 for uid in form.get('delete_patterns', []):
416 416 settings_model.delete_entries(uid)
417 417
418 418 for pattern in form.get('patterns', []):
419 419 for setting, value, type_ in pattern:
420 420 sett = settings_model.create_or_update_setting(
421 421 setting, value, type_)
422 422 Session().add(sett)
423 423
424 424 Session().commit()
425 425
426 426 SettingsModel().invalidate_settings_cache()
427 427 h.flash(_('Updated issue tracker entries'), category='success')
428 428 return redirect(url('admin_settings_issuetracker'))
429 429
430 430 @HasPermissionAllDecorator('hg.admin')
431 431 @auth.CSRFRequired()
432 432 def settings_email_update(self):
433 433 """POST /admin/settings/email: All items in the collection"""
434 434 # url('admin_settings_email')
435 435 c.active = 'email'
436 436
437 437 test_email = request.POST.get('test_email')
438 438
439 439 if not test_email:
440 440 h.flash(_('Please enter email address'), category='error')
441 441 return redirect(url('admin_settings_email'))
442 442
443 443 email_kwargs = {
444 444 'date': datetime.datetime.now(),
445 445 'user': c.rhodecode_user,
446 446 'rhodecode_version': c.rhodecode_version
447 447 }
448 448
449 449 (subject, headers, email_body,
450 450 email_body_plaintext) = EmailNotificationModel().render_email(
451 451 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
452 452
453 453 recipients = [test_email] if test_email else None
454 454
455 455 run_task(tasks.send_email, recipients, subject,
456 456 email_body_plaintext, email_body)
457 457
458 458 h.flash(_('Send email task created'), category='success')
459 459 return redirect(url('admin_settings_email'))
460 460
461 461 @HasPermissionAllDecorator('hg.admin')
462 462 def settings_email(self):
463 463 """GET /admin/settings/email: All items in the collection"""
464 464 # url('admin_settings_email')
465 465 c.active = 'email'
466 466 c.rhodecode_ini = rhodecode.CONFIG
467 467
468 468 return htmlfill.render(
469 469 render('admin/settings/settings.mako'),
470 470 defaults=self._form_defaults(),
471 471 encoding="UTF-8",
472 472 force_defaults=False)
473 473
474 474 @HasPermissionAllDecorator('hg.admin')
475 475 @auth.CSRFRequired()
476 476 def settings_hooks_update(self):
477 477 """POST or DELETE /admin/settings/hooks: All items in the collection"""
478 478 # url('admin_settings_hooks')
479 479 c.active = 'hooks'
480 480 if c.visual.allow_custom_hooks_settings:
481 481 ui_key = request.POST.get('new_hook_ui_key')
482 482 ui_value = request.POST.get('new_hook_ui_value')
483 483
484 484 hook_id = request.POST.get('hook_id')
485 485 new_hook = False
486 486
487 487 model = SettingsModel()
488 488 try:
489 489 if ui_value and ui_key:
490 490 model.create_or_update_hook(ui_key, ui_value)
491 491 h.flash(_('Added new hook'), category='success')
492 492 new_hook = True
493 493 elif hook_id:
494 494 RhodeCodeUi.delete(hook_id)
495 495 Session().commit()
496 496
497 497 # check for edits
498 498 update = False
499 499 _d = request.POST.dict_of_lists()
500 500 for k, v in zip(_d.get('hook_ui_key', []),
501 501 _d.get('hook_ui_value_new', [])):
502 502 model.create_or_update_hook(k, v)
503 503 update = True
504 504
505 505 if update and not new_hook:
506 506 h.flash(_('Updated hooks'), category='success')
507 507 Session().commit()
508 508 except Exception:
509 509 log.exception("Exception during hook creation")
510 510 h.flash(_('Error occurred during hook creation'),
511 511 category='error')
512 512
513 513 return redirect(url('admin_settings_hooks'))
514 514
515 515 @HasPermissionAllDecorator('hg.admin')
516 516 def settings_hooks(self):
517 517 """GET /admin/settings/hooks: All items in the collection"""
518 518 # url('admin_settings_hooks')
519 519 c.active = 'hooks'
520 520
521 521 model = SettingsModel()
522 522 c.hooks = model.get_builtin_hooks()
523 523 c.custom_hooks = model.get_custom_hooks()
524 524
525 525 return htmlfill.render(
526 526 render('admin/settings/settings.mako'),
527 527 defaults=self._form_defaults(),
528 528 encoding="UTF-8",
529 529 force_defaults=False)
530 530
531 531 @HasPermissionAllDecorator('hg.admin')
532 532 def settings_search(self):
533 533 """GET /admin/settings/search: All items in the collection"""
534 534 # url('admin_settings_search')
535 535 c.active = 'search'
536 536
537 537 from rhodecode.lib.index import searcher_from_config
538 538 searcher = searcher_from_config(config)
539 539 c.statistics = searcher.statistics()
540 540
541 541 return render('admin/settings/settings.mako')
542 542
543 543 @HasPermissionAllDecorator('hg.admin')
544 544 def settings_supervisor(self):
545 545 c.rhodecode_ini = rhodecode.CONFIG
546 546 c.active = 'supervisor'
547 547
548 548 c.supervisor_procs = OrderedDict([
549 549 (SUPERVISOR_MASTER, {}),
550 550 ])
551 551
552 552 c.log_size = 10240
553 553 supervisor = SupervisorModel()
554 554
555 555 _connection = supervisor.get_connection(
556 556 c.rhodecode_ini.get('supervisor.uri'))
557 557 c.connection_error = None
558 558 try:
559 559 _connection.supervisor.getAllProcessInfo()
560 560 except Exception as e:
561 561 c.connection_error = str(e)
562 562 log.exception("Exception reading supervisor data")
563 563 return render('admin/settings/settings.mako')
564 564
565 565 groupid = c.rhodecode_ini.get('supervisor.group_id')
566 566
567 567 # feed our group processes to the main
568 568 for proc in supervisor.get_group_processes(_connection, groupid):
569 569 c.supervisor_procs[proc['name']] = {}
570 570
571 571 for k in c.supervisor_procs.keys():
572 572 try:
573 573 # master process info
574 574 if k == SUPERVISOR_MASTER:
575 575 _data = supervisor.get_master_state(_connection)
576 576 _data['name'] = 'supervisor master'
577 577 _data['description'] = 'pid %s, id: %s, ver: %s' % (
578 578 _data['pid'], _data['id'], _data['ver'])
579 579 c.supervisor_procs[k] = _data
580 580 else:
581 581 procid = groupid + ":" + k
582 582 c.supervisor_procs[k] = supervisor.get_process_info(_connection, procid)
583 583 except Exception as e:
584 584 log.exception("Exception reading supervisor data")
585 585 c.supervisor_procs[k] = {'_rhodecode_error': str(e)}
586 586
587 587 return render('admin/settings/settings.mako')
588 588
589 589 @HasPermissionAllDecorator('hg.admin')
590 590 def settings_supervisor_log(self, procid):
591 591 import rhodecode
592 592 c.rhodecode_ini = rhodecode.CONFIG
593 593 c.active = 'supervisor_tail'
594 594
595 595 supervisor = SupervisorModel()
596 596 _connection = supervisor.get_connection(c.rhodecode_ini.get('supervisor.uri'))
597 597 groupid = c.rhodecode_ini.get('supervisor.group_id')
598 598 procid = groupid + ":" + procid if procid != SUPERVISOR_MASTER else procid
599 599
600 600 c.log_size = 10240
601 601 offset = abs(safe_int(request.GET.get('offset', c.log_size))) * -1
602 602 c.log = supervisor.read_process_log(_connection, procid, offset, 0)
603 603
604 604 return render('admin/settings/settings.mako')
605 605
606 606 @HasPermissionAllDecorator('hg.admin')
607 607 @auth.CSRFRequired()
608 608 def settings_labs_update(self):
609 609 """POST /admin/settings/labs: All items in the collection"""
610 610 # url('admin_settings/labs', method={'POST'})
611 611 c.active = 'labs'
612 612
613 613 application_form = LabsSettingsForm()()
614 614 try:
615 615 form_result = application_form.to_python(dict(request.POST))
616 616 except formencode.Invalid as errors:
617 617 h.flash(
618 618 _('Some form inputs contain invalid data.'),
619 619 category='error')
620 620 return htmlfill.render(
621 621 render('admin/settings/settings.mako'),
622 622 defaults=errors.value,
623 623 errors=errors.error_dict or {},
624 624 prefix_error=False,
625 625 encoding='UTF-8',
626 626 force_defaults=False
627 627 )
628 628
629 629 try:
630 630 session = Session()
631 631 for setting in _LAB_SETTINGS:
632 632 setting_name = setting.key[len('rhodecode_'):]
633 633 sett = SettingsModel().create_or_update_setting(
634 634 setting_name, form_result[setting.key], setting.type)
635 635 session.add(sett)
636 636
637 637 except Exception:
638 638 log.exception('Exception while updating lab settings')
639 639 h.flash(_('Error occurred during updating labs settings'),
640 640 category='error')
641 641 else:
642 642 Session().commit()
643 643 SettingsModel().invalidate_settings_cache()
644 644 h.flash(_('Updated Labs settings'), category='success')
645 645 return redirect(url('admin_settings_labs'))
646 646
647 647 return htmlfill.render(
648 648 render('admin/settings/settings.mako'),
649 649 defaults=self._form_defaults(),
650 650 encoding='UTF-8',
651 651 force_defaults=False)
652 652
653 653 @HasPermissionAllDecorator('hg.admin')
654 654 def settings_labs(self):
655 655 """GET /admin/settings/labs: All items in the collection"""
656 656 # url('admin_settings_labs')
657 657 if not c.labs_active:
658 658 redirect(url('admin_settings'))
659 659
660 660 c.active = 'labs'
661 661 c.lab_settings = _LAB_SETTINGS
662 662
663 663 return htmlfill.render(
664 664 render('admin/settings/settings.mako'),
665 665 defaults=self._form_defaults(),
666 666 encoding='UTF-8',
667 667 force_defaults=False)
668 668
669 669 def _form_defaults(self):
670 670 defaults = SettingsModel().get_all_settings()
671 671 defaults.update(self._get_hg_ui_settings())
672 672 defaults.update({
673 673 'new_svn_branch': '',
674 674 'new_svn_tag': '',
675 675 })
676 676 return defaults
677 677
678 678
679 679 # :param key: name of the setting including the 'rhodecode_' prefix
680 680 # :param type: the RhodeCodeSetting type to use.
681 681 # :param group: the i18ned group in which we should dispaly this setting
682 682 # :param label: the i18ned label we should display for this setting
683 683 # :param help: the i18ned help we should dispaly for this setting
684 684 LabSetting = collections.namedtuple(
685 685 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
686 686
687 687
688 688 # This list has to be kept in sync with the form
689 689 # rhodecode.model.forms.LabsSettingsForm.
690 690 _LAB_SETTINGS = [
691 691
692 692 ]
@@ -1,391 +1,390 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pylons
22 22 import deform
23 23 import logging
24 24 import colander
25 25 import peppercorn
26 26 import webhelpers.paginate
27 27
28 28 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 from rhodecode.apps.admin.navigation import navigation_list
32 33 from rhodecode.lib import auth
33 34 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 35 from rhodecode.lib.utils2 import safe_int
35 36 from rhodecode.lib.helpers import Page
36 37 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 38 from rhodecode.model.scm import ScmModel
38 39 from rhodecode.model.integration import IntegrationModel
39 from rhodecode.admin.navigation import navigation_list
40 40 from rhodecode.translation import _
41 41 from rhodecode.integrations import integration_type_registry
42 42 from rhodecode.model.validation_schema.schemas.integration_schema import (
43 43 make_integration_schema, IntegrationScopeType)
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class IntegrationSettingsViewBase(object):
49 49 """ Base Integration settings view used by both repo / global settings """
50 50
51 51 def __init__(self, context, request):
52 52 self.context = context
53 53 self.request = request
54 54 self._load_general_context()
55 55
56 56 if not self.perm_check(request.user):
57 57 raise HTTPForbidden()
58 58
59 59 def _load_general_context(self):
60 60 """
61 61 This avoids boilerplate for repo/global+list/edit+views/templates
62 62 by doing all possible contexts at the same time however it should
63 63 be split up into separate functions once more "contexts" exist
64 64 """
65 65
66 66 self.IntegrationType = None
67 67 self.repo = None
68 68 self.repo_group = None
69 69 self.integration = None
70 70 self.integrations = {}
71 71
72 72 request = self.request
73 73
74 74 if 'repo_name' in request.matchdict: # in repo settings context
75 75 repo_name = request.matchdict['repo_name']
76 76 self.repo = Repository.get_by_repo_name(repo_name)
77 77
78 if 'repo_group_name' in request.matchdict: # in group settings context
78 if 'repo_group_name' in request.matchdict: # in group settings context
79 79 repo_group_name = request.matchdict['repo_group_name']
80 80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
81 81
82
83 82 if 'integration' in request.matchdict: # integration type context
84 83 integration_type = request.matchdict['integration']
85 84 self.IntegrationType = integration_type_registry[integration_type]
86 85
87 86 if 'integration_id' in request.matchdict: # single integration context
88 87 integration_id = request.matchdict['integration_id']
89 88 self.integration = Integration.get(integration_id)
90 89
91 90 # extra perms check just in case
92 91 if not self._has_perms_for_integration(self.integration):
93 92 raise HTTPForbidden()
94 93
95 94 self.settings = self.integration and self.integration.settings or {}
96 95 self.admin_view = not (self.repo or self.repo_group)
97 96
98 97 def _has_perms_for_integration(self, integration):
99 98 perms = self.request.user.permissions
100 99
101 100 if 'hg.admin' in perms['global']:
102 101 return True
103 102
104 103 if integration.repo:
105 104 return perms['repositories'].get(
106 105 integration.repo.repo_name) == 'repository.admin'
107 106
108 107 if integration.repo_group:
109 108 return perms['repositories_groups'].get(
110 109 integration.repo_group.group_name) == 'group.admin'
111 110
112 111 return False
113 112
114 113 def _template_c_context(self):
115 114 # TODO: dan: this is a stopgap in order to inherit from current pylons
116 115 # based admin/repo settings templates - this should be removed entirely
117 116 # after port to pyramid
118 117
119 118 c = pylons.tmpl_context
120 119 c.active = 'integrations'
121 120 c.rhodecode_user = self.request.user
122 121 c.repo = self.repo
123 122 c.repo_group = self.repo_group
124 123 c.repo_name = self.repo and self.repo.repo_name or None
125 124 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
126 125
127 126 if self.repo:
128 127 c.repo_info = self.repo
129 128 c.rhodecode_db_repo = self.repo
130 129 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
131 130 else:
132 131 c.navlist = navigation_list(self.request)
133 132
134 133 return c
135 134
136 135 def _form_schema(self):
137 136 schema = make_integration_schema(IntegrationType=self.IntegrationType,
138 137 settings=self.settings)
139 138
140 139 # returns a clone, important if mutating the schema later
141 140 return schema.bind(
142 141 permissions=self.request.user.permissions,
143 142 no_scope=not self.admin_view)
144 143
145 144 def _form_defaults(self):
146 145 defaults = {}
147 146
148 147 if self.integration:
149 148 defaults['settings'] = self.integration.settings or {}
150 149 defaults['options'] = {
151 150 'name': self.integration.name,
152 151 'enabled': self.integration.enabled,
153 152 'scope': {
154 153 'repo': self.integration.repo,
155 154 'repo_group': self.integration.repo_group,
156 155 'child_repos_only': self.integration.child_repos_only,
157 156 },
158 157 }
159 158 else:
160 159 if self.repo:
161 160 scope = _('{repo_name} repository').format(
162 161 repo_name=self.repo.repo_name)
163 162 elif self.repo_group:
164 163 scope = _('{repo_group_name} repo group').format(
165 164 repo_group_name=self.repo_group.group_name)
166 165 else:
167 166 scope = _('Global')
168 167
169 168 defaults['options'] = {
170 169 'enabled': True,
171 170 'name': _('{name} integration').format(
172 171 name=self.IntegrationType.display_name),
173 172 }
174 173 defaults['options']['scope'] = {
175 174 'repo': self.repo,
176 175 'repo_group': self.repo_group,
177 176 }
178 177
179 178 return defaults
180 179
181 180 def _delete_integration(self, integration):
182 181 Session().delete(self.integration)
183 182 Session().commit()
184 183 self.request.session.flash(
185 184 _('Integration {integration_name} deleted successfully.').format(
186 185 integration_name=self.integration.name),
187 186 queue='success')
188 187
189 188 if self.repo:
190 189 redirect_to = self.request.route_url(
191 190 'repo_integrations_home', repo_name=self.repo.repo_name)
192 191 elif self.repo_group:
193 192 redirect_to = self.request.route_url(
194 193 'repo_group_integrations_home',
195 194 repo_group_name=self.repo_group.group_name)
196 195 else:
197 196 redirect_to = self.request.route_url('global_integrations_home')
198 197 raise HTTPFound(redirect_to)
199 198
200 199 def settings_get(self, defaults=None, form=None):
201 200 """
202 201 View that displays the integration settings as a form.
203 202 """
204 203
205 204 defaults = defaults or self._form_defaults()
206 205 schema = self._form_schema()
207 206
208 207 if self.integration:
209 208 buttons = ('submit', 'delete')
210 209 else:
211 210 buttons = ('submit',)
212 211
213 212 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
214 213
215 214 template_context = {
216 215 'form': form,
217 216 'current_IntegrationType': self.IntegrationType,
218 217 'integration': self.integration,
219 218 'c': self._template_c_context(),
220 219 }
221 220
222 221 return template_context
223 222
224 223 @auth.CSRFRequired()
225 224 def settings_post(self):
226 225 """
227 226 View that validates and stores the integration settings.
228 227 """
229 228 controls = self.request.POST.items()
230 229 pstruct = peppercorn.parse(controls)
231 230
232 231 if self.integration and pstruct.get('delete'):
233 232 return self._delete_integration(self.integration)
234 233
235 234 schema = self._form_schema()
236 235
237 236 skip_settings_validation = False
238 237 if self.integration and 'enabled' not in pstruct.get('options', {}):
239 238 skip_settings_validation = True
240 239 schema['settings'].validator = None
241 240 for field in schema['settings'].children:
242 241 field.validator = None
243 242 field.missing = ''
244 243
245 244 if self.integration:
246 245 buttons = ('submit', 'delete')
247 246 else:
248 247 buttons = ('submit',)
249 248
250 249 form = deform.Form(schema, buttons=buttons)
251 250
252 251 if not self.admin_view:
253 252 # scope is read only field in these cases, and has to be added
254 253 options = pstruct.setdefault('options', {})
255 254 if 'scope' not in options:
256 255 options['scope'] = IntegrationScopeType().serialize(None, {
257 256 'repo': self.repo,
258 257 'repo_group': self.repo_group,
259 258 })
260 259
261 260 try:
262 261 valid_data = form.validate_pstruct(pstruct)
263 262 except deform.ValidationFailure as e:
264 263 self.request.session.flash(
265 264 _('Errors exist when saving integration settings. '
266 265 'Please check the form inputs.'),
267 266 queue='error')
268 267 return self.settings_get(form=e)
269 268
270 269 if not self.integration:
271 270 self.integration = Integration()
272 271 self.integration.integration_type = self.IntegrationType.key
273 272 Session().add(self.integration)
274 273
275 274 scope = valid_data['options']['scope']
276 275
277 276 IntegrationModel().update_integration(self.integration,
278 277 name=valid_data['options']['name'],
279 278 enabled=valid_data['options']['enabled'],
280 279 settings=valid_data['settings'],
281 280 repo=scope['repo'],
282 281 repo_group=scope['repo_group'],
283 282 child_repos_only=scope['child_repos_only'],
284 283 )
285 284
286 285 self.integration.settings = valid_data['settings']
287 286 Session().commit()
288 287 # Display success message and redirect.
289 288 self.request.session.flash(
290 289 _('Integration {integration_name} updated successfully.').format(
291 290 integration_name=self.IntegrationType.display_name),
292 291 queue='success')
293 292
294 293 # if integration scope changes, we must redirect to the right place
295 294 # keeping in mind if the original view was for /repo/ or /_admin/
296 295 admin_view = not (self.repo or self.repo_group)
297 296
298 297 if self.integration.repo and not admin_view:
299 298 redirect_to = self.request.route_path(
300 299 'repo_integrations_edit',
301 300 repo_name=self.integration.repo.repo_name,
302 301 integration=self.integration.integration_type,
303 302 integration_id=self.integration.integration_id)
304 303 elif self.integration.repo_group and not admin_view:
305 304 redirect_to = self.request.route_path(
306 305 'repo_group_integrations_edit',
307 306 repo_group_name=self.integration.repo_group.group_name,
308 307 integration=self.integration.integration_type,
309 308 integration_id=self.integration.integration_id)
310 309 else:
311 310 redirect_to = self.request.route_path(
312 311 'global_integrations_edit',
313 312 integration=self.integration.integration_type,
314 313 integration_id=self.integration.integration_id)
315 314
316 315 return HTTPFound(redirect_to)
317 316
318 317 def index(self):
319 318 """ List integrations """
320 319 if self.repo:
321 320 scope = self.repo
322 321 elif self.repo_group:
323 322 scope = self.repo_group
324 323 else:
325 324 scope = 'all'
326 325
327 326 integrations = []
328 327
329 328 for IntType, integration in IntegrationModel().get_integrations(
330 329 scope=scope, IntegrationType=self.IntegrationType):
331 330
332 331 # extra permissions check *just in case*
333 332 if not self._has_perms_for_integration(integration):
334 333 continue
335 334
336 335 integrations.append((IntType, integration))
337 336
338 337 sort_arg = self.request.GET.get('sort', 'name:asc')
339 338 if ':' in sort_arg:
340 339 sort_field, sort_dir = sort_arg.split(':')
341 340 else:
342 341 sort_field = sort_arg, 'asc'
343 342
344 343 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
345 344
346 345 integrations.sort(
347 346 key=lambda x: getattr(x[1], sort_field),
348 347 reverse=(sort_dir == 'desc'))
349 348
350 349 page_url = webhelpers.paginate.PageURL(
351 350 self.request.path, self.request.GET)
352 351 page = safe_int(self.request.GET.get('page', 1), 1)
353 352
354 353 integrations = Page(integrations, page=page, items_per_page=10,
355 354 url=page_url)
356 355
357 356 template_context = {
358 357 'sort_field': sort_field,
359 358 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
360 359 'current_IntegrationType': self.IntegrationType,
361 360 'integrations_list': integrations,
362 361 'available_integrations': integration_type_registry,
363 362 'c': self._template_c_context(),
364 363 'request': self.request,
365 364 }
366 365 return template_context
367 366
368 367 def new_integration(self):
369 368 template_context = {
370 369 'available_integrations': integration_type_registry,
371 370 'c': self._template_c_context(),
372 371 }
373 372 return template_context
374 373
375 374
376 375 class GlobalIntegrationsView(IntegrationSettingsViewBase):
377 376 def perm_check(self, user):
378 377 return auth.HasPermissionAll('hg.admin').check_permissions(user=user)
379 378
380 379
381 380 class RepoIntegrationsView(IntegrationSettingsViewBase):
382 381 def perm_check(self, user):
383 return auth.HasRepoPermissionAll('repository.admin'
384 )(repo_name=self.repo.repo_name, user=user)
382 return auth.HasRepoPermissionAll('repository.admin')(
383 repo_name=self.repo.repo_name, user=user)
385 384
386 385
387 386 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
388 387 def perm_check(self, user):
389 return auth.HasRepoGroupPermissionAll('group.admin'
390 )(group_name=self.repo_group.group_name, user=user)
388 return auth.HasRepoGroupPermissionAll('group.admin')(
389 group_name=self.repo_group.group_name, user=user)
391 390
@@ -1,665 +1,665 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 import rhodecode
25 25 from rhodecode.config.routing import ADMIN_PREFIX
26 26 from rhodecode.lib.utils2 import md5
27 27 from rhodecode.model.db import RhodeCodeUi
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 30 from rhodecode.tests import url, assert_session_flash
31 31 from rhodecode.tests.utils import AssertResponse
32 32
33 33
34 34 UPDATE_DATA_QUALNAME = (
35 'rhodecode.admin.views.system_info.AdminSystemInfoSettingsView.get_update_data')
35 'rhodecode.apps.admin.views.system_info.AdminSystemInfoSettingsView.get_update_data')
36 36
37 37
38 38 @pytest.mark.usefixtures('autologin_user', 'app')
39 39 class TestAdminSettingsController(object):
40 40
41 41 @pytest.mark.parametrize('urlname', [
42 42 'admin_settings_vcs',
43 43 'admin_settings_mapping',
44 44 'admin_settings_global',
45 45 'admin_settings_visual',
46 46 'admin_settings_email',
47 47 'admin_settings_hooks',
48 48 'admin_settings_search',
49 49 ])
50 50 def test_simple_get(self, urlname, app):
51 51 app.get(url(urlname))
52 52
53 53 def test_create_custom_hook(self, csrf_token):
54 54 response = self.app.post(
55 55 url('admin_settings_hooks'),
56 56 params={
57 57 'new_hook_ui_key': 'test_hooks_1',
58 58 'new_hook_ui_value': 'cd /tmp',
59 59 'csrf_token': csrf_token})
60 60
61 61 response = response.follow()
62 62 response.mustcontain('test_hooks_1')
63 63 response.mustcontain('cd /tmp')
64 64
65 65 def test_create_custom_hook_delete(self, csrf_token):
66 66 response = self.app.post(
67 67 url('admin_settings_hooks'),
68 68 params={
69 69 'new_hook_ui_key': 'test_hooks_2',
70 70 'new_hook_ui_value': 'cd /tmp2',
71 71 'csrf_token': csrf_token})
72 72
73 73 response = response.follow()
74 74 response.mustcontain('test_hooks_2')
75 75 response.mustcontain('cd /tmp2')
76 76
77 77 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
78 78
79 79 # delete
80 80 self.app.post(
81 81 url('admin_settings_hooks'),
82 82 params={'hook_id': hook_id, 'csrf_token': csrf_token})
83 83 response = self.app.get(url('admin_settings_hooks'))
84 84 response.mustcontain(no=['test_hooks_2'])
85 85 response.mustcontain(no=['cd /tmp2'])
86 86
87 87
88 88 @pytest.mark.usefixtures('autologin_user', 'app')
89 89 class TestAdminSettingsGlobal(object):
90 90
91 91 def test_pre_post_code_code_active(self, csrf_token):
92 92 pre_code = 'rc-pre-code-187652122'
93 93 post_code = 'rc-postcode-98165231'
94 94
95 95 response = self.post_and_verify_settings({
96 96 'rhodecode_pre_code': pre_code,
97 97 'rhodecode_post_code': post_code,
98 98 'csrf_token': csrf_token,
99 99 })
100 100
101 101 response = response.follow()
102 102 response.mustcontain(pre_code, post_code)
103 103
104 104 def test_pre_post_code_code_inactive(self, csrf_token):
105 105 pre_code = 'rc-pre-code-187652122'
106 106 post_code = 'rc-postcode-98165231'
107 107 response = self.post_and_verify_settings({
108 108 'rhodecode_pre_code': '',
109 109 'rhodecode_post_code': '',
110 110 'csrf_token': csrf_token,
111 111 })
112 112
113 113 response = response.follow()
114 114 response.mustcontain(no=[pre_code, post_code])
115 115
116 116 def test_captcha_activate(self, csrf_token):
117 117 self.post_and_verify_settings({
118 118 'rhodecode_captcha_private_key': '1234567890',
119 119 'rhodecode_captcha_public_key': '1234567890',
120 120 'csrf_token': csrf_token,
121 121 })
122 122
123 123 response = self.app.get(ADMIN_PREFIX + '/register')
124 124 response.mustcontain('captcha')
125 125
126 126 def test_captcha_deactivate(self, csrf_token):
127 127 self.post_and_verify_settings({
128 128 'rhodecode_captcha_private_key': '',
129 129 'rhodecode_captcha_public_key': '1234567890',
130 130 'csrf_token': csrf_token,
131 131 })
132 132
133 133 response = self.app.get(ADMIN_PREFIX + '/register')
134 134 response.mustcontain(no=['captcha'])
135 135
136 136 def test_title_change(self, csrf_token):
137 137 old_title = 'RhodeCode'
138 138 new_title = old_title + '_changed'
139 139
140 140 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
141 141 response = self.post_and_verify_settings({
142 142 'rhodecode_title': new_title,
143 143 'csrf_token': csrf_token,
144 144 })
145 145
146 146 response = response.follow()
147 147 response.mustcontain(
148 148 """<div class="branding">- %s</div>""" % new_title)
149 149
150 150 def post_and_verify_settings(self, settings):
151 151 old_title = 'RhodeCode'
152 152 old_realm = 'RhodeCode authentication'
153 153 params = {
154 154 'rhodecode_title': old_title,
155 155 'rhodecode_realm': old_realm,
156 156 'rhodecode_pre_code': '',
157 157 'rhodecode_post_code': '',
158 158 'rhodecode_captcha_private_key': '',
159 159 'rhodecode_captcha_public_key': '',
160 160 'rhodecode_create_personal_repo_group': False,
161 161 'rhodecode_personal_repo_group_pattern': '${username}',
162 162 }
163 163 params.update(settings)
164 164 response = self.app.post(url('admin_settings_global'), params=params)
165 165
166 166 assert_session_flash(response, 'Updated application settings')
167 167 app_settings = SettingsModel().get_all_settings()
168 168 del settings['csrf_token']
169 169 for key, value in settings.iteritems():
170 170 assert app_settings[key] == value.decode('utf-8')
171 171
172 172 return response
173 173
174 174
175 175 @pytest.mark.usefixtures('autologin_user', 'app')
176 176 class TestAdminSettingsVcs(object):
177 177
178 178 def test_contains_svn_default_patterns(self, app):
179 179 response = app.get(url('admin_settings_vcs'))
180 180 expected_patterns = [
181 181 '/trunk',
182 182 '/branches/*',
183 183 '/tags/*',
184 184 ]
185 185 for pattern in expected_patterns:
186 186 response.mustcontain(pattern)
187 187
188 188 def test_add_new_svn_branch_and_tag_pattern(
189 189 self, app, backend_svn, form_defaults, disable_sql_cache,
190 190 csrf_token):
191 191 form_defaults.update({
192 192 'new_svn_branch': '/exp/branches/*',
193 193 'new_svn_tag': '/important_tags/*',
194 194 'csrf_token': csrf_token,
195 195 })
196 196
197 197 response = app.post(
198 198 url('admin_settings_vcs'), params=form_defaults, status=302)
199 199 response = response.follow()
200 200
201 201 # Expect to find the new values on the page
202 202 response.mustcontain('/exp/branches/*')
203 203 response.mustcontain('/important_tags/*')
204 204
205 205 # Expect that those patterns are used to match branches and tags now
206 206 repo = backend_svn['svn-simple-layout'].scm_instance()
207 207 assert 'exp/branches/exp-sphinx-docs' in repo.branches
208 208 assert 'important_tags/v0.5' in repo.tags
209 209
210 210 def test_add_same_svn_value_twice_shows_an_error_message(
211 211 self, app, form_defaults, csrf_token, settings_util):
212 212 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
213 213 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
214 214
215 215 response = app.post(
216 216 url('admin_settings_vcs'),
217 217 params={
218 218 'paths_root_path': form_defaults['paths_root_path'],
219 219 'new_svn_branch': '/test',
220 220 'new_svn_tag': '/test',
221 221 'csrf_token': csrf_token,
222 222 },
223 223 status=200)
224 224
225 225 response.mustcontain("Pattern already exists")
226 226 response.mustcontain("Some form inputs contain invalid data.")
227 227
228 228 @pytest.mark.parametrize('section', [
229 229 'vcs_svn_branch',
230 230 'vcs_svn_tag',
231 231 ])
232 232 def test_delete_svn_patterns(
233 233 self, section, app, csrf_token, settings_util):
234 234 setting = settings_util.create_rhodecode_ui(
235 235 section, '/test_delete', cleanup=False)
236 236
237 237 app.post(
238 238 url('admin_settings_vcs'),
239 239 params={
240 240 '_method': 'delete',
241 241 'delete_svn_pattern': setting.ui_id,
242 242 'csrf_token': csrf_token},
243 243 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
244 244
245 245 @pytest.mark.parametrize('section', [
246 246 'vcs_svn_branch',
247 247 'vcs_svn_tag',
248 248 ])
249 249 def test_delete_svn_patterns_raises_400_when_no_xhr(
250 250 self, section, app, csrf_token, settings_util):
251 251 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
252 252
253 253 app.post(
254 254 url('admin_settings_vcs'),
255 255 params={
256 256 '_method': 'delete',
257 257 'delete_svn_pattern': setting.ui_id,
258 258 'csrf_token': csrf_token},
259 259 status=400)
260 260
261 261 def test_extensions_hgsubversion(self, app, form_defaults, csrf_token):
262 262 form_defaults.update({
263 263 'csrf_token': csrf_token,
264 264 'extensions_hgsubversion': 'True',
265 265 })
266 266 response = app.post(
267 267 url('admin_settings_vcs'),
268 268 params=form_defaults,
269 269 status=302)
270 270
271 271 response = response.follow()
272 272 extensions_input = (
273 273 '<input id="extensions_hgsubversion" '
274 274 'name="extensions_hgsubversion" type="checkbox" '
275 275 'value="True" checked="checked" />')
276 276 response.mustcontain(extensions_input)
277 277
278 278 def test_has_a_section_for_pull_request_settings(self, app):
279 279 response = app.get(url('admin_settings_vcs'))
280 280 response.mustcontain('Pull Request Settings')
281 281
282 282 def test_has_an_input_for_invalidation_of_inline_comments(
283 283 self, app):
284 284 response = app.get(url('admin_settings_vcs'))
285 285 assert_response = AssertResponse(response)
286 286 assert_response.one_element_exists(
287 287 '[name=rhodecode_use_outdated_comments]')
288 288
289 289 @pytest.mark.parametrize('new_value', [True, False])
290 290 def test_allows_to_change_invalidation_of_inline_comments(
291 291 self, app, form_defaults, csrf_token, new_value):
292 292 setting_key = 'use_outdated_comments'
293 293 setting = SettingsModel().create_or_update_setting(
294 294 setting_key, not new_value, 'bool')
295 295 Session().add(setting)
296 296 Session().commit()
297 297
298 298 form_defaults.update({
299 299 'csrf_token': csrf_token,
300 300 'rhodecode_use_outdated_comments': str(new_value),
301 301 })
302 302 response = app.post(
303 303 url('admin_settings_vcs'),
304 304 params=form_defaults,
305 305 status=302)
306 306 response = response.follow()
307 307 setting = SettingsModel().get_setting_by_name(setting_key)
308 308 assert setting.app_settings_value is new_value
309 309
310 310 def test_has_a_section_for_labs_settings_if_enabled(self, app):
311 311 with mock.patch.dict(
312 312 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
313 313 response = self.app.get(url('admin_settings_vcs'))
314 314 response.mustcontain('Labs Settings')
315 315
316 316 def test_has_not_a_section_for_labs_settings_if_disables(self, app):
317 317 with mock.patch.dict(
318 318 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
319 319 response = self.app.get(url('admin_settings_vcs'))
320 320 response.mustcontain(no='Labs Settings')
321 321
322 322 @pytest.mark.parametrize('new_value', [True, False])
323 323 def test_allows_to_change_hg_rebase_merge_strategy(
324 324 self, app, form_defaults, csrf_token, new_value):
325 325 setting_key = 'hg_use_rebase_for_merging'
326 326
327 327 form_defaults.update({
328 328 'csrf_token': csrf_token,
329 329 'rhodecode_' + setting_key: str(new_value),
330 330 })
331 331
332 332 with mock.patch.dict(
333 333 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
334 334 app.post(
335 335 url('admin_settings_vcs'),
336 336 params=form_defaults,
337 337 status=302)
338 338
339 339 setting = SettingsModel().get_setting_by_name(setting_key)
340 340 assert setting.app_settings_value is new_value
341 341
342 342 @pytest.fixture
343 343 def disable_sql_cache(self, request):
344 344 patcher = mock.patch(
345 345 'rhodecode.lib.caching_query.FromCache.process_query')
346 346 request.addfinalizer(patcher.stop)
347 347 patcher.start()
348 348
349 349 @pytest.fixture
350 350 def form_defaults(self):
351 351 from rhodecode.controllers.admin.settings import SettingsController
352 352 controller = SettingsController()
353 353 return controller._form_defaults()
354 354
355 355 # TODO: johbo: What we really want is to checkpoint before a test run and
356 356 # reset the session afterwards.
357 357 @pytest.fixture(scope='class', autouse=True)
358 358 def cleanup_settings(self, request, pylonsapp):
359 359 ui_id = RhodeCodeUi.ui_id
360 360 original_ids = list(
361 361 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
362 362
363 363 @request.addfinalizer
364 364 def cleanup():
365 365 RhodeCodeUi.query().filter(
366 366 ui_id.notin_(original_ids)).delete(False)
367 367
368 368
369 369 @pytest.mark.usefixtures('autologin_user', 'app')
370 370 class TestLabsSettings(object):
371 371 def test_get_settings_page_disabled(self):
372 372 with mock.patch.dict(rhodecode.CONFIG,
373 373 {'labs_settings_active': 'false'}):
374 374 response = self.app.get(url('admin_settings_labs'), status=302)
375 375
376 376 assert response.location.endswith(url('admin_settings'))
377 377
378 378 def test_get_settings_page_enabled(self):
379 379 from rhodecode.controllers.admin import settings
380 380 lab_settings = [
381 381 settings.LabSetting(
382 382 key='rhodecode_bool',
383 383 type='bool',
384 384 group='bool group',
385 385 label='bool label',
386 386 help='bool help'
387 387 ),
388 388 settings.LabSetting(
389 389 key='rhodecode_text',
390 390 type='unicode',
391 391 group='text group',
392 392 label='text label',
393 393 help='text help'
394 394 ),
395 395 ]
396 396 with mock.patch.dict(rhodecode.CONFIG,
397 397 {'labs_settings_active': 'true'}):
398 398 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
399 399 response = self.app.get(url('admin_settings_labs'))
400 400
401 401 assert '<label>bool group:</label>' in response
402 402 assert '<label for="rhodecode_bool">bool label</label>' in response
403 403 assert '<p class="help-block">bool help</p>' in response
404 404 assert 'name="rhodecode_bool" type="checkbox"' in response
405 405
406 406 assert '<label>text group:</label>' in response
407 407 assert '<label for="rhodecode_text">text label</label>' in response
408 408 assert '<p class="help-block">text help</p>' in response
409 409 assert 'name="rhodecode_text" size="60" type="text"' in response
410 410
411 411
412 412 @pytest.mark.usefixtures('app')
413 413 class TestOpenSourceLicenses(object):
414 414
415 415 def _get_url(self):
416 416 return ADMIN_PREFIX + '/settings/open_source'
417 417
418 418 def test_records_are_displayed(self, autologin_user):
419 419 sample_licenses = {
420 420 "python2.7-pytest-2.7.1": {
421 421 "UNKNOWN": None
422 422 },
423 423 "python2.7-Markdown-2.6.2": {
424 424 "BSD-3-Clause": "http://spdx.org/licenses/BSD-3-Clause"
425 425 }
426 426 }
427 427 read_licenses_patch = mock.patch(
428 'rhodecode.admin.views.open_source_licenses.read_opensource_licenses',
428 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
429 429 return_value=sample_licenses)
430 430 with read_licenses_patch:
431 431 response = self.app.get(self._get_url(), status=200)
432 432
433 433 assert_response = AssertResponse(response)
434 434 assert_response.element_contains(
435 435 '.panel-heading', 'Licenses of Third Party Packages')
436 436 for name in sample_licenses:
437 437 response.mustcontain(name)
438 438 for license in sample_licenses[name]:
439 439 assert_response.element_contains('.panel-body', license)
440 440
441 441 def test_records_can_be_read(self, autologin_user):
442 442 response = self.app.get(self._get_url(), status=200)
443 443 assert_response = AssertResponse(response)
444 444 assert_response.element_contains(
445 445 '.panel-heading', 'Licenses of Third Party Packages')
446 446
447 447 def test_forbidden_when_normal_user(self, autologin_regular_user):
448 448 self.app.get(self._get_url(), status=403)
449 449
450 450
451 451 @pytest.mark.usefixtures('app')
452 452 class TestUserSessions(object):
453 453
454 454 def _get_url(self, name='admin_settings_sessions'):
455 455 return {
456 456 'admin_settings_sessions': ADMIN_PREFIX + '/settings/sessions',
457 457 'admin_settings_sessions_cleanup': ADMIN_PREFIX + '/settings/sessions/cleanup'
458 458 }[name]
459 459
460 460 def test_forbidden_when_normal_user(self, autologin_regular_user):
461 461 self.app.get(self._get_url(), status=403)
462 462
463 463 def test_show_sessions_page(self, autologin_user):
464 464 response = self.app.get(self._get_url(), status=200)
465 465 response.mustcontain('file')
466 466
467 467 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
468 468
469 469 post_data = {
470 470 'csrf_token': csrf_token,
471 471 'expire_days': '60'
472 472 }
473 473 response = self.app.post(
474 474 self._get_url('admin_settings_sessions_cleanup'), params=post_data,
475 475 status=302)
476 476 assert_session_flash(response, 'Cleaned up old sessions')
477 477
478 478
479 479 @pytest.mark.usefixtures('app')
480 480 class TestAdminSystemInfo(object):
481 481 def _get_url(self, name='admin_settings_system'):
482 482 return {
483 483 'admin_settings_system': ADMIN_PREFIX + '/settings/system',
484 484 'admin_settings_system_update': ADMIN_PREFIX + '/settings/system/updates',
485 485 }[name]
486 486
487 487 def test_forbidden_when_normal_user(self, autologin_regular_user):
488 488 self.app.get(self._get_url(), status=403)
489 489
490 490 def test_system_info_page(self, autologin_user):
491 491 response = self.app.get(self._get_url())
492 492 response.mustcontain('RhodeCode Community Edition, version {}'.format(
493 493 rhodecode.__version__))
494 494
495 495 def test_system_update_new_version(self, autologin_user):
496 496 update_data = {
497 497 'versions': [
498 498 {
499 499 'version': '100.3.1415926535',
500 500 'general': 'The latest version we are ever going to ship'
501 501 },
502 502 {
503 503 'version': '0.0.0',
504 504 'general': 'The first version we ever shipped'
505 505 }
506 506 ]
507 507 }
508 508 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
509 509 response = self.app.get(self._get_url('admin_settings_system_update'))
510 510 response.mustcontain('A <b>new version</b> is available')
511 511
512 512 def test_system_update_nothing_new(self, autologin_user):
513 513 update_data = {
514 514 'versions': [
515 515 {
516 516 'version': '0.0.0',
517 517 'general': 'The first version we ever shipped'
518 518 }
519 519 ]
520 520 }
521 521 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
522 522 response = self.app.get(self._get_url('admin_settings_system_update'))
523 523 response.mustcontain(
524 524 'You already have the <b>latest</b> stable version.')
525 525
526 526 def test_system_update_bad_response(self, autologin_user):
527 527 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
528 528 response = self.app.get(self._get_url('admin_settings_system_update'))
529 529 response.mustcontain(
530 530 'Bad data sent from update server')
531 531
532 532
533 533 @pytest.mark.usefixtures("app")
534 534 class TestAdminSettingsIssueTracker(object):
535 535 RC_PREFIX = 'rhodecode_'
536 536 SHORT_PATTERN_KEY = 'issuetracker_pat_'
537 537 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
538 538
539 539 def test_issuetracker_index(self, autologin_user):
540 540 response = self.app.get(url('admin_settings_issuetracker'))
541 541 assert response.status_code == 200
542 542
543 543 def test_add_empty_issuetracker_pattern(
544 544 self, request, autologin_user, csrf_token):
545 545 post_url = url('admin_settings_issuetracker_save')
546 546 post_data = {
547 547 'csrf_token': csrf_token
548 548 }
549 549 self.app.post(post_url, post_data, status=302)
550 550
551 551 def test_add_issuetracker_pattern(
552 552 self, request, autologin_user, csrf_token):
553 553 pattern = 'issuetracker_pat'
554 554 another_pattern = pattern+'1'
555 555 post_url = url('admin_settings_issuetracker_save')
556 556 post_data = {
557 557 'new_pattern_pattern_0': pattern,
558 558 'new_pattern_url_0': 'url',
559 559 'new_pattern_prefix_0': 'prefix',
560 560 'new_pattern_description_0': 'description',
561 561 'new_pattern_pattern_1': another_pattern,
562 562 'new_pattern_url_1': 'url1',
563 563 'new_pattern_prefix_1': 'prefix1',
564 564 'new_pattern_description_1': 'description1',
565 565 'csrf_token': csrf_token
566 566 }
567 567 self.app.post(post_url, post_data, status=302)
568 568 settings = SettingsModel().get_all_settings()
569 569 self.uid = md5(pattern)
570 570 assert settings[self.PATTERN_KEY+self.uid] == pattern
571 571 self.another_uid = md5(another_pattern)
572 572 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
573 573
574 574 @request.addfinalizer
575 575 def cleanup():
576 576 defaults = SettingsModel().get_all_settings()
577 577
578 578 entries = [name for name in defaults if (
579 579 (self.uid in name) or (self.another_uid) in name)]
580 580 start = len(self.RC_PREFIX)
581 581 for del_key in entries:
582 582 # TODO: anderson: get_by_name needs name without prefix
583 583 entry = SettingsModel().get_setting_by_name(del_key[start:])
584 584 Session().delete(entry)
585 585
586 586 Session().commit()
587 587
588 588 def test_edit_issuetracker_pattern(
589 589 self, autologin_user, backend, csrf_token, request):
590 590 old_pattern = 'issuetracker_pat'
591 591 old_uid = md5(old_pattern)
592 592 pattern = 'issuetracker_pat_new'
593 593 self.new_uid = md5(pattern)
594 594
595 595 SettingsModel().create_or_update_setting(
596 596 self.SHORT_PATTERN_KEY+old_uid, old_pattern, 'unicode')
597 597
598 598 post_url = url('admin_settings_issuetracker_save')
599 599 post_data = {
600 600 'new_pattern_pattern_0': pattern,
601 601 'new_pattern_url_0': 'url',
602 602 'new_pattern_prefix_0': 'prefix',
603 603 'new_pattern_description_0': 'description',
604 604 'uid': old_uid,
605 605 'csrf_token': csrf_token
606 606 }
607 607 self.app.post(post_url, post_data, status=302)
608 608 settings = SettingsModel().get_all_settings()
609 609 assert settings[self.PATTERN_KEY+self.new_uid] == pattern
610 610 assert self.PATTERN_KEY+old_uid not in settings
611 611
612 612 @request.addfinalizer
613 613 def cleanup():
614 614 IssueTrackerSettingsModel().delete_entries(self.new_uid)
615 615
616 616 def test_replace_issuetracker_pattern_description(
617 617 self, autologin_user, csrf_token, request, settings_util):
618 618 prefix = 'issuetracker'
619 619 pattern = 'issuetracker_pat'
620 620 self.uid = md5(pattern)
621 621 pattern_key = '_'.join([prefix, 'pat', self.uid])
622 622 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
623 623 desc_key = '_'.join([prefix, 'desc', self.uid])
624 624 rc_desc_key = '_'.join(['rhodecode', desc_key])
625 625 new_description = 'new_description'
626 626
627 627 settings_util.create_rhodecode_setting(
628 628 pattern_key, pattern, 'unicode', cleanup=False)
629 629 settings_util.create_rhodecode_setting(
630 630 desc_key, 'old description', 'unicode', cleanup=False)
631 631
632 632 post_url = url('admin_settings_issuetracker_save')
633 633 post_data = {
634 634 'new_pattern_pattern_0': pattern,
635 635 'new_pattern_url_0': 'url',
636 636 'new_pattern_prefix_0': 'prefix',
637 637 'new_pattern_description_0': new_description,
638 638 'uid': self.uid,
639 639 'csrf_token': csrf_token
640 640 }
641 641 self.app.post(post_url, post_data, status=302)
642 642 settings = SettingsModel().get_all_settings()
643 643 assert settings[rc_pattern_key] == pattern
644 644 assert settings[rc_desc_key] == new_description
645 645
646 646 @request.addfinalizer
647 647 def cleanup():
648 648 IssueTrackerSettingsModel().delete_entries(self.uid)
649 649
650 650 def test_delete_issuetracker_pattern(
651 651 self, autologin_user, backend, csrf_token, settings_util):
652 652 pattern = 'issuetracker_pat'
653 653 uid = md5(pattern)
654 654 settings_util.create_rhodecode_setting(
655 655 self.SHORT_PATTERN_KEY+uid, pattern, 'unicode', cleanup=False)
656 656
657 657 post_url = url('admin_issuetracker_delete')
658 658 post_data = {
659 659 '_method': 'delete',
660 660 'uid': uid,
661 661 'csrf_token': csrf_token
662 662 }
663 663 self.app.post(post_url, post_data, status=302)
664 664 settings = SettingsModel().get_all_settings()
665 665 assert 'rhodecode_%s%s' % (self.SHORT_PATTERN_KEY, uid) not in settings
General Comments 0
You need to be logged in to leave comments. Login now